diff --git a/.backportrc.json b/.backportrc.json index 7658c6ac39734..1953f27b7c453 100644 --- a/.backportrc.json +++ b/.backportrc.json @@ -1,5 +1,5 @@ { "upstream": "elastic/kibana", - "branches": [{ "name": "7.x", "checked": true }, "7.1", "7.0", "6.8", "6.7", "6.6", "6.5", "6.4", "6.3", "6.2", "6.1", "6.0", "5.6"], + "branches": [{ "name": "7.x", "checked": true }, "7.2", "7.1", "7.0", "6.8", "6.7", "6.6", "6.5", "6.4", "6.3", "6.2", "6.1", "6.0", "5.6"], "labels": ["backport"] } diff --git a/.ci/jobs.yml b/.ci/jobs.yml index 596f22215a2a6..b2bc16859483b 100644 --- a/.ci/jobs.yml +++ b/.ci/jobs.yml @@ -21,6 +21,9 @@ JOB: - x-pack-ciGroup5 - x-pack-ciGroup6 - x-pack-ciGroup7 + - x-pack-ciGroup8 + - x-pack-ciGroup9 + - x-pack-ciGroup10 # `~` is yaml for `null` exclude: ~ diff --git a/.eslintignore b/.eslintignore index 56ca8fffca631..ea431347b32e0 100644 --- a/.eslintignore +++ b/.eslintignore @@ -31,9 +31,11 @@ bower_components /x-pack/plugins/**/__tests__/fixtures/** /packages/kbn-interpreter/src/common/lib/grammar.js /x-pack/plugins/canvas/canvas_plugin +/x-pack/plugins/canvas/storybook /x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts /x-pack/plugins/infra/common/graphql/types.ts /x-pack/plugins/infra/public/graphql/types.ts /x-pack/plugins/infra/server/graphql/types.ts +**/graphql/types.ts **/*.js.snap !/.eslintrc.js diff --git a/.eslintrc.js b/.eslintrc.js index d09551ef27422..51d9fd65dae94 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -184,7 +184,13 @@ module.exports = { // instructs import/no-extraneous-dependencies to treat modules // in plugins/ or ui/ namespace as "core modules" so they don't // trigger failures for not being listed in package.json - 'import/core-modules': ['plugins', 'legacy/ui', 'uiExports'], + 'import/core-modules': [ + 'plugins', + 'legacy/ui', + 'uiExports', + // TODO: Remove once https://github.com/benmosher/eslint-plugin-import/issues/1374 is fixed + 'querystring', + ], 'import/resolver': { '@kbn/eslint-import-resolver-kibana': { @@ -400,10 +406,181 @@ module.exports = { * SIEM overrides */ { - files: ['x-pack/plugins/siem/**/*.ts'], + // front end typescript and javascript files only + files: ['x-pack/plugins/siem/public/**/*.{js,ts,tsx}'], + rules: { + 'import/no-nodejs-modules': 'error', + 'no-restricted-imports': [ + 'error', + { + // prevents UI code from importing server side code and then webpack including it when doing builds + patterns: ['**/server/*'], + }, + ], + }, + }, + { + // typescript only for front and back end + files: ['x-pack/plugins/siem/**/*.{ts,tsx}'], rules: { + // This will be turned on after bug fixes are complete + // '@typescript-eslint/explicit-member-accessibility': 'warn', + '@typescript-eslint/no-this-alias': 'error', '@typescript-eslint/no-explicit-any': 'error', - 'import/order': 'error', + '@typescript-eslint/no-useless-constructor': 'error', + // This will be turned on after bug fixes are complete + // '@typescript-eslint/no-object-literal-type-assertion': 'warn', + '@typescript-eslint/unified-signatures': 'error', + + // eventually we want this to be a warn and then an error since this is a recommended linter rule + // for now, keeping it commented out to avoid too much IDE noise until the other linter issues + // are fixed in the next release or two + // '@typescript-eslint/explicit-function-return-type': 'warn', + + // these rules cannot be turned on and tested at the moment until this issue is resolved: + // https://github.com/prettier/prettier-eslint/issues/201 + // '@typescript-eslint/await-thenable': 'error', + // '@typescript-eslint/no-non-null-assertion': 'error' + // '@typescript-eslint/no-unnecessary-type-assertion': 'error', + // '@typescript-eslint/no-unused-vars': 'error', + // '@typescript-eslint/prefer-includes': 'error', + // '@typescript-eslint/prefer-string-starts-ends-with': 'error', + // '@typescript-eslint/promise-function-async': 'error', + // '@typescript-eslint/prefer-regexp-exec': 'error', + // '@typescript-eslint/promise-function-async': 'error', + // '@typescript-eslint/require-array-sort-compare': 'error', + // '@typescript-eslint/restrict-plus-operands': 'error', + // '@typescript-eslint/unbound-method': 'error', + }, + }, + { + // typescript and javascript for front and back end + files: ['x-pack/plugins/siem/**/*.{js,ts,tsx}'], + plugins: ['eslint-plugin-node', 'react'], + rules: { + 'accessor-pairs': 'error', + 'array-callback-return': 'error', + 'no-array-constructor': 'error', + // This will be turned on after bug fixes are mostly completed + // 'arrow-body-style': ['warn', 'as-needed'], + complexity: 'warn', + // This will be turned on after bug fixes are mostly completed + // 'consistent-return': 'warn', + // This will be turned on after bug fixes are mostly completed + // 'func-style': ['warn', 'expression'], + // These will be turned on after bug fixes are mostly completed and we can + // run a fix-lint + /* + 'import/order': [ + 'warn', + { + groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], + 'newlines-between': 'always', + }, + ], + */ + 'node/no-deprecated-api': 'error', + 'no-bitwise': 'error', + 'no-continue': 'error', + 'no-dupe-keys': 'error', + 'no-duplicate-case': 'error', + // This will be turned on after bug fixes are mostly completed + // 'no-duplicate-imports': 'warn', + 'no-empty-character-class': 'error', + 'no-empty-pattern': 'error', + 'no-ex-assign': 'error', + 'no-extend-native': 'error', + 'no-extra-bind': 'error', + 'no-extra-boolean-cast': 'error', + 'no-extra-label': 'error', + 'no-floating-decimal': 'error', + 'no-func-assign': 'error', + 'no-implicit-globals': 'error', + 'no-implied-eval': 'error', + 'no-invalid-regexp': 'error', + 'no-inner-declarations': 'error', + 'no-lone-blocks': 'error', + 'no-multi-assign': 'error', + 'no-misleading-character-class': 'error', + 'no-new-symbol': 'error', + 'no-obj-calls': 'error', + // This will be turned on after bug fixes are mostly complete + // 'no-param-reassign': 'warn', + 'no-process-exit': 'error', + 'no-prototype-builtins': 'error', + // This will be turned on after bug fixes are mostly complete + // 'no-return-await': 'warn', + 'no-self-compare': 'error', + 'no-shadow-restricted-names': 'error', + 'no-sparse-arrays': 'error', + 'no-this-before-super': 'error', + // This will be turned on after bug fixes are mostly complete + // 'no-undef': 'warn', + 'no-unreachable': 'error', + 'no-unsafe-finally': 'error', + 'no-useless-call': 'error', + // This will be turned on after bug fixes are mostly complete + // 'no-useless-catch': 'warn', + 'no-useless-concat': 'error', + 'no-useless-computed-key': 'error', + // This will be turned on after bug fixes are mostly complete + // 'no-useless-escape': 'warn', + 'no-useless-rename': 'error', + // This will be turned on after bug fixes are mostly complete + // 'no-useless-return': 'warn', + // This will be turned on after bug fixers are mostly complete + // 'no-void': 'warn', + 'one-var-declaration-per-line': 'error', + 'prefer-object-spread': 'error', + 'prefer-promise-reject-errors': 'error', + 'prefer-rest-params': 'error', + 'prefer-spread': 'error', + // This style will be turned on after most bugs are fixed + // 'prefer-template': 'warn', + // This style will be turned on after most bugs are fixed + // quotes: ['warn', 'single', { avoidEscape: true }], + 'react/boolean-prop-naming': 'error', + 'react/button-has-type': 'error', + 'react/forbid-dom-props': 'error', + 'react/no-access-state-in-setstate': 'error', + // This style will be turned on after most bugs are fixed + // 'react/no-children-prop': 'warn', + 'react/no-danger-with-children': 'error', + 'react/no-deprecated': 'error', + 'react/no-did-mount-set-state': 'error', + // Re-enable once we have better options per this issue: + // https://github.com/airbnb/javascript/issues/1875 + // 'react/no-did-update-set-state': 'error', + 'react/no-direct-mutation-state': 'error', + 'react/no-find-dom-node': 'error', + 'react/no-redundant-should-component-update': 'error', + 'react/no-render-return-value': 'error', + 'react/no-typos': 'error', + 'react/no-string-refs': 'error', + 'react/no-this-in-sfc': 'error', + 'react/no-unescaped-entities': 'error', + 'react/no-unsafe': 'error', + 'react/no-unused-prop-types': 'error', + 'react/no-unused-state': 'error', + // will introduced after the other warns are fixed + // 'react/sort-comp': 'error', + 'react/void-dom-elements-no-children': 'error', + 'react/jsx-boolean-value': ['error', 'warn'], + // will introduced after the other warns are fixed + // 'react/jsx-no-bind': 'error', + 'react/jsx-no-comment-textnodes': 'error', + 'react/jsx-no-literals': 'error', + 'react/jsx-no-target-blank': 'error', + 'react/jsx-fragments': 'error', + 'react/jsx-sort-default-props': 'error', + // might be introduced after the other warns are fixed + // 'react/jsx-sort-props': 'error', + 'react/jsx-tag-spacing': 'error', + 'require-atomic-updates': 'error', + 'rest-spread-spacing': ['error', 'never'], + 'symbol-description': 'error', + 'template-curly-spacing': 'error', + 'vars-on-top': 'error', }, }, @@ -529,5 +706,16 @@ module.exports = { jquery: true, }, }, + + /** + * TSVB overrides + */ + { + files: ['src/legacy/core_plugins/metrics/**/*.js'], + excludedFiles: 'src/legacy/core_plugins/metrics/index.js', + rules: { + 'import/no-default-export': 'error', + }, + }, ], }; diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ad9591a897813..2655630886667 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -23,12 +23,15 @@ /x-pack/plugins/ml/ @elastic/ml-ui # Operations +/renovate.json5 @elastic/kibana-operations /src/dev/ @elastic/kibana-operations /src/setup_node_env/ @elastic/kibana-operations /src/optimize/ @elastic/kibana-operations # Platform /src/core/ @elastic/kibana-platform +/src/legacy/server/saved_objects/ @elastic/kibana-platform +/src/legacy/ui/public/saved_objects @elastic/kibana-platform # Security /x-pack/plugins/security/ @elastic/kibana-security diff --git a/.gitignore b/.gitignore index 0ed1cf89ba583..efb5c57774633 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ .DS_Store .node_binaries node_modules -!/src/dev/npm/__tests__/fixtures/fixture1/node_modules +!/src/dev/npm/integration_tests/__fixtures__/fixture1/node_modules !/src/dev/notice/__fixtures__/node_modules trash /optimize @@ -31,7 +31,7 @@ webpackstats.json !/config/kibana.yml coverage selenium -.babelcache.json +.babel_register_cache.json .webpack.babelcache *.swp *.swo diff --git a/.i18nrc.json b/.i18nrc.json index f8d1ac25aafb2..0f905cc687d34 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -24,6 +24,7 @@ "kbnESQuery": "packages/kbn-es-query", "xpack.apm": "x-pack/plugins/apm", "xpack.beatsManagement": "x-pack/plugins/beats_management", + "xpack.canvas": "x-pack/plugins/canvas", "xpack.crossClusterReplication": "x-pack/plugins/cross_cluster_replication", "xpack.dashboardMode": "x-pack/plugins/dashboard_mode", "xpack.graph": "x-pack/plugins/graph", @@ -31,6 +32,7 @@ "xpack.idxMgmt": "x-pack/plugins/index_management", "xpack.indexLifecycleMgmt": "x-pack/plugins/index_lifecycle_management", "xpack.infra": "x-pack/plugins/infra", + "xpack.integrationsManager": "x-pack/plugins/integrations_manager", "xpack.kueryAutocomplete": "x-pack/plugins/kuery_autocomplete", "xpack.licenseMgmt": "x-pack/plugins/license_management", "xpack.maps": "x-pack/plugins/maps", @@ -51,10 +53,9 @@ "xpack.uptime": "x-pack/plugins/uptime", "xpack.watcher": "x-pack/plugins/watcher" }, - "exclude": [ - "src/legacy/ui/ui_render/ui_render_mixin.js" - ], + "exclude": ["src/legacy/ui/ui_render/ui_render_mixin.js"], "translations": [ - "x-pack/plugins/translations/translations/zh-CN.json" + "x-pack/plugins/translations/translations/zh-CN.json", + "x-pack/plugins/translations/translations/ja-JP.json" ] } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d26d73ea20561..b6c25aedf5f77 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -177,9 +177,7 @@ yarn kbn bootstrap There are a few options when it comes to running Elasticsearch: -First, you'll need to have a `java` binary in `PATH` and `JAVA_HOME` set. The version of Java required is specified in [.ci/java-version.properties](https://github.com/elastic/elasticsearch/blob/master/.ci/java-versions.properties) on the ES branch. - -**Nightly snapshot** +#### Nightly snapshot (recommended) These snapshots are built on a nightly basis which expire after a couple weeks. If running from an old, untracted branch this snapshot might not exist. In which case you might need to run from source or an archive. @@ -187,7 +185,7 @@ These snapshots are built on a nightly basis which expire after a couple weeks. yarn es snapshot ``` -**Source** +#### Source By default, it will reference an [elasticsearch](https://github.com/elastic/elasticsearch) checkout which is a sibling to the Kibana directory named `elasticsearch`. If you wish to use a checkout in another location you can provide that by supplying `--source-path` @@ -195,7 +193,7 @@ By default, it will reference an [elasticsearch](https://github.com/elastic/elas yarn es source ``` -**Archive** +#### Archive Use this if you already have a distributable. For released versions, one can be obtained on the [Elasticsearch downloads](https://www.elastic.co/downloads/elasticsearch) page. @@ -203,28 +201,33 @@ Use this if you already have a distributable. For released versions, one can be yarn es archive ``` +**Each of these will run Elasticsearch with a `basic` license. Additional options are available, pass `--help` for more information.** -Each of these will run Elasticsearch with a `basic` license. Additional options are available, pass `--help` for more information. - +##### Sample Data -If you're just getting started with `elasticsearch`, you could use the following command to populate your instance with a few fake logs to hit the ground running. +If you're just getting started with Elasticsearch, you could use the following command to populate your instance with a few fake logs to hit the ground running. ```bash -node scripts/makelogs +node scripts/makelogs --auth : ``` +> The default username and password combination are `elastic:changeme` > Make sure to execute `node scripts/makelogs` *after* elasticsearch is up and running! +### Running Kibana + Start the development server. ```bash yarn start ``` -> On Windows, you'll need you use Git Bash, Cygwin, or a similar shell that exposes the `sh` command. And to successfully build you'll need Cygwin optional packages zip, tar, and shasum. +> On Windows, you'll need to use Git Bash, Cygwin, or a similar shell that exposes the `sh` command. And to successfully build you'll need Cygwin optional packages zip, tar, and shasum. Now you can point your web browser to http://localhost:5601 and start using Kibana! When running `yarn start`, Kibana will also log that it is listening on port 5603 due to the base path proxy, but you should still access Kibana on port 5601. +By default, you can log in with username `elastic` and password `changeme`. See the `--help` options on `yarn es ` if you'd like to configure a different password. + #### Running Kibana in Open-Source mode If you're looking to only work with the open-source software, supply the license type to `yarn es`: @@ -279,6 +282,17 @@ IntelliJ | Settings » Languages & Frameworks » JavaScript » Code Quality To Another tool we use for enforcing consistent coding style is EditorConfig, which can be set up by installing a plugin in your editor that dynamically updates its configuration. Take a look at the [EditorConfig](http://editorconfig.org/#download) site to find a plugin for your editor, and browse our [`.editorconfig`](https://github.com/elastic/kibana/blob/master/.editorconfig) file to see what config rules we set up. +Note that for VSCode, to enable "live" linting of TypeScript (and other) file types, you will need to modify your local settings, as shown below. The default for the ESLint extension is to only lint JavaScript file types. + +```json + "eslint.validate": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact", + ] +``` + ### Internationalization All user-facing labels and info texts in Kibana should be internationalized. Please take a look at the [readme](packages/kbn-i18n/README.md) and the [guideline](packages/kbn-i18n/GUIDELINE.md) of the i18n package on how to do so. @@ -295,7 +309,7 @@ ReactDOM.render( ); ``` -There is a number of tools was created to support internationalization in Kibana that would allow one to validate internationalized labels, +There are a number of tools created to support internationalization in Kibana that would allow one to validate internationalized labels, extract them to a `JSON` file or integrate translations back to Kibana. To know more, please read corresponding [readme](src/dev/i18n/README.md) file. ### Testing and Building diff --git a/NOTICE.txt b/NOTICE.txt index 23def5d327301..d1903a471341f 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -4,32 +4,6 @@ Copyright 2012-2019 Elasticsearch B.V. --- This product has relied on ASTExplorer that is licensed under MIT. ---- -This product includes code that was extracted from angular-ui-bootstrap@0.13.1 -which is available under an "MIT" license - -The MIT License - -Copyright (c) 2012-2016 the AngularUI Team, http://angular-ui.github.io/bootstrap/ - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - --- This product uses Noto fonts that are licensed under the SIL Open Font License, Version 1.1. diff --git a/config/kibana.yml b/config/kibana.yml index 495d2030083d9..218c32b8f680a 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. @@ -111,4 +111,5 @@ #ops.interval: 5000 # Specifies locale to be used for all localizable strings, dates and number formats. +# Supported languages are the following: English - en , by default , Chinese - zh-CN . #i18n.locale: "en" diff --git a/docs/api/role-management.asciidoc b/docs/api/role-management.asciidoc index ded9db75e6150..2f4aa75d46d32 100644 --- a/docs/api/role-management.asciidoc +++ b/docs/api/role-management.asciidoc @@ -2,12 +2,12 @@ [[role-management-api]] == Kibana Role Management API -experimental[This API is *experimental* and may be changed or removed completely in a future release. The underlying mechanism of enforcing role based access control is stable, but the APIs for managing the roles are currently experimental.] +experimental["This API is *experimental* and may be changed or removed completely in a future release. The underlying mechanism of enforcing role based access control is stable, but the APIs for managing the roles are currently experimental."] The role management API allows people to manage roles that grant <>. It is *not* supported to do so using the -{ref}/security-api.html#security-role-apis[{es} role management APIs], and doing -so will likely cause {kib}'s authorization to behave unexpectedly. +{ref}/security-api.html#security-role-apis[{es} role management APIs], and doing +so will likely cause {kib}'s authorization to behave unexpectedly. NOTE: You cannot access these endpoints via the Console in Kibana. diff --git a/docs/api/role-management/delete.asciidoc b/docs/api/role-management/delete.asciidoc index 4eec3031c1b64..3a2eb6cfb731f 100644 --- a/docs/api/role-management/delete.asciidoc +++ b/docs/api/role-management/delete.asciidoc @@ -1,7 +1,7 @@ [[role-management-api-delete]] === Delete role -experimental[This API is experimental and may be changed or removed completely in a future release. Although the underlying mechanism of enforcing role-based access control is stable, the APIs for managing the roles are currently experimental.] +experimental["This API is experimental and may be changed or removed completely in a future release. Although the underlying mechanism of enforcing role-based access control is stable, the APIs for managing the roles are currently experimental."] NOTE: You cannot access this endpoint via the Console in Kibana. diff --git a/docs/api/role-management/get.asciidoc b/docs/api/role-management/get.asciidoc index 48c1b936b3edf..ce878588abcd4 100644 --- a/docs/api/role-management/get.asciidoc +++ b/docs/api/role-management/get.asciidoc @@ -1,7 +1,7 @@ [[role-management-api-get]] === Get Role -experimental[This API is experimental and may be changed or removed completely in a future release. Although the underlying mechanism of enforcing role-based access control is stable, the APIs for managing the roles are currently experimental.] +experimental["This API is experimental and may be changed or removed completely in a future release. Although the underlying mechanism of enforcing role-based access control is stable, the APIs for managing the roles are currently experimental."] Retrieves all {kib} roles, or a specific role. @@ -26,7 +26,7 @@ GET /api/security/role ===== Response -A successful call returns a response code of `200` and a response body containing a JSON +A successful call returns a response code of `200` and a response body containing a JSON representation of the roles. [source,js] @@ -45,9 +45,15 @@ representation of the roles. "cluster": [ ], "run_as": [ ] }, - "kibana": [ { - "privileges": [ "all" ] - } ], + "kibana": [{ + "base": [ + "all" + ], + "feature": {}, + "spaces": [ + "*" + ] + }] }, { "name": "my_admin_role", @@ -82,19 +88,19 @@ the `/api/security/role/` endpoint: [source,js] -------------------------------------------------- -GET /api/security/role/my_kibana_role +GET /api/security/role/my_restricted_kibana_role -------------------------------------------------- // KIBANA ===== Response -A successful call returns a response code of `200` and a response body containing a JSON +A successful call returns a response code of `200` and a response body containing a JSON representation of the role. [source,js] -------------------------------------------------- { - "name": "my_kibana_role", + "name": "my_restricted_kibana_role", "metadata" : { "version" : 1 }, @@ -106,8 +112,67 @@ representation of the role. "indices": [ ], "run_as": [ ] }, - "kibana": [ { - "privileges": [ "all" ] - } ], + "kibana": [ + { + "base": [ + "read" + ], + "feature": {}, + "spaces": [ + "marketing" + ] + }, + { + "base": [], + "feature": { + "discover": [ + "all" + ], + "visualize": [ + "all" + ], + "dashboard": [ + "all" + ], + "dev_tools": [ + "read" + ], + "advancedSettings": [ + "read" + ], + "indexPatterns": [ + "read" + ], + "timelion": [ + "all" + ], + "graph": [ + "all" + ], + "apm": [ + "read" + ], + "maps": [ + "read" + ], + "canvas": [ + "read" + ], + "infrastructure": [ + "all" + ], + "logs": [ + "all" + ], + "uptime": [ + "all" + ] + }, + "spaces": [ + "sales", + "default" + ] + } + ] } -------------------------------------------------- diff --git a/docs/api/role-management/put.asciidoc b/docs/api/role-management/put.asciidoc index c369ec644b559..e02445adfb19c 100644 --- a/docs/api/role-management/put.asciidoc +++ b/docs/api/role-management/put.asciidoc @@ -1,7 +1,7 @@ [[role-management-api-put]] === Create or Update Role -experimental[This API is experimental and may be changed or removed completely in a future release. Although the underlying mechanism of enforcing role-based access control is stable, the APIs for managing the roles are currently experimental.] +experimental["This API is experimental and may be changed or removed completely in a future release. Although the underlying mechanism of enforcing role-based access control is stable, the APIs for managing the roles are currently experimental."] Creates a new {kib} role or updates the attributes of an existing role. {kib} roles are stored in the {es} native realm. @@ -14,7 +14,7 @@ To use this API, you must have at least the `manage_security` cluster privilege. ==== Request -To create or update a role, issue a PUT request to the +To create or update a role, issue a PUT request to the `/api/security/role/` endpoint. [source,js] @@ -29,12 +29,24 @@ The following parameters can be specified in the body of a PUT request to add or `metadata`:: (object) Optional meta-data. Within the `metadata` object, keys that begin with `_` are reserved for system usage. -`elasticsearch`:: (object) Optional {es} cluster and index privileges, valid keys are +`elasticsearch`:: (object) Optional {es} cluster and index privileges, valid keys are `cluster`, `indices` and `run_as`. For more information, see {xpack-ref}/defining-roles.html[Defining Roles]. -`kibana`:: (object) An object that specifies the <>. Valid keys are `global` and `space`. Privileges defined in the `global` key will apply to all spaces within Kibana, and will take precedent over any privileges defined in the `space` key. For example, specifying `global: ["all"]` will grant full access to all spaces within Kibana, even if the role indicates that a specific space should only have `read` privileges. +`kibana`:: (list) A list of objects that specifies the <> for this role: +`base` ::: (list) An optional base privilege. If specified, must either be `["all"]` or `["read"]`. +The `feature` section cannot be used if a base privilege is specified here. You must use one or the other. +"all" grants read/write access to all Kibana features for the specified spaces. +"read" grants read-only access to all Kibana features for the specified spaces. -===== Example +`feature` ::: (object) Object containing privileges for specific features. +The `base` section cannot be used if feature privileges are specified here. You must use one or the other. +Use the <> to retrieve a list of available features. + +`spaces` ::: (list) The spaces these privileges should be applied to. +To grant access to all spaces, set this to `["*"]`, or omit the value. + +===== Example 1 +Granting access to various features in all spaces. [source,js] -------------------------------------------------- @@ -44,30 +56,159 @@ PUT /api/security/role/my_kibana_role "version" : 1 }, "elasticsearch": { - "cluster" : [ "all" ], - "indices" : [ { - "names" : [ "index1", "index2" ], - "privileges" : [ "all" ], - "field_security" : { - "grant" : [ "title", "body" ] - }, - "query" : "{\"match\": {\"title\": \"foo\"}}" - } ] + "cluster" : [ ], + "indices" : [ ] }, - "kibana": { - "global": ["all"] - } + "kibana": [ + { + "base": [], + "feature": { + "discover": [ + "all" + ], + "visualize": [ + "all" + ], + "dashboard": [ + "all" + ], + "dev_tools": [ + "read" + ], + "advancedSettings": [ + "read" + ], + "indexPatterns": [ + "read" + ], + "timelion": [ + "all" + ], + "graph": [ + "all" + ], + "apm": [ + "read" + ], + "maps": [ + "read" + ], + "canvas": [ + "read" + ], + "infrastructure": [ + "all" + ], + "logs": [ + "all" + ], + "uptime": [ + "all" + ] + }, + "spaces": [ + "*" + ] + } + ] } -------------------------------------------------- // KIBANA -==== Response +===== Example 2 +Granting "dashboard only" access to only the Marketing space. -A successful call returns a response code of `204` and no response body. +[source,js] +-------------------------------------------------- +PUT /api/security/role/my_kibana_role +{ + "metadata" : { + "version" : 1 + }, + "elasticsearch": { + "cluster" : [ ], + "indices" : [ ] + }, + "kibana": [ + { + "base": [], + "feature": { + "dashboard": ["read"] + }, + "spaces": [ + "marketing" + ] + } + ] +} +-------------------------------------------------- +===== Example 3 +Granting full access to all features in the Default space. -==== Granting access to specific spaces -To grant access to individual spaces within {kib}, specify the space identifier within the `kibana` object. +[source,js] +-------------------------------------------------- +PUT /api/security/role/my_kibana_role +{ + "metadata" : { + "version" : 1 + }, + "elasticsearch": { + "cluster" : [ ], + "indices" : [ ] + }, + "kibana": [ + { + "base": ["all"], + "feature": { + }, + "spaces": [ + "default" + ] + } + ] +} +-------------------------------------------------- + +===== Example 4 +Granting different access to different spaces. + +[source,js] +-------------------------------------------------- +PUT /api/security/role/my_kibana_role +{ + "metadata" : { + "version" : 1 + }, + "elasticsearch": { + "cluster" : [ ], + "indices" : [ ] + }, + "kibana": [ + { + "base": [], + "feature": { + "discover": ["all"], + "dashboard": ["all"] + }, + "spaces": [ + "default" + ] + }, + { + "base": ["read"], + "spaces": [ + "marketing", + "sales" + ] + } + ] +} +-------------------------------------------------- + + +===== Example 5 +Granting access to both Kibana and Elasticsearch. [source,js] -------------------------------------------------- @@ -87,12 +228,19 @@ PUT /api/security/role/my_kibana_role "query" : "{\"match\": {\"title\": \"foo\"}}" } ] }, - "kibana": { - "global": [], - "space": { - "marketing": ["all"], - "engineering": ["read"] + "kibana": [ + { + "base": ["all"], + "feature": { + }, + "spaces": [ + "default" + ] } - } + ] } -------------------------------------------------- + +==== Response + +A successful call returns a response code of `204` and no response body. diff --git a/docs/api/saved-objects/export.asciidoc b/docs/api/saved-objects/export.asciidoc index 9c757c31bc178..4cda4dd6278df 100644 --- a/docs/api/saved-objects/export.asciidoc +++ b/docs/api/saved-objects/export.asciidoc @@ -16,7 +16,7 @@ Note: You cannot access this endpoint via the Console in Kibana. (array|string) The saved object type(s) that the export should be limited to `objects` (optional):: (array) A list of objects to export -`includeReferencesDeep`:: +`includeReferencesDeep` (optional):: (boolean) This will make the exported objects include all the referenced objects needed Note: At least `type` or `objects` must be passed in. @@ -33,9 +33,25 @@ The following example exports all index pattern saved objects. -------------------------------------------------- POST api/saved_objects/_export { - "type": "index-patterns" + "type": "index-pattern" } -------------------------------------------------- // KIBANA A successful call returns a response code of `200` along with the exported objects as the response body. + +The following example exports specific saved objects. + +[source,js] +-------------------------------------------------- +POST api/saved_objects/_export +{ + "objects": [ + { + "type": "dashboard", + "id": "be3733a0-9efe-11e7-acb3-3dab96693fab" + } + ] +} +-------------------------------------------------- +// KIBANA \ No newline at end of file diff --git a/docs/api/saved-objects/import.asciidoc b/docs/api/saved-objects/import.asciidoc index ed90625e95508..f5bc9d184bea4 100644 --- a/docs/api/saved-objects/import.asciidoc +++ b/docs/api/saved-objects/import.asciidoc @@ -35,17 +35,16 @@ The following example imports an index pattern and dashboard. [source,js] -------------------------------------------------- -POST api/saved_objects/_import -Content-Type: multipart/form-data; boundary=EXAMPLE ---EXAMPLE -Content-Disposition: form-data; name="file"; filename="export.ndjson" -Content-Type: application/ndjson +$ curl -X POST "localhost:5601/api/saved_objects/_import" -H "kbn-xsrf: true" --form file=@file.ndjson +-------------------------------------------------- + +The `file.ndjson` file would contain the following. +[source,js] +-------------------------------------------------- {"type":"index-pattern","id":"my-pattern","attributes":{"title":"my-pattern-*"}} {"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"}} ---EXAMPLE-- -------------------------------------------------- -// KIBANA A successful call returns a response code of `200` and a response body containing a JSON structure similar to the following example: @@ -62,17 +61,16 @@ The following example imports an index pattern and dashboard but has a conflict [source,js] -------------------------------------------------- -POST api/saved_objects/_import -Content-Type: multipart/form-data; boundary=EXAMPLE ---EXAMPLE -Content-Disposition: form-data; name="file"; filename="export.ndjson" -Content-Type: application/ndjson +$ curl -X POST "localhost:5601/api/saved_objects/_import" -H "kbn-xsrf: true" --form file=@file.ndjson +-------------------------------------------------- + +The `file.ndjson` file would contain the following. +[source,js] +-------------------------------------------------- {"type":"index-pattern","id":"my-pattern","attributes":{"title":"my-pattern-*"}} {"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"}} ---EXAMPLE-- -------------------------------------------------- -// KIBANA The call returns a response code of `200` and a response body containing a JSON structure similar to the following example: @@ -99,17 +97,16 @@ The following example imports a visualization and dashboard but the index patter [source,js] -------------------------------------------------- -POST api/saved_objects/_import -Content-Type: multipart/form-data; boundary=EXAMPLE ---EXAMPLE -Content-Disposition: form-data; name="file"; filename="export.ndjson" -Content-Type: application/ndjson +$ curl -X POST "localhost:5601/api/saved_objects/_import" -H "kbn-xsrf: true" --form file=@file.ndjson +-------------------------------------------------- +The `file.ndjson` file would contain the following. + +[source,js] +-------------------------------------------------- {"type":"visualization","id":"my-vis","attributes":{"title":"my-vis"},"references":[{"name":"ref_0","type":"index-pattern","id":"my-pattern-*"}]} {"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"},"references":[{"name":"ref_0","type":"visualization","id":"my-vis"}]} ---EXAMPLE-- -------------------------------------------------- -// KIBANA The call returns a response code of `200` and a response body containing a JSON structure similar to the following example: diff --git a/docs/api/saved-objects/resolve_import_errors.asciidoc b/docs/api/saved-objects/resolve_import_errors.asciidoc index 6221229c4d559..ab376e97aa25d 100644 --- a/docs/api/saved-objects/resolve_import_errors.asciidoc +++ b/docs/api/saved-objects/resolve_import_errors.asciidoc @@ -33,20 +33,15 @@ The following example retries importing a dashboard. [source,js] -------------------------------------------------- -POST api/saved_objects/_resolve_import_errors -Content-Type: multipart/form-data; boundary=EXAMPLE ---EXAMPLE -Content-Disposition: form-data; name="file"; filename="export.ndjson" -Content-Type: application/ndjson +$ curl -X POST "localhost:5601/api/saved_objects/_resolve_import_errors" -H "kbn-xsrf: true" --form file=@file.ndjson --form retries='[{"type":"dashboard","id":"my-dashboard"}]' +-------------------------------------------------- -{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"}} ---EXAMPLE -Content-Disposition: form-data; name="retries" +The `file.ndjson` file would contain the following. -[{"type":"dashboard","id":"my-dashboard"}] ---EXAMPLE-- +[source,js] +-------------------------------------------------- +{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"}} -------------------------------------------------- -// KIBANA A successful call returns a response code of `200` and a response body containing a JSON structure similar to the following example: @@ -63,21 +58,16 @@ The following example resolves errors for a dashboard. This will cause the dashb [source,js] -------------------------------------------------- -POST api/saved_objects/_resolve_import_errors -Content-Type: multipart/form-data; boundary=EXAMPLE ---EXAMPLE -Content-Disposition: form-data; name="file"; filename="export.ndjson" -Content-Type: application/ndjson +$ curl -X POST "localhost:5601/api/saved_objects/_resolve_import_errors" -H "kbn-xsrf: true" --form file=@file.ndjson --form retries='[{"type":"dashboard","id":"my-dashboard","overwrite":true}]' +-------------------------------------------------- +The `file.ndjson` file would contain the following. + +[source,js] +-------------------------------------------------- {"type":"index-pattern","id":"my-pattern","attributes":{"title":"my-pattern-*"}} {"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"}} ---EXAMPLE -Content-Disposition: form-data; name="retries" - -[{"type":"dashboard","id":"my-dashboard","overwrite":true}] ---EXAMPLE-- -------------------------------------------------- -// KIBANA A successful call returns a response code of `200` and a response body containing a JSON structure similar to the following example: @@ -94,20 +84,15 @@ The following example resolves errors for a visualization by replacing the index [source,js] -------------------------------------------------- -POST api/saved_objects/_resolve_import_errors -Content-Type: multipart/form-data; boundary=EXAMPLE ---EXAMPLE -Content-Disposition: form-data; name="file"; filename="export.ndjson" -Content-Type: application/ndjson +$ curl -X POST "localhost:5601/api/saved_objects/_resolve_import_errors" -H "kbn-xsrf: true" --form file=@file.ndjson --form retries='[{"type":"visualization","id":"my-vis","replaceReferences":[{"type":"index-pattern","from":"missing","to":"existing"}]}]' +-------------------------------------------------- -{"type":"visualization","id":"my-vis","attributes":{"title":"Look at my visualization"},"references":[{"name":"ref_0","type":"index-pattern","id":"missing"}]} ---EXAMPLE -Content-Disposition: form-data; name="retries" +The `file.ndjson` file would contain the following. -[{"type":"visualization","id":"my-vis","replaceReferences":[{"type":"index-pattern","from":"missing","to":"existing"}]}] ---EXAMPLE-- +[source,js] +-------------------------------------------------- +{"type":"visualization","id":"my-vis","attributes":{"title":"Look at my visualization"},"references":[{"name":"ref_0","type":"index-pattern","id":"missing"}]} -------------------------------------------------- -// KIBANA A successful call returns a response code of `200` and a response body containing a JSON structure similar to the following example: diff --git a/docs/api/spaces-management.asciidoc b/docs/api/spaces-management.asciidoc index 869f7e722dd3d..9d34bbd56fe67 100644 --- a/docs/api/spaces-management.asciidoc +++ b/docs/api/spaces-management.asciidoc @@ -2,7 +2,7 @@ [[spaces-api]] == Kibana Spaces API -experimental[This API is *experimental* and may be changed or removed completely in a future release. The underlying Spaces concepts are stable, but the APIs for managing Spaces are currently experimental.] +experimental["This API is *experimental* and may be changed or removed completely in a future release. The underlying Spaces concepts are stable, but the APIs for managing Spaces are currently experimental."] The spaces API allows people to manage their spaces within {kib}. diff --git a/docs/api/spaces-management/delete.asciidoc b/docs/api/spaces-management/delete.asciidoc index fbc20bbf26d0c..c5cb284f66895 100644 --- a/docs/api/spaces-management/delete.asciidoc +++ b/docs/api/spaces-management/delete.asciidoc @@ -1,7 +1,7 @@ [[spaces-api-delete]] === Delete space -experimental[This API is *experimental* and may be changed or removed completely in a future release. The underlying Spaces concepts are stable, but the APIs for managing Spaces are currently experimental.] +experimental["This API is *experimental* and may be changed or removed completely in a future release. The underlying Spaces concepts are stable, but the APIs for managing Spaces are currently experimental."] [WARNING] ================================================== diff --git a/docs/api/spaces-management/get.asciidoc b/docs/api/spaces-management/get.asciidoc index b7b83426bf7c8..a31aad4c1d6f9 100644 --- a/docs/api/spaces-management/get.asciidoc +++ b/docs/api/spaces-management/get.asciidoc @@ -1,7 +1,7 @@ [[spaces-api-get]] === Get Space -experimental[This API is *experimental* and may be changed or removed completely in a future release. The underlying Spaces concepts are stable, but the APIs for managing Spaces are currently experimental.] +experimental["This API is *experimental* and may be changed or removed completely in a future release. The underlying Spaces concepts are stable, but the APIs for managing Spaces are currently experimental."] Retrieves all {kib} spaces, or a specific space. @@ -22,7 +22,7 @@ GET /api/spaces/space ===== Response -A successful call returns a response code of `200` and a response body containing a JSON +A successful call returns a response code of `200` and a response body containing a JSON representation of the spaces. [source,js] @@ -67,7 +67,7 @@ GET /api/spaces/space/marketing ===== Response -A successful call returns a response code of `200` and a response body containing a JSON +A successful call returns a response code of `200` and a response body containing a JSON representation of the space. [source,js] diff --git a/docs/api/spaces-management/post.asciidoc b/docs/api/spaces-management/post.asciidoc index 5b41e67ee84b0..ccd9ea7ea61c2 100644 --- a/docs/api/spaces-management/post.asciidoc +++ b/docs/api/spaces-management/post.asciidoc @@ -1,7 +1,7 @@ [[spaces-api-post]] === Create Space -experimental[This API is *experimental* and may be changed or removed completely in a future release. The underlying Spaces concepts are stable, but the APIs for managing Spaces are currently experimental.] +experimental["This API is *experimental* and may be changed or removed completely in a future release. The underlying Spaces concepts are stable, but the APIs for managing Spaces are currently experimental."] Creates a new {kib} space. To update an existing space, use the PUT command. @@ -9,7 +9,7 @@ Note: You cannot access this endpoint via the Console in Kibana. ==== Request -To create a space, issue a POST request to the +To create a space, issue a POST request to the `/api/spaces/space` endpoint. [source,js] diff --git a/docs/api/spaces-management/put.asciidoc b/docs/api/spaces-management/put.asciidoc index a1caae59efb8e..55a6022e729c0 100644 --- a/docs/api/spaces-management/put.asciidoc +++ b/docs/api/spaces-management/put.asciidoc @@ -1,7 +1,7 @@ [[spaces-api-put]] === Update Space -experimental[This API is *experimental* and may be changed or removed completely in a future release. The underlying Spaces concepts are stable, but the APIs for managing Spaces are currently experimental.] +experimental["This API is *experimental* and may be changed or removed completely in a future release. The underlying Spaces concepts are stable, but the APIs for managing Spaces are currently experimental."] Updates an existing {kib} space. To create a new space, use the POST command. @@ -9,7 +9,7 @@ Note: You cannot access this endpoint via the Console in Kibana. ==== Request -To update a space, issue a PUT request to the +To update a space, issue a PUT request to the `/api/spaces/space/` endpoint. [source,js] diff --git a/docs/code/code-basic-nav.asciidoc b/docs/code/code-basic-nav.asciidoc new file mode 100644 index 0000000000000..81db4fbe7fa89 --- /dev/null +++ b/docs/code/code-basic-nav.asciidoc @@ -0,0 +1,20 @@ +[[code-basic-nav]] +== Basic navigation + +[float] +==== View file structure and information +The *File* tree on the left is the major way for you to navigate through your folder structure. When you are on a directory, *Code* also presents a finder-like view on the right. Additionally, a file path breadcrumb makes it easy to go back to any parent directory. + +[float] +==== Git History and Blame +The *Directory* view shows the most recent commits. Clicking *View More* or *History* shows the complete commit history of the current folder or file. + +[role="screenshot"] +image::images/code-history.png[] + +Clicking *Blame* shows the most recent commit per line. + +[role="screenshot"] +image::images/code-blame.png[] + +include::code-semantic-nav.asciidoc[] diff --git a/docs/code/code-getting-started.asciidoc b/docs/code/code-getting-started.asciidoc new file mode 100644 index 0000000000000..3c33da17d98d9 --- /dev/null +++ b/docs/code/code-getting-started.asciidoc @@ -0,0 +1,37 @@ +[[code-getting-started]] +== Getting Started with Code + +The easiest way to get started with *Code* is to import a real-world repository into *Code*. + +[float] +==== Before you begin +You must have a {kib} instance up and running. + +If you are in an environment where you have multiple {kib} instances in a cluster, see <>. + +[float] +==== Import your first repository +. Navigate to the Code app. + +. In *Repository URL*, paste the following GitHub clone URL: ++ +[source,bash] +---- +https://github.com/Microsoft/TypeScript-Node-Starter +---- + +. Click *Import*. ++ +A list item shows the cloning, then indexing progress of the `TypeScript-Node-Starter` repo. ++ +[role="screenshot"] +image::images/code-import-repo.png[] + +. After the indexing is complete, navigate to the repo by clicking its name in the list. ++ +[role="screenshot"] +image::images/code-starter-root.png[] ++ +Congratulations! You just imported your first repo into *Code*. + +include::code-repo-management.asciidoc[] diff --git a/docs/code/code-install-lang-server.asciidoc b/docs/code/code-install-lang-server.asciidoc new file mode 100644 index 0000000000000..583ccc2e95025 --- /dev/null +++ b/docs/code/code-install-lang-server.asciidoc @@ -0,0 +1,21 @@ +[[code-install-lang-server]] +== Install language server + +*Code* comes to with built-in language support to TypeScript. You can install additional languages as a {kib} plugin. + +[role="screenshot"] +image::images/code-lang-server-tab.png[] + +The following languages are supported for the current version: + +* Built-in language support: `TypeScript` + +* Additional language support: `Java` + +You can check the status of the language servers and get installation instructions on the *Language Servers* tab. Make sure the status of the language server is `INSTALLED` or `RUNNING` after your restart the {kib} instance. +[role="screenshot"] +image::images/code-lang-server-status.png[] + + + +include::code-basic-nav.asciidoc[] diff --git a/docs/code/code-multiple-kibana-instances-config.asciidoc b/docs/code/code-multiple-kibana-instances-config.asciidoc new file mode 100644 index 0000000000000..e061559245839 --- /dev/null +++ b/docs/code/code-multiple-kibana-instances-config.asciidoc @@ -0,0 +1,10 @@ +[[code-multiple-kibana-instances-config]] +== Config for multiple {kib} instances +If you are using multiple instances of {kib}, you must assign one {kib} instance as a *Code* `node`. Add the following line of code to your `kibana.yml` file for every {kib} instance and restart the instances: + +[source,yaml] +---- +xpack.code.codeNodeUrl: 'http://$YourCodeNodeAddress' +---- + +`$YourCodeNoteAddress` is the URL of your assigned *Code* node accessible by other {kib} instances. diff --git a/docs/code/code-repo-management.asciidoc b/docs/code/code-repo-management.asciidoc new file mode 100644 index 0000000000000..3751ed361313e --- /dev/null +++ b/docs/code/code-repo-management.asciidoc @@ -0,0 +1,51 @@ +[[code-repo-management]] +== Repo management + +Code starts with an overview of your repositories. You can then use the UI to add, delete, and reindex a repo. +[role="screenshot"] +image::images/code-repo-management.png[] + +[float] +==== Add and delete a repo +The <> provides step-by-step instructions for adding a GitHub repo to *Code*. You can fine tune the hostname of the git clone URL in your `kibana.yml` file. + +For security reasons, Code allows only a few trusted hostnames, such as github.com, by default. You can add SSH key to {kib} to clone private repo. + +Deleting a repo removes it from local storage and the Elasticsearch index. + +[float] +==== Reindex a repo +*Code* automatically reindexes an imported repo, but in some cases you might need to manually refresh the index. For example, you might refresh an index after a new language server is installed. Or, you might want to immediately update the index to the HEAD revision. Click *Reindex* to initiate a reindex. + +[float] +==== Clone URL management +For security reasons, *Code* only allows the following hostnames in the git clone URL by default: + +[source,yaml] +---- +['github.com', 'gitlab.com', 'bitbucket.org', 'gitbox.apache.org', 'eclipse.org'] +---- + +You can add your own hostname (for example, acme.com) to the whitelist by adding the following line to your `config/kibana.yaml` file: + +[source,yaml] +---- +xpack.code.gitHostWhitelist: [ "github.com", "gitlab.com", "bitbucket.org", "gitbox.apache.org", "eclipse.org", "acme.com" ] +---- + +Set `xpack.code.gitHostWhitelist` to [] (empty list) allow any hostname. + +You can also control the protocol to use for the clone address. By default, the following protocols are supported: `[ 'https', 'git', 'ssh' ]`. You can change this value by adding the following line to your `config/kibana.yaml` file. In this example, the user only wants to support the `https` protocol: + +[source,yaml] +---- +xpack.code.gitProtocolWhitelist: [ "https" ] +---- + +[float] +==== Clone repo with SSH key +If your repo clone requires an SSH key for authentication, put the SSH key in `data/code/credentials/` under the {kib} folder. + + + +include::code-install-lang-server.asciidoc[] diff --git a/docs/code/code-search.asciidoc b/docs/code/code-search.asciidoc new file mode 100644 index 0000000000000..3e5aa1e011e5c --- /dev/null +++ b/docs/code/code-search.asciidoc @@ -0,0 +1,24 @@ +[[code-search]] +== Search + +[float] +==== Typeahead search +The search bar is built to minimize the time for you to locate the result. It shows `Symbols`, `Files`, and `Repos` results as you type, and clicking on any result takes you to the definition. You can use the search type dropdown to show all types of results or limit it to a specific search type. + +[role="screenshot"] +image::images/code-quick-search.png[] + +[float] +==== Full-text search +If the quick search results don’t contain what you are looking for, you can press ‘Enter’ to bring out the full text search. +[role="screenshot"] +image::images/code-full-text-search.png[] +You can further slice and dice the results using the repo and language facet filters on the left. + +[float] +==== Search filter +You can also use the Search Filters to limit the search scope to certain repos before issuing a query. To search across all repos, remove all repo filters. By default, the search repo is limited to the current repo. +[role="screenshot"] +image::images/code-search-filter.png[] + +include::code-multiple-kibana-instances-config.asciidoc[] diff --git a/docs/code/code-semantic-nav.asciidoc b/docs/code/code-semantic-nav.asciidoc new file mode 100644 index 0000000000000..c0e45075944fa --- /dev/null +++ b/docs/code/code-semantic-nav.asciidoc @@ -0,0 +1,24 @@ +[[code-semantic-nav]] + +== Semantic code navigation +If the file is one of *Code’s* <> and the corresponding language server is <>, you can navigate the files with semantic code navigation features. + +[float] +==== Goto definition and find reference +Hovering your cursor over a symbol in a file opens information about the symbol, including its qualified name and documentation, when available. You can perform two actions: + +* *Goto Definition* navigates to the symbol definition. If the definition is defined in another repo, *Code* can find the definition if the definition repo is also imported. + +* *Find Reference* opens a panel that lists all the places where the symbol is referenced in the current repo. + +[role="screenshot"] +image::images/code-semantic-nav.png[] + +[float] +==== View symbol table +From the *Structure* tab, you can open a symbol table that details the structure of the current class. Clicking on a member function or variable jumps to its definition. + +[role="screenshot"] +image::images/code-symbol-table.png[] + +include::code-search.asciidoc[] diff --git a/docs/code/index.asciidoc b/docs/code/index.asciidoc new file mode 100644 index 0000000000000..fee5a36b35ba5 --- /dev/null +++ b/docs/code/index.asciidoc @@ -0,0 +1,19 @@ +[[code-intro]] += Code + +[partintro] +-- + +beta[] Interaction with source code is pervasive and essential for any technology company. How efficiently you search, navigate, and gain insight to your source code impacts how fast your organization can innovate. *Code* provides an easy-to-use code search solution that scales with your organization, so your team can focus on shipping awesome products and providing the best services. *Code* provides the following functions: + +Code provides: + +* Jump to definition and find references for a symbol +* Typeahead search for symbol definition, file, and repo +* Symbol table +* Full text search with repo and language filters + +<> with *Code* by importing your first repo. +-- + +include::code-getting-started.asciidoc[] diff --git a/docs/dashboard.asciidoc b/docs/dashboard.asciidoc index 5e855fdb85d0f..48189465bc3e7 100644 --- a/docs/dashboard.asciidoc +++ b/docs/dashboard.asciidoc @@ -20,6 +20,21 @@ you'll be prompted to do so as you follow the steps for creating a dashboard. Or, you can use one of the prebuilt sample data sets, available from the Kibana home page. +[float] +[[dashboard-read-only-access]] +=== [xpack]#Read only access# +When you have insufficient privileges to create or save dashboards, the following +indicator in Kibana will be displayed. The buttons to create new dashboards or edit +existing dashboard won't be visible. For more information on granting access to +Kibana see <>. + +[role="screenshot"] +image::images/dashboard-read-only-badge.png[Example of Dashboard's read only access indicator in Kibana's header] + +[float] +[[dashboard-create-new-dashboard]] +=== Creating a new Dashboard + . In the side navigation, click *Dashboard*. . Click *Create new dashboard.* . Click *Add*. diff --git a/docs/dev-tools/searchprofiler/getting-started.asciidoc b/docs/dev-tools/searchprofiler/getting-started.asciidoc index 07cb9cd143743..fdac188c2855a 100644 --- a/docs/dev-tools/searchprofiler/getting-started.asciidoc +++ b/docs/dev-tools/searchprofiler/getting-started.asciidoc @@ -1,52 +1,22 @@ [role="xpack"] [[profiler-getting-started]] - -ifndef::gs-mini[] === Getting Started -endif::gs-mini[] - -ifdef::gs-mini[] -== Getting Started -endif::gs-mini[] -The {searchprofiler} is automatically enabled in {kib}. It is located under the -*Dev Tools* tab in {kib}. +The {searchprofiler} is automatically enabled in {kib}. Go to *Dev Tools > Search Profiler* +to get started. -[[first-profile]] -To start profiling queries: +{searchprofiler} displays the names of the indices searched, the shards in each index, +and how long it took for the query to complete. To try it out, replace the default `match_all` query +with the query you want to profile and click *Profile*. -. Open Kibana in your web browser and log in. If you are running Kibana -locally, go to `http://localhost:5601/`. +The following example shows the results of profiling the `match_all` query. +If we take a closer look at the information for the `.kibana_1` sample index, the +Cumulative Time field shows us that the query took 0.067ms to execute. -. Click **DevTools** in the side navigation to open the {searchprofiler}. -Console is the default tool to open when first accessing DevTools. -+ -image::dev-tools/searchprofiler/images/gs1.png["Opening DevTools"] -+ -On the top navigation bar, click the second item: *Search Profiler* -+ -image::dev-tools/searchprofiler/images/gs2.png["Opening the {searchprofiler}"] +[role="screenshot"] +image::dev-tools/searchprofiler/images/query.png["{searchprofiler} example"] -. This opens the {searchprofiler} interface. -+ -image::dev-tools/searchprofiler/images/gs3.png["{searchprofiler} Interface"] -. Replace the default `match_all` query with the query you want to profile and click *Profile*. -+ -image::dev-tools/searchprofiler/images/gs4.png["Profiling the match_all query"] -+ -{searchprofiler} displays the names of the indices searched, the shards in each index, -and how long the query took. The following example shows the results of profiling -the match_all query. Three indices were searched: `.monitoring-kibana-2-2016.11.30`, -`.monitoring-data-2` and `test`. -+ -If we take a closer look at the information for the test index, we can see from the -Cumulative Time that the query took 0.132ms to execute. Of the five shards in the -index (`DWZD0iosQNeJMTvb4q1JDw` 0 through 5), shard 3 was the slowest (0.053ms), followed by shard 2 (0.038ms). Shards are -sorted by their time in descending order. -+ -image::dev-tools/searchprofiler/images/gs5.png["Profile details for the test index"] -+ [NOTE] ==== The Cumulative Time metric is the sum of individual shard times. @@ -59,14 +29,21 @@ While the Cumulative Time metric is useful for comparing the performance of your indices and shards, it doesn't necessarily represent the actual physical query times. ==== -. To view more detailed profiling information for a shard, click the Expand button. -This displays details about the query component(s) that ran on the shard. -+ -In this example, there is a single `"MatchAllDocsQuery"` that ran on the shard. -Since it was the only query run, it took 100% of the time. When you mouse over -a row, the {searchprofiler} displays additional information about the query component." -+ -image::dev-tools/searchprofiler/images/gs6.png["Profile details for the first shard"] -+ -This panel shows the timing breakdown of low-level Lucene methods. For more information, -see the reference docs for timing breakdowns in {ref}/search-profile-queries.html[Profiling queries]. +You can select the name of the shard and then click *View details* to see more profiling information, +including details about the query component(s) that ran on the shard, as well as the timing +breakdown of low-level Lucene methods. For more information, see {ref}/search-profile-queries.html[Profiling queries]. + +[float] +=== Index and type filtering + +By default, all queries executed by the {searchprofiler} are sent +to `GET /_search`. It searches across your entire cluster (all indices, all types). + +If you need to query a specific index or type (or several), you can use the Index +and Type filters at the top left. + +In the following example, the query is executed against the indices `test` and `kibana_1` +and the type `my_type`. This is equivalent making a request to `GET /test,kibana_1/my_type/_search`. + +[role="screenshot"] +image::dev-tools/searchprofiler/images/filter.png["Filtering by index and type"] \ No newline at end of file diff --git a/docs/dev-tools/searchprofiler/gs-index.asciidoc b/docs/dev-tools/searchprofiler/gs-index.asciidoc index a48473e176c5e..b4f5d48290f5e 100644 --- a/docs/dev-tools/searchprofiler/gs-index.asciidoc +++ b/docs/dev-tools/searchprofiler/gs-index.asciidoc @@ -1,19 +1,19 @@ [role="xpack"] [[xpack-profiler]] -= Profiling your Queries and Aggregations += Profiling queries and aggregations [partintro] -- -Elasticsearch has a powerful profiler API which can be used to inspect and analyze -your search queries. The response, however, is a very large JSON blob which is -difficult to analyze by hand. +{es} has a powerful {ref}/search-profile.html[Profile API] which can be used to inspect and analyze +your search queries. The response returns a large JSON blob, which can be +difficult to analyze manually. The {searchprofiler} tool can transform this JSON output into a visualization that is easy to navigate, allowing you to diagnose and debug poorly performing queries much faster. - -image::dev-tools/searchprofile/images/overview.png["{searchprofiler} Visualization"] +[role="screenshot"] +image::dev-tools/searchprofiler/images/overview.png["{searchprofiler} Visualization"] -- diff --git a/docs/dev-tools/searchprofiler/images/filter.png b/docs/dev-tools/searchprofiler/images/filter.png index a1da7ece1beee..1b36d5ebe1d0d 100644 Binary files a/docs/dev-tools/searchprofiler/images/filter.png and b/docs/dev-tools/searchprofiler/images/filter.png differ diff --git a/docs/dev-tools/searchprofiler/images/gs1.png b/docs/dev-tools/searchprofiler/images/gs1.png deleted file mode 100644 index 1b8021365cfbe..0000000000000 Binary files a/docs/dev-tools/searchprofiler/images/gs1.png and /dev/null differ diff --git a/docs/dev-tools/searchprofiler/images/gs10.png b/docs/dev-tools/searchprofiler/images/gs10.png index 4df1fa67ebe4c..7d8ae8402d6b9 100644 Binary files a/docs/dev-tools/searchprofiler/images/gs10.png and b/docs/dev-tools/searchprofiler/images/gs10.png differ diff --git a/docs/dev-tools/searchprofiler/images/gs2.png b/docs/dev-tools/searchprofiler/images/gs2.png deleted file mode 100644 index c9cb3c676f7dc..0000000000000 Binary files a/docs/dev-tools/searchprofiler/images/gs2.png and /dev/null differ diff --git a/docs/dev-tools/searchprofiler/images/gs3.png b/docs/dev-tools/searchprofiler/images/gs3.png deleted file mode 100644 index 71ac2a40f6526..0000000000000 Binary files a/docs/dev-tools/searchprofiler/images/gs3.png and /dev/null differ diff --git a/docs/dev-tools/searchprofiler/images/gs4.png b/docs/dev-tools/searchprofiler/images/gs4.png deleted file mode 100644 index f34c387e210ac..0000000000000 Binary files a/docs/dev-tools/searchprofiler/images/gs4.png and /dev/null differ diff --git a/docs/dev-tools/searchprofiler/images/gs5.png b/docs/dev-tools/searchprofiler/images/gs5.png deleted file mode 100644 index b6588ba1ec56a..0000000000000 Binary files a/docs/dev-tools/searchprofiler/images/gs5.png and /dev/null differ diff --git a/docs/dev-tools/searchprofiler/images/gs6.png b/docs/dev-tools/searchprofiler/images/gs6.png deleted file mode 100644 index 1def1bf347132..0000000000000 Binary files a/docs/dev-tools/searchprofiler/images/gs6.png and /dev/null differ diff --git a/docs/dev-tools/searchprofiler/images/gs7.png b/docs/dev-tools/searchprofiler/images/gs7.png deleted file mode 100644 index 5151cfc0f0c9e..0000000000000 Binary files a/docs/dev-tools/searchprofiler/images/gs7.png and /dev/null differ diff --git a/docs/dev-tools/searchprofiler/images/gs8.png b/docs/dev-tools/searchprofiler/images/gs8.png index 16abadec9b606..efe00fdcc6f09 100644 Binary files a/docs/dev-tools/searchprofiler/images/gs8.png and b/docs/dev-tools/searchprofiler/images/gs8.png differ diff --git a/docs/dev-tools/searchprofiler/images/gs9.png b/docs/dev-tools/searchprofiler/images/gs9.png deleted file mode 100644 index 14d2c185b7568..0000000000000 Binary files a/docs/dev-tools/searchprofiler/images/gs9.png and /dev/null differ diff --git a/docs/dev-tools/searchprofiler/images/overview.png b/docs/dev-tools/searchprofiler/images/overview.png index f9974a82d5db4..cce3ab43f5fa9 100644 Binary files a/docs/dev-tools/searchprofiler/images/overview.png and b/docs/dev-tools/searchprofiler/images/overview.png differ diff --git a/docs/dev-tools/searchprofiler/images/pasting.png b/docs/dev-tools/searchprofiler/images/pasting.png index a826f05523d60..298cef203a7de 100644 Binary files a/docs/dev-tools/searchprofiler/images/pasting.png and b/docs/dev-tools/searchprofiler/images/pasting.png differ diff --git a/docs/dev-tools/searchprofiler/images/query.png b/docs/dev-tools/searchprofiler/images/query.png new file mode 100644 index 0000000000000..31b0f54eebfa7 Binary files /dev/null and b/docs/dev-tools/searchprofiler/images/query.png differ diff --git a/docs/dev-tools/searchprofiler/index.asciidoc b/docs/dev-tools/searchprofiler/index.asciidoc index 295b9467a30ec..aca96dbfe3ee3 100644 --- a/docs/dev-tools/searchprofiler/index.asciidoc +++ b/docs/dev-tools/searchprofiler/index.asciidoc @@ -1,10 +1,10 @@ [role="xpack"] [[xpack-profiler]] -== Profiling your Queries and Aggregations +== Profiling queries and aggregations -Elasticsearch has a powerful profiler API which can be used to inspect and analyze -your search queries. The response, however, is a very large JSON blob which is -difficult to analyze by hand. +{es} has a powerful {ref}/search-profile.html[Profile API] which can be used to inspect and analyze +your search queries. The response returns a large JSON blob, which can be +difficult to analyze manually. The {searchprofiler} tool can transform this JSON output into a visualization that is easy to navigate, allowing you to diagnose and debug @@ -18,5 +18,3 @@ include::getting-started.asciidoc[] include::more-complicated.asciidoc[] include::pasting.asciidoc[] - -include::misc.asciidoc[] diff --git a/docs/dev-tools/searchprofiler/misc.asciidoc b/docs/dev-tools/searchprofiler/misc.asciidoc deleted file mode 100644 index 4a60e4397b5d6..0000000000000 --- a/docs/dev-tools/searchprofiler/misc.asciidoc +++ /dev/null @@ -1,16 +0,0 @@ -[role="xpack"] -[[profiler-index]] -=== Index and Type filtering - -By default, all queries executed by the {searchprofiler} are sent -to `GET /_search`. It searches across your entire cluster (all indices, all types). - -If you need to query a specific index or type (or several), you can use the Index -and Type filters at the top-left of the UI - -In the following example, the query is executed against the indices `my_index` and `my_index1` - and the type `my_type`: - -image::dev-tools/searchprofiler/images/filter.png["Filtering by index and type"] - -This is equivalent to `GET /my_index,my_index2/my_type/_search`. diff --git a/docs/dev-tools/searchprofiler/more-complicated.asciidoc b/docs/dev-tools/searchprofiler/more-complicated.asciidoc index 5e06d179d938b..ea786938b24eb 100644 --- a/docs/dev-tools/searchprofiler/more-complicated.asciidoc +++ b/docs/dev-tools/searchprofiler/more-complicated.asciidoc @@ -5,12 +5,12 @@ To understand how the query trees are displayed inside the {searchprofiler}, let's look at a more complicated query. -. Index the following data: +. Index the following data via *Console*: + -- [source,js] -------------------------------------------------- -POST test/test/_bulk +POST test/_bulk {"index":{}} {"name":"aaron","age":23,"hair":"brown"} {"index":{}} @@ -25,16 +25,11 @@ POST test/test/_bulk // CONSOLE -- -. Enter "test" in the Index filter above the query editor (the input box with a - grayed-out `_all`). This restricts profiled queries to the `test` index. -+ --- -image::dev-tools/searchprofiler/images/gs7.png["Using the index filter"] --- +. From the {searchprofiler}, enter "test" in the Index field above the query editor to restrict profiled +queries to the `test` index. -. Replace the default `match_all` query with a query that has two sub-query -components and includes a simple aggregation. For example, copy and paste -the following query into the query editor. +. Replace the default `match_all` query in the query editor with a query that has two sub-query +components and includes a simple aggregation, like the example below. + -- [source,js] @@ -71,52 +66,38 @@ the following query into the query editor. // NOTCONSOLE -- -. Click *Profile* to profile the query and visualize the results. +. Click *Profile* to profile the query and visualize the results. +. Select the shard to view the query details. + --- +[role="screenshot"] image::dev-tools/searchprofiler/images/gs8.png["Profiling the more complicated query"] -As before, you'll see a list of shards appear in the center panel. You'll notice -that the query was slightly slower (15ms vs 0.13ms) because it actually had to do -a bit of work this time, unlike the `match_all` query. --- - -. Click the first shard's Expand button to view the query details. -+ --- -image::dev-tools/searchprofiler/images/gs9.png["Drilling into the first shard's details"] - --- -You'll notice several interesting things in the results. The shard details -contain a row for each query component: +The detail view contains a row for each query component: - The top-level `BooleanQuery` component corresponds to the bool in the query. - The second `BooleanQuery` corresponds to the terms query, which is internally converted to a `Boolean` of should clauses. It has two child queries that correspond to "sue" and "sally" from the terms query. - - The `TermQuery` that's labeled with _"name:fred"_ corresponds to match: fred in the query. + - The `TermQuery` that's labeled with "name:fred" corresponds to match: fred in the query. -If you look at the timings, you can see that "Self Time" and "Total Time" are no longer +If you look at the time columns, you can see that "Self time" and "Total time" are no longer identical on all the rows. Self time represents how long the query component took to execute. -Total time is the time a query component _and all its children_ took to execute. -Therefore, queries like the Boolean queries often have larger Total than Self. - -In particular, you can see that the `BooleanQuery` for "name:sue name:sally" took 3.8ms total, -but 2.8ms of that was "self" time. That means 2.8ms was spent by the `BooleanQuery` itself, processing -the should clauses +Total time is the time a query component and all its children took to execute. +Therefore, queries like the Boolean queries often have a larger total time than self time. ==== Aggregations This particular query also includes a aggregation (a `stats` agg on the `"age"` field). -To view Aggregation profiling statistics, click the *Aggregation Profile* tab. This tab -is only enabled if the query being profiled contains an aggregation. +Click *Aggregation Profile* to view aggregation profiling statistics (this tab +is only enabled if the query being profiled contains an aggregation). -image::dev-tools/searchprofiler/images/gs10.png["Drilling into the first shard's details"] -Click a shard's Expand button to view the aggregation details. Hover over an -aggregation row to view the timing breakdown. +Select the name of the shard to view the aggregation details and timing breakdown. + +[role="screenshot"] +image::dev-tools/searchprofiler/images/gs10.png["Drilling into the first shard's details"] For more information about how the {searchprofiler} works, how timings are calculated, and how to interpret various results, see diff --git a/docs/dev-tools/searchprofiler/pasting.asciidoc b/docs/dev-tools/searchprofiler/pasting.asciidoc index ac4ae4631cc6f..9257a4d84fb56 100644 --- a/docs/dev-tools/searchprofiler/pasting.asciidoc +++ b/docs/dev-tools/searchprofiler/pasting.asciidoc @@ -5,17 +5,18 @@ The {searchprofiler} queries the cluster that the Kibana node is attached to. It does this by executing the query against the cluster and collecting the results. -This is convenient, but sometimes performance problems are temporal in nature. For example, -a query might only be slow at certain time of day when many customers are using your system. +But sometimes you may want to investigate performance problems that are temporal in nature. +For example, a query might only be slow at certain time of day when many customers are using your system. You can setup a process to automatically profile slow queries when they occur and then save those profile responses for later analysis. -The {searchprofiler} supports this workflow by enabling you to paste the -pre-captured JSON. The tool will detect that this is a profiler response JSON -rather than a query, and render the visualization rather than querying the cluster. +The {searchprofiler} supports this workflow by allowing you to paste the +pre-captured JSON in the query editor. The {searchprofiler} will detect that you +have entered a JSON response (rather than a query) and will just render the visualization, +rather than querying the cluster. To see how this works, copy and paste the following profile response into the -query editor and click *Profile*: +query editor and click *Profile*. [source,js] -------------------------------------------------- diff --git a/docs/development/core/public/kibana-plugin-public.applicationsetup.md b/docs/development/core/public/kibana-plugin-public.applicationsetup.md index 55d882f743a8a..a3ab77e43446c 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationsetup.md +++ b/docs/development/core/public/kibana-plugin-public.applicationsetup.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) ## ApplicationSetup interface diff --git a/docs/development/core/public/kibana-plugin-public.applicationsetup.registerapp.md b/docs/development/core/public/kibana-plugin-public.applicationsetup.registerapp.md index d76b3cb0c96dd..f2532ae71ca2f 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationsetup.registerapp.md +++ b/docs/development/core/public/kibana-plugin-public.applicationsetup.registerapp.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) > [registerApp](./kibana-plugin-public.applicationsetup.registerapp.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) > [registerApp](./kibana-plugin-public.applicationsetup.registerapp.md) ## ApplicationSetup.registerApp() method diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.availableapps.md b/docs/development/core/public/kibana-plugin-public.applicationstart.availableapps.md index 7ac2a9ccfc875..bca2a7046d7c9 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationstart.availableapps.md +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.availableapps.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationStart](./kibana-plugin-public.applicationstart.md) > [availableApps](./kibana-plugin-public.applicationstart.availableapps.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationStart](./kibana-plugin-public.applicationstart.md) > [availableApps](./kibana-plugin-public.applicationstart.availableapps.md) ## ApplicationStart.availableApps property diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.capabilities.md b/docs/development/core/public/kibana-plugin-public.applicationstart.capabilities.md index b4e29e84a1800..9ef82592f8754 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationstart.capabilities.md +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.capabilities.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationStart](./kibana-plugin-public.applicationstart.md) > [capabilities](./kibana-plugin-public.applicationstart.capabilities.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationStart](./kibana-plugin-public.applicationstart.md) > [capabilities](./kibana-plugin-public.applicationstart.capabilities.md) ## ApplicationStart.capabilities property diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.md b/docs/development/core/public/kibana-plugin-public.applicationstart.md index 6ee3818ad50e5..820d75cbd0e18 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationstart.md +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationStart](./kibana-plugin-public.applicationstart.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationStart](./kibana-plugin-public.applicationstart.md) ## ApplicationStart interface diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.mount.md b/docs/development/core/public/kibana-plugin-public.applicationstart.mount.md index 8d43b107f858e..c6fd7872348bc 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationstart.mount.md +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.mount.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationStart](./kibana-plugin-public.applicationstart.md) > [mount](./kibana-plugin-public.applicationstart.mount.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationStart](./kibana-plugin-public.applicationstart.md) > [mount](./kibana-plugin-public.applicationstart.mount.md) ## ApplicationStart.mount property diff --git a/docs/development/core/public/kibana-plugin-public.basepathsetup.addtopath.md b/docs/development/core/public/kibana-plugin-public.basepathsetup.addtopath.md deleted file mode 100644 index a05ea4daec04d..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.basepathsetup.addtopath.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [BasePathSetup](./kibana-plugin-public.basepathsetup.md) > [addToPath](./kibana-plugin-public.basepathsetup.addtopath.md) - -## BasePathSetup.addToPath() method - -Add the current basePath to a path string. - -Signature: - -```typescript -addToPath(path: string): string; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| path | string | A relative url including the leading /, otherwise it will be returned without modification | - -Returns: - -`string` - diff --git a/docs/development/core/public/kibana-plugin-public.basepathsetup.get.md b/docs/development/core/public/kibana-plugin-public.basepathsetup.get.md deleted file mode 100644 index 570e2f935cadb..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.basepathsetup.get.md +++ /dev/null @@ -1,19 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [BasePathSetup](./kibana-plugin-public.basepathsetup.md) > [get](./kibana-plugin-public.basepathsetup.get.md) - -## BasePathSetup.get() method - -Get the basePath as defined by the server - -Signature: - -```typescript -get(): string; -``` -Returns: - -`string` - -The basePath as defined by the server - diff --git a/docs/development/core/public/kibana-plugin-public.basepathsetup.md b/docs/development/core/public/kibana-plugin-public.basepathsetup.md deleted file mode 100644 index 2ce42dd1e24aa..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.basepathsetup.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [BasePathSetup](./kibana-plugin-public.basepathsetup.md) - -## BasePathSetup interface - -Provides access to the 'server.basePath' configuration option in kibana.yml - -Signature: - -```typescript -export interface BasePathSetup -``` - -## Methods - -| Method | Description | -| --- | --- | -| [addToPath(path)](./kibana-plugin-public.basepathsetup.addtopath.md) | Add the current basePath to a path string. | -| [get()](./kibana-plugin-public.basepathsetup.get.md) | Get the basePath as defined by the server | -| [removeFromPath(path)](./kibana-plugin-public.basepathsetup.removefrompath.md) | Removes basePath from the given path if the path starts with it | - diff --git a/docs/development/core/public/kibana-plugin-public.basepathsetup.removefrompath.md b/docs/development/core/public/kibana-plugin-public.basepathsetup.removefrompath.md deleted file mode 100644 index c55bb09ed4b7a..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.basepathsetup.removefrompath.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [BasePathSetup](./kibana-plugin-public.basepathsetup.md) > [removeFromPath](./kibana-plugin-public.basepathsetup.removefrompath.md) - -## BasePathSetup.removeFromPath() method - -Removes basePath from the given path if the path starts with it - -Signature: - -```typescript -removeFromPath(path: string): string; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| path | string | A relative url that starts with the basePath, which will be stripped | - -Returns: - -`string` - diff --git a/docs/development/core/public/kibana-plugin-public.basepathstart.md b/docs/development/core/public/kibana-plugin-public.basepathstart.md deleted file mode 100644 index 0f6a441975c21..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.basepathstart.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [BasePathStart](./kibana-plugin-public.basepathstart.md) - -## BasePathStart type - -Provides access to the 'server.basePath' configuration option in kibana.yml - -Signature: - -```typescript -export declare type BasePathStart = BasePathSetup; -``` diff --git a/docs/development/core/public/kibana-plugin-public.capabilities.catalogue.md b/docs/development/core/public/kibana-plugin-public.capabilities.catalogue.md index 1529a5b24a9bf..ea3380d70053b 100644 --- a/docs/development/core/public/kibana-plugin-public.capabilities.catalogue.md +++ b/docs/development/core/public/kibana-plugin-public.capabilities.catalogue.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [Capabilities](./kibana-plugin-public.capabilities.md) > [catalogue](./kibana-plugin-public.capabilities.catalogue.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [Capabilities](./kibana-plugin-public.capabilities.md) > [catalogue](./kibana-plugin-public.capabilities.catalogue.md) ## Capabilities.catalogue property diff --git a/docs/development/core/public/kibana-plugin-public.capabilities.management.md b/docs/development/core/public/kibana-plugin-public.capabilities.management.md index 04c12dff9ebea..5f4c159aef974 100644 --- a/docs/development/core/public/kibana-plugin-public.capabilities.management.md +++ b/docs/development/core/public/kibana-plugin-public.capabilities.management.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [Capabilities](./kibana-plugin-public.capabilities.md) > [management](./kibana-plugin-public.capabilities.management.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [Capabilities](./kibana-plugin-public.capabilities.md) > [management](./kibana-plugin-public.capabilities.management.md) ## Capabilities.management property diff --git a/docs/development/core/public/kibana-plugin-public.capabilities.md b/docs/development/core/public/kibana-plugin-public.capabilities.md index dda1b2c52f7b5..e7dc542e6ed5e 100644 --- a/docs/development/core/public/kibana-plugin-public.capabilities.md +++ b/docs/development/core/public/kibana-plugin-public.capabilities.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [Capabilities](./kibana-plugin-public.capabilities.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [Capabilities](./kibana-plugin-public.capabilities.md) ## Capabilities interface @@ -17,6 +17,6 @@ export interface Capabilities | Property | Type | Description | | --- | --- | --- | | [catalogue](./kibana-plugin-public.capabilities.catalogue.md) | Record<string, boolean> | Catalogue capabilities. Catalogue entries drive the visibility of the Kibana homepage options. | -| [management](./kibana-plugin-public.capabilities.management.md) | {`

` [sectionId: string]: Record<string, boolean>;`

` } | Management section capabilities. | +| [management](./kibana-plugin-public.capabilities.management.md) | {
[sectionId: string]: Record<string, boolean>;
} | Management section capabilities. | | [navLinks](./kibana-plugin-public.capabilities.navlinks.md) | Record<string, boolean> | Navigation link capabilities. | diff --git a/docs/development/core/public/kibana-plugin-public.capabilities.navlinks.md b/docs/development/core/public/kibana-plugin-public.capabilities.navlinks.md index c3540c092b148..a6c337ef70277 100644 --- a/docs/development/core/public/kibana-plugin-public.capabilities.navlinks.md +++ b/docs/development/core/public/kibana-plugin-public.capabilities.navlinks.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [Capabilities](./kibana-plugin-public.capabilities.md) > [navLinks](./kibana-plugin-public.capabilities.navlinks.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [Capabilities](./kibana-plugin-public.capabilities.md) > [navLinks](./kibana-plugin-public.capabilities.navlinks.md) ## Capabilities.navLinks property diff --git a/docs/development/core/public/kibana-plugin-public.chromebadge.icontype.md b/docs/development/core/public/kibana-plugin-public.chromebadge.icontype.md index 893a656a16141..535b0cb627e7e 100644 --- a/docs/development/core/public/kibana-plugin-public.chromebadge.icontype.md +++ b/docs/development/core/public/kibana-plugin-public.chromebadge.icontype.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeBadge](./kibana-plugin-public.chromebadge.md) > [iconType](./kibana-plugin-public.chromebadge.icontype.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeBadge](./kibana-plugin-public.chromebadge.md) > [iconType](./kibana-plugin-public.chromebadge.icontype.md) ## ChromeBadge.iconType property diff --git a/docs/development/core/public/kibana-plugin-public.chromebadge.md b/docs/development/core/public/kibana-plugin-public.chromebadge.md index c6df61946f36b..5323193dcdd0e 100644 --- a/docs/development/core/public/kibana-plugin-public.chromebadge.md +++ b/docs/development/core/public/kibana-plugin-public.chromebadge.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeBadge](./kibana-plugin-public.chromebadge.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeBadge](./kibana-plugin-public.chromebadge.md) ## ChromeBadge interface diff --git a/docs/development/core/public/kibana-plugin-public.chromebadge.text.md b/docs/development/core/public/kibana-plugin-public.chromebadge.text.md index da0b22b7e9cc8..5b334a8440ee2 100644 --- a/docs/development/core/public/kibana-plugin-public.chromebadge.text.md +++ b/docs/development/core/public/kibana-plugin-public.chromebadge.text.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeBadge](./kibana-plugin-public.chromebadge.md) > [text](./kibana-plugin-public.chromebadge.text.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeBadge](./kibana-plugin-public.chromebadge.md) > [text](./kibana-plugin-public.chromebadge.text.md) ## ChromeBadge.text property diff --git a/docs/development/core/public/kibana-plugin-public.chromebadge.tooltip.md b/docs/development/core/public/kibana-plugin-public.chromebadge.tooltip.md index 1276dbf636ca0..a1a0590cf093d 100644 --- a/docs/development/core/public/kibana-plugin-public.chromebadge.tooltip.md +++ b/docs/development/core/public/kibana-plugin-public.chromebadge.tooltip.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeBadge](./kibana-plugin-public.chromebadge.md) > [tooltip](./kibana-plugin-public.chromebadge.tooltip.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeBadge](./kibana-plugin-public.chromebadge.md) > [tooltip](./kibana-plugin-public.chromebadge.tooltip.md) ## ChromeBadge.tooltip property diff --git a/docs/development/core/public/kibana-plugin-public.chromebrand.logo.md b/docs/development/core/public/kibana-plugin-public.chromebrand.logo.md index d8063ade0e569..7edbfb97fba95 100644 --- a/docs/development/core/public/kibana-plugin-public.chromebrand.logo.md +++ b/docs/development/core/public/kibana-plugin-public.chromebrand.logo.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeBrand](./kibana-plugin-public.chromebrand.md) > [logo](./kibana-plugin-public.chromebrand.logo.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeBrand](./kibana-plugin-public.chromebrand.md) > [logo](./kibana-plugin-public.chromebrand.logo.md) ## ChromeBrand.logo property diff --git a/docs/development/core/public/kibana-plugin-public.chromebrand.md b/docs/development/core/public/kibana-plugin-public.chromebrand.md index 95506b2050643..42af5255c0042 100644 --- a/docs/development/core/public/kibana-plugin-public.chromebrand.md +++ b/docs/development/core/public/kibana-plugin-public.chromebrand.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeBrand](./kibana-plugin-public.chromebrand.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeBrand](./kibana-plugin-public.chromebrand.md) ## ChromeBrand interface diff --git a/docs/development/core/public/kibana-plugin-public.chromebrand.smalllogo.md b/docs/development/core/public/kibana-plugin-public.chromebrand.smalllogo.md index ae569d4a40155..53d05ed89144a 100644 --- a/docs/development/core/public/kibana-plugin-public.chromebrand.smalllogo.md +++ b/docs/development/core/public/kibana-plugin-public.chromebrand.smalllogo.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeBrand](./kibana-plugin-public.chromebrand.md) > [smallLogo](./kibana-plugin-public.chromebrand.smalllogo.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeBrand](./kibana-plugin-public.chromebrand.md) > [smallLogo](./kibana-plugin-public.chromebrand.smalllogo.md) ## ChromeBrand.smallLogo property diff --git a/docs/development/core/public/kibana-plugin-public.chromebreadcrumb.data-test-subj.md b/docs/development/core/public/kibana-plugin-public.chromebreadcrumb.data-test-subj.md index 42e73d0138f1e..6b1294b673c9a 100644 --- a/docs/development/core/public/kibana-plugin-public.chromebreadcrumb.data-test-subj.md +++ b/docs/development/core/public/kibana-plugin-public.chromebreadcrumb.data-test-subj.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeBreadcrumb](./kibana-plugin-public.chromebreadcrumb.md) > [data-test-subj](./kibana-plugin-public.chromebreadcrumb.data-test-subj.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeBreadcrumb](./kibana-plugin-public.chromebreadcrumb.md) > [data-test-subj](./kibana-plugin-public.chromebreadcrumb.data-test-subj.md) ## ChromeBreadcrumb.data-test-subj property diff --git a/docs/development/core/public/kibana-plugin-public.chromebreadcrumb.href.md b/docs/development/core/public/kibana-plugin-public.chromebreadcrumb.href.md index ecf013e859e89..b40f19c99f668 100644 --- a/docs/development/core/public/kibana-plugin-public.chromebreadcrumb.href.md +++ b/docs/development/core/public/kibana-plugin-public.chromebreadcrumb.href.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeBreadcrumb](./kibana-plugin-public.chromebreadcrumb.md) > [href](./kibana-plugin-public.chromebreadcrumb.href.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeBreadcrumb](./kibana-plugin-public.chromebreadcrumb.md) > [href](./kibana-plugin-public.chromebreadcrumb.href.md) ## ChromeBreadcrumb.href property diff --git a/docs/development/core/public/kibana-plugin-public.chromebreadcrumb.md b/docs/development/core/public/kibana-plugin-public.chromebreadcrumb.md index ad5043d071476..9a6e0a7cc8715 100644 --- a/docs/development/core/public/kibana-plugin-public.chromebreadcrumb.md +++ b/docs/development/core/public/kibana-plugin-public.chromebreadcrumb.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeBreadcrumb](./kibana-plugin-public.chromebreadcrumb.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeBreadcrumb](./kibana-plugin-public.chromebreadcrumb.md) ## ChromeBreadcrumb interface diff --git a/docs/development/core/public/kibana-plugin-public.chromebreadcrumb.text.md b/docs/development/core/public/kibana-plugin-public.chromebreadcrumb.text.md index e117b37a57702..fa7e3804576e3 100644 --- a/docs/development/core/public/kibana-plugin-public.chromebreadcrumb.text.md +++ b/docs/development/core/public/kibana-plugin-public.chromebreadcrumb.text.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeBreadcrumb](./kibana-plugin-public.chromebreadcrumb.md) > [text](./kibana-plugin-public.chromebreadcrumb.text.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeBreadcrumb](./kibana-plugin-public.chromebreadcrumb.md) > [text](./kibana-plugin-public.chromebreadcrumb.text.md) ## ChromeBreadcrumb.text property diff --git a/docs/development/core/public/kibana-plugin-public.chromehelpextension.md b/docs/development/core/public/kibana-plugin-public.chromehelpextension.md index 14faaa16c23c6..2414722c090fa 100644 --- a/docs/development/core/public/kibana-plugin-public.chromehelpextension.md +++ b/docs/development/core/public/kibana-plugin-public.chromehelpextension.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeHelpExtension](./kibana-plugin-public.chromehelpextension.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeHelpExtension](./kibana-plugin-public.chromehelpextension.md) ## ChromeHelpExtension type diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.active.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.active.md index 5100532313986..e1546488ba425 100644 --- a/docs/development/core/public/kibana-plugin-public.chromenavlink.active.md +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.active.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [active](./kibana-plugin-public.chromenavlink.active.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [active](./kibana-plugin-public.chromenavlink.active.md) ## ChromeNavLink.active property diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.baseurl.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.baseurl.md index 5d50e45c9fe55..780a17617be04 100644 --- a/docs/development/core/public/kibana-plugin-public.chromenavlink.baseurl.md +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.baseurl.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [baseUrl](./kibana-plugin-public.chromenavlink.baseurl.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [baseUrl](./kibana-plugin-public.chromenavlink.baseurl.md) ## ChromeNavLink.baseUrl property diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.disabled.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.disabled.md index 87f290573b496..8dacb95e3aa18 100644 --- a/docs/development/core/public/kibana-plugin-public.chromenavlink.disabled.md +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.disabled.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [disabled](./kibana-plugin-public.chromenavlink.disabled.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [disabled](./kibana-plugin-public.chromenavlink.disabled.md) ## ChromeNavLink.disabled property diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.euiicontype.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.euiicontype.md index 37d196ae4558a..5373e4705d1b3 100644 --- a/docs/development/core/public/kibana-plugin-public.chromenavlink.euiicontype.md +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.euiicontype.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [euiIconType](./kibana-plugin-public.chromenavlink.euiicontype.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [euiIconType](./kibana-plugin-public.chromenavlink.euiicontype.md) ## ChromeNavLink.euiIconType property diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.hidden.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.hidden.md index cde90415a2df2..431ef0e6b8774 100644 --- a/docs/development/core/public/kibana-plugin-public.chromenavlink.hidden.md +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.hidden.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [hidden](./kibana-plugin-public.chromenavlink.hidden.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [hidden](./kibana-plugin-public.chromenavlink.hidden.md) ## ChromeNavLink.hidden property diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.icon.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.icon.md index 05e182e756d7e..dadb2ab044640 100644 --- a/docs/development/core/public/kibana-plugin-public.chromenavlink.icon.md +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.icon.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [icon](./kibana-plugin-public.chromenavlink.icon.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [icon](./kibana-plugin-public.chromenavlink.icon.md) ## ChromeNavLink.icon property diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.id.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.id.md index 179ca9200178c..7fbabc4a42032 100644 --- a/docs/development/core/public/kibana-plugin-public.chromenavlink.id.md +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.id.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [id](./kibana-plugin-public.chromenavlink.id.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [id](./kibana-plugin-public.chromenavlink.id.md) ## ChromeNavLink.id property diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.linktolastsuburl.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.linktolastsuburl.md index 9a7f438f289a6..fa7020ae52bb9 100644 --- a/docs/development/core/public/kibana-plugin-public.chromenavlink.linktolastsuburl.md +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.linktolastsuburl.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [linkToLastSubUrl](./kibana-plugin-public.chromenavlink.linktolastsuburl.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [linkToLastSubUrl](./kibana-plugin-public.chromenavlink.linktolastsuburl.md) ## ChromeNavLink.linkToLastSubUrl property diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.md index e13efce19c094..b7696aec74be3 100644 --- a/docs/development/core/public/kibana-plugin-public.chromenavlink.md +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) ## ChromeNavLink interface diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.order.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.order.md index 19c86371e334b..e4e2ad2c7a3a7 100644 --- a/docs/development/core/public/kibana-plugin-public.chromenavlink.order.md +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.order.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [order](./kibana-plugin-public.chromenavlink.order.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [order](./kibana-plugin-public.chromenavlink.order.md) ## ChromeNavLink.order property diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.suburlbase.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.suburlbase.md index a00984396cc0c..b3957f22611f4 100644 --- a/docs/development/core/public/kibana-plugin-public.chromenavlink.suburlbase.md +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.suburlbase.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [subUrlBase](./kibana-plugin-public.chromenavlink.suburlbase.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [subUrlBase](./kibana-plugin-public.chromenavlink.suburlbase.md) ## ChromeNavLink.subUrlBase property diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.title.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.title.md index 7c4ff8612f231..a693b971d5178 100644 --- a/docs/development/core/public/kibana-plugin-public.chromenavlink.title.md +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.title.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [title](./kibana-plugin-public.chromenavlink.title.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [title](./kibana-plugin-public.chromenavlink.title.md) ## ChromeNavLink.title property diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.tooltip.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.tooltip.md index c33ca742fae29..e1ff92d8d7442 100644 --- a/docs/development/core/public/kibana-plugin-public.chromenavlink.tooltip.md +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.tooltip.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [tooltip](./kibana-plugin-public.chromenavlink.tooltip.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [tooltip](./kibana-plugin-public.chromenavlink.tooltip.md) ## ChromeNavLink.tooltip property diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.url.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.url.md index bba9d83ab434c..ce9f502fd5d39 100644 --- a/docs/development/core/public/kibana-plugin-public.chromenavlink.url.md +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.url.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [url](./kibana-plugin-public.chromenavlink.url.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [url](./kibana-plugin-public.chromenavlink.url.md) ## ChromeNavLink.url property diff --git a/docs/development/core/public/kibana-plugin-public.chromesetup.md b/docs/development/core/public/kibana-plugin-public.chromesetup.md index aebe27ab73656..7d7fda4a661b0 100644 --- a/docs/development/core/public/kibana-plugin-public.chromesetup.md +++ b/docs/development/core/public/kibana-plugin-public.chromesetup.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeSetup](./kibana-plugin-public.chromesetup.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeSetup](./kibana-plugin-public.chromesetup.md) ## ChromeSetup type diff --git a/docs/development/core/public/kibana-plugin-public.chromestart.md b/docs/development/core/public/kibana-plugin-public.chromestart.md index 28ea29dab9b50..fb3cee30b6ec0 100644 --- a/docs/development/core/public/kibana-plugin-public.chromestart.md +++ b/docs/development/core/public/kibana-plugin-public.chromestart.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeStart](./kibana-plugin-public.chromestart.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeStart](./kibana-plugin-public.chromestart.md) ## ChromeStart type diff --git a/docs/development/core/public/kibana-plugin-public.coresetup.application.md b/docs/development/core/public/kibana-plugin-public.coresetup.application.md deleted file mode 100644 index 1462a9f75f408..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.coresetup.application.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreSetup](./kibana-plugin-public.coresetup.md) > [application](./kibana-plugin-public.coresetup.application.md) - -## CoreSetup.application property - -[ApplicationSetup](./kibana-plugin-public.applicationsetup.md) - -Signature: - -```typescript -application: ApplicationSetup; -``` diff --git a/docs/development/core/public/kibana-plugin-public.coresetup.basepath.md b/docs/development/core/public/kibana-plugin-public.coresetup.basepath.md deleted file mode 100644 index 3d5585fd3bc00..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.coresetup.basepath.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreSetup](./kibana-plugin-public.coresetup.md) > [basePath](./kibana-plugin-public.coresetup.basepath.md) - -## CoreSetup.basePath property - -[BasePathSetup](./kibana-plugin-public.basepathsetup.md) - -Signature: - -```typescript -basePath: BasePathSetup; -``` diff --git a/docs/development/core/public/kibana-plugin-public.coresetup.chrome.md b/docs/development/core/public/kibana-plugin-public.coresetup.chrome.md index eeb50c13a8d00..2b245dab942ac 100644 --- a/docs/development/core/public/kibana-plugin-public.coresetup.chrome.md +++ b/docs/development/core/public/kibana-plugin-public.coresetup.chrome.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreSetup](./kibana-plugin-public.coresetup.md) > [chrome](./kibana-plugin-public.coresetup.chrome.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreSetup](./kibana-plugin-public.coresetup.md) > [chrome](./kibana-plugin-public.coresetup.chrome.md) ## CoreSetup.chrome property diff --git a/docs/development/core/public/kibana-plugin-public.coresetup.fatalerrors.md b/docs/development/core/public/kibana-plugin-public.coresetup.fatalerrors.md index 31a1bdd9de34c..5d51af0898e4f 100644 --- a/docs/development/core/public/kibana-plugin-public.coresetup.fatalerrors.md +++ b/docs/development/core/public/kibana-plugin-public.coresetup.fatalerrors.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreSetup](./kibana-plugin-public.coresetup.md) > [fatalErrors](./kibana-plugin-public.coresetup.fatalerrors.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreSetup](./kibana-plugin-public.coresetup.md) > [fatalErrors](./kibana-plugin-public.coresetup.fatalerrors.md) ## CoreSetup.fatalErrors property diff --git a/docs/development/core/public/kibana-plugin-public.coresetup.http.md b/docs/development/core/public/kibana-plugin-public.coresetup.http.md index 0f699d8313704..7471f7daa668d 100644 --- a/docs/development/core/public/kibana-plugin-public.coresetup.http.md +++ b/docs/development/core/public/kibana-plugin-public.coresetup.http.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreSetup](./kibana-plugin-public.coresetup.md) > [http](./kibana-plugin-public.coresetup.http.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreSetup](./kibana-plugin-public.coresetup.md) > [http](./kibana-plugin-public.coresetup.http.md) ## CoreSetup.http property diff --git a/docs/development/core/public/kibana-plugin-public.coresetup.i18n.md b/docs/development/core/public/kibana-plugin-public.coresetup.i18n.md index c399cc98b4185..5fa4b6d0e26a0 100644 --- a/docs/development/core/public/kibana-plugin-public.coresetup.i18n.md +++ b/docs/development/core/public/kibana-plugin-public.coresetup.i18n.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreSetup](./kibana-plugin-public.coresetup.md) > [i18n](./kibana-plugin-public.coresetup.i18n.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreSetup](./kibana-plugin-public.coresetup.md) > [i18n](./kibana-plugin-public.coresetup.i18n.md) ## CoreSetup.i18n property diff --git a/docs/development/core/public/kibana-plugin-public.coresetup.injectedmetadata.md b/docs/development/core/public/kibana-plugin-public.coresetup.injectedmetadata.md deleted file mode 100644 index 96b477496453f..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.coresetup.injectedmetadata.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreSetup](./kibana-plugin-public.coresetup.md) > [injectedMetadata](./kibana-plugin-public.coresetup.injectedmetadata.md) - -## CoreSetup.injectedMetadata property - -[InjectedMetadataSetup](./kibana-plugin-public.injectedmetadatasetup.md) - -Signature: - -```typescript -injectedMetadata: InjectedMetadataSetup; -``` diff --git a/docs/development/core/public/kibana-plugin-public.coresetup.md b/docs/development/core/public/kibana-plugin-public.coresetup.md index 67af8d3fdb1a2..aac7452fc27a6 100644 --- a/docs/development/core/public/kibana-plugin-public.coresetup.md +++ b/docs/development/core/public/kibana-plugin-public.coresetup.md @@ -1,10 +1,10 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreSetup](./kibana-plugin-public.coresetup.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreSetup](./kibana-plugin-public.coresetup.md) ## CoreSetup interface -Core services exposed to the setup lifecycle +Core services exposed to the `Plugin` setup lifecycle Signature: @@ -16,13 +16,10 @@ export interface CoreSetup | Property | Type | Description | | --- | --- | --- | -| [application](./kibana-plugin-public.coresetup.application.md) | ApplicationSetup | [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) | -| [basePath](./kibana-plugin-public.coresetup.basepath.md) | BasePathSetup | [BasePathSetup](./kibana-plugin-public.basepathsetup.md) | | [chrome](./kibana-plugin-public.coresetup.chrome.md) | ChromeSetup | [ChromeSetup](./kibana-plugin-public.chromesetup.md) | | [fatalErrors](./kibana-plugin-public.coresetup.fatalerrors.md) | FatalErrorsSetup | [FatalErrorsSetup](./kibana-plugin-public.fatalerrorssetup.md) | | [http](./kibana-plugin-public.coresetup.http.md) | HttpSetup | [HttpSetup](./kibana-plugin-public.httpsetup.md) | | [i18n](./kibana-plugin-public.coresetup.i18n.md) | I18nSetup | [I18nSetup](./kibana-plugin-public.i18nsetup.md) | -| [injectedMetadata](./kibana-plugin-public.coresetup.injectedmetadata.md) | InjectedMetadataSetup | [InjectedMetadataSetup](./kibana-plugin-public.injectedmetadatasetup.md) | | [notifications](./kibana-plugin-public.coresetup.notifications.md) | NotificationsSetup | [NotificationsSetup](./kibana-plugin-public.notificationssetup.md) | | [uiSettings](./kibana-plugin-public.coresetup.uisettings.md) | UiSettingsSetup | [UiSettingsSetup](./kibana-plugin-public.uisettingssetup.md) | diff --git a/docs/development/core/public/kibana-plugin-public.coresetup.notifications.md b/docs/development/core/public/kibana-plugin-public.coresetup.notifications.md index ebfb619d082c2..ea050925bbafc 100644 --- a/docs/development/core/public/kibana-plugin-public.coresetup.notifications.md +++ b/docs/development/core/public/kibana-plugin-public.coresetup.notifications.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreSetup](./kibana-plugin-public.coresetup.md) > [notifications](./kibana-plugin-public.coresetup.notifications.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreSetup](./kibana-plugin-public.coresetup.md) > [notifications](./kibana-plugin-public.coresetup.notifications.md) ## CoreSetup.notifications property diff --git a/docs/development/core/public/kibana-plugin-public.coresetup.uisettings.md b/docs/development/core/public/kibana-plugin-public.coresetup.uisettings.md index a84ed85f6f010..fae6040b5337f 100644 --- a/docs/development/core/public/kibana-plugin-public.coresetup.uisettings.md +++ b/docs/development/core/public/kibana-plugin-public.coresetup.uisettings.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreSetup](./kibana-plugin-public.coresetup.md) > [uiSettings](./kibana-plugin-public.coresetup.uisettings.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreSetup](./kibana-plugin-public.coresetup.md) > [uiSettings](./kibana-plugin-public.coresetup.uisettings.md) ## CoreSetup.uiSettings property diff --git a/docs/development/core/public/kibana-plugin-public.corestart.application.md b/docs/development/core/public/kibana-plugin-public.corestart.application.md index 5d54018763e1b..1dd05ff947aeb 100644 --- a/docs/development/core/public/kibana-plugin-public.corestart.application.md +++ b/docs/development/core/public/kibana-plugin-public.corestart.application.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreStart](./kibana-plugin-public.corestart.md) > [application](./kibana-plugin-public.corestart.application.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreStart](./kibana-plugin-public.corestart.md) > [application](./kibana-plugin-public.corestart.application.md) ## CoreStart.application property @@ -9,5 +9,5 @@ Signature: ```typescript -application: ApplicationStart; +application: Pick; ``` diff --git a/docs/development/core/public/kibana-plugin-public.corestart.basepath.md b/docs/development/core/public/kibana-plugin-public.corestart.basepath.md deleted file mode 100644 index c7eb5e19d533a..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.corestart.basepath.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreStart](./kibana-plugin-public.corestart.md) > [basePath](./kibana-plugin-public.corestart.basepath.md) - -## CoreStart.basePath property - -[BasePathStart](./kibana-plugin-public.basepathstart.md) - -Signature: - -```typescript -basePath: BasePathStart; -``` diff --git a/docs/development/core/public/kibana-plugin-public.corestart.chrome.md b/docs/development/core/public/kibana-plugin-public.corestart.chrome.md index 9a574edf6b3e5..390bde25bae93 100644 --- a/docs/development/core/public/kibana-plugin-public.corestart.chrome.md +++ b/docs/development/core/public/kibana-plugin-public.corestart.chrome.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreStart](./kibana-plugin-public.corestart.md) > [chrome](./kibana-plugin-public.corestart.chrome.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreStart](./kibana-plugin-public.corestart.md) > [chrome](./kibana-plugin-public.corestart.chrome.md) ## CoreStart.chrome property diff --git a/docs/development/core/public/kibana-plugin-public.corestart.http.md b/docs/development/core/public/kibana-plugin-public.corestart.http.md index e70b60b8d5285..6af183480c663 100644 --- a/docs/development/core/public/kibana-plugin-public.corestart.http.md +++ b/docs/development/core/public/kibana-plugin-public.corestart.http.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreStart](./kibana-plugin-public.corestart.md) > [http](./kibana-plugin-public.corestart.http.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreStart](./kibana-plugin-public.corestart.md) > [http](./kibana-plugin-public.corestart.http.md) ## CoreStart.http property diff --git a/docs/development/core/public/kibana-plugin-public.corestart.i18n.md b/docs/development/core/public/kibana-plugin-public.corestart.i18n.md index 156dc8f3f9939..6a62025874aa9 100644 --- a/docs/development/core/public/kibana-plugin-public.corestart.i18n.md +++ b/docs/development/core/public/kibana-plugin-public.corestart.i18n.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreStart](./kibana-plugin-public.corestart.md) > [i18n](./kibana-plugin-public.corestart.i18n.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreStart](./kibana-plugin-public.corestart.md) > [i18n](./kibana-plugin-public.corestart.i18n.md) ## CoreStart.i18n property diff --git a/docs/development/core/public/kibana-plugin-public.corestart.injectedmetadata.md b/docs/development/core/public/kibana-plugin-public.corestart.injectedmetadata.md deleted file mode 100644 index f7de664f43f62..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.corestart.injectedmetadata.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreStart](./kibana-plugin-public.corestart.md) > [injectedMetadata](./kibana-plugin-public.corestart.injectedmetadata.md) - -## CoreStart.injectedMetadata property - -[InjectedMetadataStart](./kibana-plugin-public.injectedmetadatastart.md) - -Signature: - -```typescript -injectedMetadata: InjectedMetadataStart; -``` diff --git a/docs/development/core/public/kibana-plugin-public.corestart.md b/docs/development/core/public/kibana-plugin-public.corestart.md index 031c694b3e505..f61459c43910c 100644 --- a/docs/development/core/public/kibana-plugin-public.corestart.md +++ b/docs/development/core/public/kibana-plugin-public.corestart.md @@ -1,10 +1,10 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreStart](./kibana-plugin-public.corestart.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreStart](./kibana-plugin-public.corestart.md) ## CoreStart interface -Core services exposed to the start lifecycle +Core services exposed to the `Plugin` start lifecycle Signature: @@ -16,12 +16,10 @@ export interface CoreStart | Property | Type | Description | | --- | --- | --- | -| [application](./kibana-plugin-public.corestart.application.md) | ApplicationStart | [ApplicationStart](./kibana-plugin-public.applicationstart.md) | -| [basePath](./kibana-plugin-public.corestart.basepath.md) | BasePathStart | [BasePathStart](./kibana-plugin-public.basepathstart.md) | +| [application](./kibana-plugin-public.corestart.application.md) | Pick<ApplicationStart, 'capabilities'> | [ApplicationStart](./kibana-plugin-public.applicationstart.md) | | [chrome](./kibana-plugin-public.corestart.chrome.md) | ChromeStart | [ChromeStart](./kibana-plugin-public.chromestart.md) | | [http](./kibana-plugin-public.corestart.http.md) | HttpStart | [HttpStart](./kibana-plugin-public.httpstart.md) | | [i18n](./kibana-plugin-public.corestart.i18n.md) | I18nStart | [I18nStart](./kibana-plugin-public.i18nstart.md) | -| [injectedMetadata](./kibana-plugin-public.corestart.injectedmetadata.md) | InjectedMetadataStart | [InjectedMetadataStart](./kibana-plugin-public.injectedmetadatastart.md) | | [notifications](./kibana-plugin-public.corestart.notifications.md) | NotificationsStart | [NotificationsStart](./kibana-plugin-public.notificationsstart.md) | | [overlays](./kibana-plugin-public.corestart.overlays.md) | OverlayStart | [OverlayStart](./kibana-plugin-public.overlaystart.md) | diff --git a/docs/development/core/public/kibana-plugin-public.corestart.notifications.md b/docs/development/core/public/kibana-plugin-public.corestart.notifications.md index 7f058f04c88b4..c9533a1ec2f10 100644 --- a/docs/development/core/public/kibana-plugin-public.corestart.notifications.md +++ b/docs/development/core/public/kibana-plugin-public.corestart.notifications.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreStart](./kibana-plugin-public.corestart.md) > [notifications](./kibana-plugin-public.corestart.notifications.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreStart](./kibana-plugin-public.corestart.md) > [notifications](./kibana-plugin-public.corestart.notifications.md) ## CoreStart.notifications property diff --git a/docs/development/core/public/kibana-plugin-public.corestart.overlays.md b/docs/development/core/public/kibana-plugin-public.corestart.overlays.md index c9f626774d0c3..53d20b994f43d 100644 --- a/docs/development/core/public/kibana-plugin-public.corestart.overlays.md +++ b/docs/development/core/public/kibana-plugin-public.corestart.overlays.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreStart](./kibana-plugin-public.corestart.md) > [overlays](./kibana-plugin-public.corestart.overlays.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreStart](./kibana-plugin-public.corestart.md) > [overlays](./kibana-plugin-public.corestart.overlays.md) ## CoreStart.overlays property diff --git a/docs/development/core/public/kibana-plugin-public.errortoastoptions.md b/docs/development/core/public/kibana-plugin-public.errortoastoptions.md new file mode 100644 index 0000000000000..135418632ac98 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.errortoastoptions.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ErrorToastOptions](./kibana-plugin-public.errortoastoptions.md) + +## ErrorToastOptions interface + +Signature: + +```typescript +export interface ErrorToastOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [title](./kibana-plugin-public.errortoastoptions.title.md) | string | The title of the toast and the dialog when expanding the message. | +| [toastMessage](./kibana-plugin-public.errortoastoptions.toastmessage.md) | string | The message to be shown in the toast. If this is not specified the error's message will be shown in the toast instead. Overwriting that message can be used to provide more user-friendly toasts. If you specify this, the error message will still be shown in the detailed error modal. | + diff --git a/docs/development/core/public/kibana-plugin-public.errortoastoptions.title.md b/docs/development/core/public/kibana-plugin-public.errortoastoptions.title.md new file mode 100644 index 0000000000000..8c636998bcbd7 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.errortoastoptions.title.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ErrorToastOptions](./kibana-plugin-public.errortoastoptions.md) > [title](./kibana-plugin-public.errortoastoptions.title.md) + +## ErrorToastOptions.title property + +The title of the toast and the dialog when expanding the message. + +Signature: + +```typescript +title: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.errortoastoptions.toastmessage.md b/docs/development/core/public/kibana-plugin-public.errortoastoptions.toastmessage.md new file mode 100644 index 0000000000000..8094ed3a5bdc7 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.errortoastoptions.toastmessage.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ErrorToastOptions](./kibana-plugin-public.errortoastoptions.md) > [toastMessage](./kibana-plugin-public.errortoastoptions.toastmessage.md) + +## ErrorToastOptions.toastMessage property + +The message to be shown in the toast. If this is not specified the error's message will be shown in the toast instead. Overwriting that message can be used to provide more user-friendly toasts. If you specify this, the error message will still be shown in the detailed error modal. + +Signature: + +```typescript +toastMessage?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.fatalerrorinfo.md b/docs/development/core/public/kibana-plugin-public.fatalerrorinfo.md index ff326a2122b3c..a1e2a95ec9bb1 100644 --- a/docs/development/core/public/kibana-plugin-public.fatalerrorinfo.md +++ b/docs/development/core/public/kibana-plugin-public.fatalerrorinfo.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [FatalErrorInfo](./kibana-plugin-public.fatalerrorinfo.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [FatalErrorInfo](./kibana-plugin-public.fatalerrorinfo.md) ## FatalErrorInfo interface diff --git a/docs/development/core/public/kibana-plugin-public.fatalerrorinfo.message.md b/docs/development/core/public/kibana-plugin-public.fatalerrorinfo.message.md index 68b2c912a9030..8eebba48f0777 100644 --- a/docs/development/core/public/kibana-plugin-public.fatalerrorinfo.message.md +++ b/docs/development/core/public/kibana-plugin-public.fatalerrorinfo.message.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [FatalErrorInfo](./kibana-plugin-public.fatalerrorinfo.md) > [message](./kibana-plugin-public.fatalerrorinfo.message.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [FatalErrorInfo](./kibana-plugin-public.fatalerrorinfo.md) > [message](./kibana-plugin-public.fatalerrorinfo.message.md) ## FatalErrorInfo.message property diff --git a/docs/development/core/public/kibana-plugin-public.fatalerrorinfo.stack.md b/docs/development/core/public/kibana-plugin-public.fatalerrorinfo.stack.md index c251a4866cbda..5578e4f8c8acd 100644 --- a/docs/development/core/public/kibana-plugin-public.fatalerrorinfo.stack.md +++ b/docs/development/core/public/kibana-plugin-public.fatalerrorinfo.stack.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [FatalErrorInfo](./kibana-plugin-public.fatalerrorinfo.md) > [stack](./kibana-plugin-public.fatalerrorinfo.stack.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [FatalErrorInfo](./kibana-plugin-public.fatalerrorinfo.md) > [stack](./kibana-plugin-public.fatalerrorinfo.stack.md) ## FatalErrorInfo.stack property diff --git a/docs/development/core/public/kibana-plugin-public.fatalerrorssetup.add.md b/docs/development/core/public/kibana-plugin-public.fatalerrorssetup.add.md index 1bfcd84015e3c..31a1c239388ee 100644 --- a/docs/development/core/public/kibana-plugin-public.fatalerrorssetup.add.md +++ b/docs/development/core/public/kibana-plugin-public.fatalerrorssetup.add.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [FatalErrorsSetup](./kibana-plugin-public.fatalerrorssetup.md) > [add](./kibana-plugin-public.fatalerrorssetup.add.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [FatalErrorsSetup](./kibana-plugin-public.fatalerrorssetup.md) > [add](./kibana-plugin-public.fatalerrorssetup.add.md) ## FatalErrorsSetup.add property diff --git a/docs/development/core/public/kibana-plugin-public.fatalerrorssetup.get$.md b/docs/development/core/public/kibana-plugin-public.fatalerrorssetup.get$.md index e3bc308a3dd8b..1a7f6fa185e2b 100644 --- a/docs/development/core/public/kibana-plugin-public.fatalerrorssetup.get$.md +++ b/docs/development/core/public/kibana-plugin-public.fatalerrorssetup.get$.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [FatalErrorsSetup](./kibana-plugin-public.fatalerrorssetup.md) > [get$](./kibana-plugin-public.fatalerrorssetup.get$.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [FatalErrorsSetup](./kibana-plugin-public.fatalerrorssetup.md) > [get$](./kibana-plugin-public.fatalerrorssetup.get$.md) ## FatalErrorsSetup.get$ property diff --git a/docs/development/core/public/kibana-plugin-public.fatalerrorssetup.md b/docs/development/core/public/kibana-plugin-public.fatalerrorssetup.md index c2e79ca191859..d9f4605905fd8 100644 --- a/docs/development/core/public/kibana-plugin-public.fatalerrorssetup.md +++ b/docs/development/core/public/kibana-plugin-public.fatalerrorssetup.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [FatalErrorsSetup](./kibana-plugin-public.fatalerrorssetup.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [FatalErrorsSetup](./kibana-plugin-public.fatalerrorssetup.md) ## FatalErrorsSetup interface diff --git a/docs/development/core/public/kibana-plugin-public.httpinterceptor.md b/docs/development/core/public/kibana-plugin-public.httpinterceptor.md new file mode 100644 index 0000000000000..c694151701ffb --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpinterceptor.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md) + +## HttpInterceptor interface + + +Signature: + +```typescript +export interface HttpInterceptor +``` + +## Methods + +| Method | Description | +| --- | --- | +| [request(request, controller)](./kibana-plugin-public.httpinterceptor.request.md) | | +| [requestError(httpErrorRequest, controller)](./kibana-plugin-public.httpinterceptor.requesterror.md) | | +| [response(httpResponse, controller)](./kibana-plugin-public.httpinterceptor.response.md) | | +| [responseError(httpErrorResponse, controller)](./kibana-plugin-public.httpinterceptor.responseerror.md) | | + diff --git a/docs/development/core/public/kibana-plugin-public.httpinterceptor.request.md b/docs/development/core/public/kibana-plugin-public.httpinterceptor.request.md new file mode 100644 index 0000000000000..0e57cf8dc51f9 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpinterceptor.request.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md) > [request](./kibana-plugin-public.httpinterceptor.request.md) + +## HttpInterceptor.request() method + +Signature: + +```typescript +request?(request: Request, controller: HttpInterceptController): Promise | Request | void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| request | Request | | +| controller | HttpInterceptController | | + +Returns: + +`Promise | Request | void` + diff --git a/docs/development/core/public/kibana-plugin-public.httpinterceptor.requesterror.md b/docs/development/core/public/kibana-plugin-public.httpinterceptor.requesterror.md new file mode 100644 index 0000000000000..5c95e15697c35 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpinterceptor.requesterror.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md) > [requestError](./kibana-plugin-public.httpinterceptor.requesterror.md) + +## HttpInterceptor.requestError() method + +Signature: + +```typescript +requestError?(httpErrorRequest: HttpErrorRequest, controller: HttpInterceptController): Promise | Request | void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| httpErrorRequest | HttpErrorRequest | | +| controller | HttpInterceptController | | + +Returns: + +`Promise | Request | void` + diff --git a/docs/development/core/public/kibana-plugin-public.httpinterceptor.response.md b/docs/development/core/public/kibana-plugin-public.httpinterceptor.response.md new file mode 100644 index 0000000000000..01609eb2bbac7 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpinterceptor.response.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md) > [response](./kibana-plugin-public.httpinterceptor.response.md) + +## HttpInterceptor.response() method + +Signature: + +```typescript +response?(httpResponse: HttpResponse, controller: HttpInterceptController): Promise | HttpResponse | void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| httpResponse | HttpResponse | | +| controller | HttpInterceptController | | + +Returns: + +`Promise | HttpResponse | void` + diff --git a/docs/development/core/public/kibana-plugin-public.httpinterceptor.responseerror.md b/docs/development/core/public/kibana-plugin-public.httpinterceptor.responseerror.md new file mode 100644 index 0000000000000..71e0f11205d7b --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpinterceptor.responseerror.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md) > [responseError](./kibana-plugin-public.httpinterceptor.responseerror.md) + +## HttpInterceptor.responseError() method + +Signature: + +```typescript +responseError?(httpErrorResponse: HttpErrorResponse, controller: HttpInterceptController): Promise | HttpResponse | void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| httpErrorResponse | HttpErrorResponse | | +| controller | HttpInterceptController | | + +Returns: + +`Promise | HttpResponse | void` + diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.addloadingcount.md b/docs/development/core/public/kibana-plugin-public.httpservicebase.addloadingcount.md new file mode 100644 index 0000000000000..0dc64a3f75443 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpservicebase.addloadingcount.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [addLoadingCount](./kibana-plugin-public.httpservicebase.addloadingcount.md) + +## HttpServiceBase.addLoadingCount() method + +Signature: + +```typescript +addLoadingCount(count$: Observable): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| count$ | Observable<number> | | + +Returns: + +`void` + diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.delete.md b/docs/development/core/public/kibana-plugin-public.httpservicebase.delete.md new file mode 100644 index 0000000000000..55e67a50b156f --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpservicebase.delete.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [delete](./kibana-plugin-public.httpservicebase.delete.md) + +## HttpServiceBase.delete property + +Signature: + +```typescript +delete: HttpHandler; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.fetch.md b/docs/development/core/public/kibana-plugin-public.httpservicebase.fetch.md new file mode 100644 index 0000000000000..92936230149b6 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpservicebase.fetch.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [fetch](./kibana-plugin-public.httpservicebase.fetch.md) + +## HttpServiceBase.fetch property + +Signature: + +```typescript +fetch: HttpHandler; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.get.md b/docs/development/core/public/kibana-plugin-public.httpservicebase.get.md new file mode 100644 index 0000000000000..d4b9a3810a497 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpservicebase.get.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [get](./kibana-plugin-public.httpservicebase.get.md) + +## HttpServiceBase.get property + +Signature: + +```typescript +get: HttpHandler; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.getbasepath.md b/docs/development/core/public/kibana-plugin-public.httpservicebase.getbasepath.md new file mode 100644 index 0000000000000..918f0514b3c4b --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpservicebase.getbasepath.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [getBasePath](./kibana-plugin-public.httpservicebase.getbasepath.md) + +## HttpServiceBase.getBasePath() method + +Signature: + +```typescript +getBasePath(): string; +``` +Returns: + +`string` + diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.getloadingcount$.md b/docs/development/core/public/kibana-plugin-public.httpservicebase.getloadingcount$.md new file mode 100644 index 0000000000000..5d8967a458257 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpservicebase.getloadingcount$.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [getLoadingCount$](./kibana-plugin-public.httpservicebase.getloadingcount$.md) + +## HttpServiceBase.getLoadingCount$() method + +Signature: + +```typescript +getLoadingCount$(): Observable; +``` +Returns: + +`Observable` + diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.head.md b/docs/development/core/public/kibana-plugin-public.httpservicebase.head.md new file mode 100644 index 0000000000000..78546d0fbb4f3 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpservicebase.head.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [head](./kibana-plugin-public.httpservicebase.head.md) + +## HttpServiceBase.head property + +Signature: + +```typescript +head: HttpHandler; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.intercept.md b/docs/development/core/public/kibana-plugin-public.httpservicebase.intercept.md new file mode 100644 index 0000000000000..805884a21d18f --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpservicebase.intercept.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [intercept](./kibana-plugin-public.httpservicebase.intercept.md) + +## HttpServiceBase.intercept() method + +Signature: + +```typescript +intercept(interceptor: HttpInterceptor): () => void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| interceptor | HttpInterceptor | | + +Returns: + +`() => void` + diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.md b/docs/development/core/public/kibana-plugin-public.httpservicebase.md new file mode 100644 index 0000000000000..2287855e20122 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpservicebase.md @@ -0,0 +1,39 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) + +## HttpServiceBase interface + + +Signature: + +```typescript +export interface HttpServiceBase +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [delete](./kibana-plugin-public.httpservicebase.delete.md) | HttpHandler | | +| [fetch](./kibana-plugin-public.httpservicebase.fetch.md) | HttpHandler | | +| [get](./kibana-plugin-public.httpservicebase.get.md) | HttpHandler | | +| [head](./kibana-plugin-public.httpservicebase.head.md) | HttpHandler | | +| [options](./kibana-plugin-public.httpservicebase.options.md) | HttpHandler | | +| [patch](./kibana-plugin-public.httpservicebase.patch.md) | HttpHandler | | +| [post](./kibana-plugin-public.httpservicebase.post.md) | HttpHandler | | +| [put](./kibana-plugin-public.httpservicebase.put.md) | HttpHandler | | + +## Methods + +| Method | Description | +| --- | --- | +| [addLoadingCount(count$)](./kibana-plugin-public.httpservicebase.addloadingcount.md) | | +| [getBasePath()](./kibana-plugin-public.httpservicebase.getbasepath.md) | | +| [getLoadingCount$()](./kibana-plugin-public.httpservicebase.getloadingcount$.md) | | +| [intercept(interceptor)](./kibana-plugin-public.httpservicebase.intercept.md) | | +| [prependBasePath(path)](./kibana-plugin-public.httpservicebase.prependbasepath.md) | | +| [removeAllInterceptors()](./kibana-plugin-public.httpservicebase.removeallinterceptors.md) | | +| [removeBasePath(path)](./kibana-plugin-public.httpservicebase.removebasepath.md) | | +| [stop()](./kibana-plugin-public.httpservicebase.stop.md) | | + diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.options.md b/docs/development/core/public/kibana-plugin-public.httpservicebase.options.md new file mode 100644 index 0000000000000..6579a92f695a4 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpservicebase.options.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [options](./kibana-plugin-public.httpservicebase.options.md) + +## HttpServiceBase.options property + +Signature: + +```typescript +options: HttpHandler; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.patch.md b/docs/development/core/public/kibana-plugin-public.httpservicebase.patch.md new file mode 100644 index 0000000000000..3b81f2f863376 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpservicebase.patch.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [patch](./kibana-plugin-public.httpservicebase.patch.md) + +## HttpServiceBase.patch property + +Signature: + +```typescript +patch: HttpHandler; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.post.md b/docs/development/core/public/kibana-plugin-public.httpservicebase.post.md new file mode 100644 index 0000000000000..b990fd1458c33 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpservicebase.post.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [post](./kibana-plugin-public.httpservicebase.post.md) + +## HttpServiceBase.post property + +Signature: + +```typescript +post: HttpHandler; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.prependbasepath.md b/docs/development/core/public/kibana-plugin-public.httpservicebase.prependbasepath.md new file mode 100644 index 0000000000000..a4ae95eb1c307 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpservicebase.prependbasepath.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [prependBasePath](./kibana-plugin-public.httpservicebase.prependbasepath.md) + +## HttpServiceBase.prependBasePath() method + +Signature: + +```typescript +prependBasePath(path: string): string; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| path | string | | + +Returns: + +`string` + diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.put.md b/docs/development/core/public/kibana-plugin-public.httpservicebase.put.md new file mode 100644 index 0000000000000..3f41c6c21fec6 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpservicebase.put.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [put](./kibana-plugin-public.httpservicebase.put.md) + +## HttpServiceBase.put property + +Signature: + +```typescript +put: HttpHandler; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.removeallinterceptors.md b/docs/development/core/public/kibana-plugin-public.httpservicebase.removeallinterceptors.md new file mode 100644 index 0000000000000..405baf458e516 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpservicebase.removeallinterceptors.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [removeAllInterceptors](./kibana-plugin-public.httpservicebase.removeallinterceptors.md) + +## HttpServiceBase.removeAllInterceptors() method + +Signature: + +```typescript +removeAllInterceptors(): void; +``` +Returns: + +`void` + diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.removebasepath.md b/docs/development/core/public/kibana-plugin-public.httpservicebase.removebasepath.md new file mode 100644 index 0000000000000..90fefb1532a68 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpservicebase.removebasepath.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [removeBasePath](./kibana-plugin-public.httpservicebase.removebasepath.md) + +## HttpServiceBase.removeBasePath() method + +Signature: + +```typescript +removeBasePath(path: string): string; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| path | string | | + +Returns: + +`string` + diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.stop.md b/docs/development/core/public/kibana-plugin-public.httpservicebase.stop.md new file mode 100644 index 0000000000000..2a2323dbdda16 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpservicebase.stop.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [stop](./kibana-plugin-public.httpservicebase.stop.md) + +## HttpServiceBase.stop() method + +Signature: + +```typescript +stop(): void; +``` +Returns: + +`void` + diff --git a/docs/development/core/public/kibana-plugin-public.httpsetup.md b/docs/development/core/public/kibana-plugin-public.httpsetup.md index 08622497df708..cc8ae6ab6aac1 100644 --- a/docs/development/core/public/kibana-plugin-public.httpsetup.md +++ b/docs/development/core/public/kibana-plugin-public.httpsetup.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpSetup](./kibana-plugin-public.httpsetup.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpSetup](./kibana-plugin-public.httpsetup.md) ## HttpSetup type @@ -8,5 +8,5 @@ Signature: ```typescript -export declare type HttpSetup = ReturnType; +export declare type HttpSetup = HttpServiceBase; ``` diff --git a/docs/development/core/public/kibana-plugin-public.httpstart.md b/docs/development/core/public/kibana-plugin-public.httpstart.md index 333c6c140ea50..f70c08a1bde50 100644 --- a/docs/development/core/public/kibana-plugin-public.httpstart.md +++ b/docs/development/core/public/kibana-plugin-public.httpstart.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpStart](./kibana-plugin-public.httpstart.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpStart](./kibana-plugin-public.httpstart.md) ## HttpStart type @@ -8,5 +8,5 @@ Signature: ```typescript -export declare type HttpStart = ReturnType; +export declare type HttpStart = HttpServiceBase; ``` diff --git a/docs/development/core/public/kibana-plugin-public.i18nsetup.context.md b/docs/development/core/public/kibana-plugin-public.i18nsetup.context.md index 685499971650e..440456f5cdd63 100644 --- a/docs/development/core/public/kibana-plugin-public.i18nsetup.context.md +++ b/docs/development/core/public/kibana-plugin-public.i18nsetup.context.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [I18nSetup](./kibana-plugin-public.i18nsetup.md) > [Context](./kibana-plugin-public.i18nsetup.context.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [I18nSetup](./kibana-plugin-public.i18nsetup.md) > [Context](./kibana-plugin-public.i18nsetup.context.md) ## I18nSetup.Context property diff --git a/docs/development/core/public/kibana-plugin-public.i18nsetup.md b/docs/development/core/public/kibana-plugin-public.i18nsetup.md index 2fc46b2564000..350fde1636f6a 100644 --- a/docs/development/core/public/kibana-plugin-public.i18nsetup.md +++ b/docs/development/core/public/kibana-plugin-public.i18nsetup.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [I18nSetup](./kibana-plugin-public.i18nsetup.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [I18nSetup](./kibana-plugin-public.i18nsetup.md) ## I18nSetup interface @@ -16,5 +16,5 @@ export interface I18nSetup | Property | Type | Description | | --- | --- | --- | -| [Context](./kibana-plugin-public.i18nsetup.context.md) | ({ children }: {`

` children: React.ReactNode;`

` }) => JSX.Element | React Context provider required as the topmost component for any i18n-compatible React tree. | +| [Context](./kibana-plugin-public.i18nsetup.context.md) | ({ children }: {
children: React.ReactNode;
}) => JSX.Element | React Context provider required as the topmost component for any i18n-compatible React tree. | diff --git a/docs/development/core/public/kibana-plugin-public.i18nstart.md b/docs/development/core/public/kibana-plugin-public.i18nstart.md index 297e4f9cee5c7..e5d8b9194aec2 100644 --- a/docs/development/core/public/kibana-plugin-public.i18nstart.md +++ b/docs/development/core/public/kibana-plugin-public.i18nstart.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [I18nStart](./kibana-plugin-public.i18nstart.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [I18nStart](./kibana-plugin-public.i18nstart.md) ## I18nStart type diff --git a/docs/development/core/public/kibana-plugin-public.injectedmetadatasetup.getbasepath.md b/docs/development/core/public/kibana-plugin-public.injectedmetadatasetup.getbasepath.md deleted file mode 100644 index 60cbab119c8fe..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.injectedmetadatasetup.getbasepath.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [InjectedMetadataSetup](./kibana-plugin-public.injectedmetadatasetup.md) > [getBasePath](./kibana-plugin-public.injectedmetadatasetup.getbasepath.md) - -## InjectedMetadataSetup.getBasePath property - -Signature: - -```typescript -getBasePath: () => string; -``` diff --git a/docs/development/core/public/kibana-plugin-public.injectedmetadatasetup.getcspconfig.md b/docs/development/core/public/kibana-plugin-public.injectedmetadatasetup.getcspconfig.md deleted file mode 100644 index 0bb957787e5a9..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.injectedmetadatasetup.getcspconfig.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [InjectedMetadataSetup](./kibana-plugin-public.injectedmetadatasetup.md) > [getCspConfig](./kibana-plugin-public.injectedmetadatasetup.getcspconfig.md) - -## InjectedMetadataSetup.getCspConfig property - -Signature: - -```typescript -getCspConfig: () => { - warnLegacyBrowsers: boolean; - }; -``` diff --git a/docs/development/core/public/kibana-plugin-public.injectedmetadatasetup.getinjectedvar.md b/docs/development/core/public/kibana-plugin-public.injectedmetadatasetup.getinjectedvar.md deleted file mode 100644 index 97954a799cd77..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.injectedmetadatasetup.getinjectedvar.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [InjectedMetadataSetup](./kibana-plugin-public.injectedmetadatasetup.md) > [getInjectedVar](./kibana-plugin-public.injectedmetadatasetup.getinjectedvar.md) - -## InjectedMetadataSetup.getInjectedVar property - -Signature: - -```typescript -getInjectedVar: (name: string, defaultValue?: any) => unknown; -``` diff --git a/docs/development/core/public/kibana-plugin-public.injectedmetadatasetup.getinjectedvars.md b/docs/development/core/public/kibana-plugin-public.injectedmetadatasetup.getinjectedvars.md deleted file mode 100644 index 1cf2e8f9290cd..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.injectedmetadatasetup.getinjectedvars.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [InjectedMetadataSetup](./kibana-plugin-public.injectedmetadatasetup.md) > [getInjectedVars](./kibana-plugin-public.injectedmetadatasetup.getinjectedvars.md) - -## InjectedMetadataSetup.getInjectedVars property - -Signature: - -```typescript -getInjectedVars: () => { - [key: string]: unknown; - }; -``` diff --git a/docs/development/core/public/kibana-plugin-public.injectedmetadatasetup.getkibanabuildnumber.md b/docs/development/core/public/kibana-plugin-public.injectedmetadatasetup.getkibanabuildnumber.md deleted file mode 100644 index e5a88b65b5620..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.injectedmetadatasetup.getkibanabuildnumber.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [InjectedMetadataSetup](./kibana-plugin-public.injectedmetadatasetup.md) > [getKibanaBuildNumber](./kibana-plugin-public.injectedmetadatasetup.getkibanabuildnumber.md) - -## InjectedMetadataSetup.getKibanaBuildNumber property - -Signature: - -```typescript -getKibanaBuildNumber: () => number; -``` diff --git a/docs/development/core/public/kibana-plugin-public.injectedmetadatasetup.getkibanaversion.md b/docs/development/core/public/kibana-plugin-public.injectedmetadatasetup.getkibanaversion.md deleted file mode 100644 index d5a7b1acdc85a..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.injectedmetadatasetup.getkibanaversion.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [InjectedMetadataSetup](./kibana-plugin-public.injectedmetadatasetup.md) > [getKibanaVersion](./kibana-plugin-public.injectedmetadatasetup.getkibanaversion.md) - -## InjectedMetadataSetup.getKibanaVersion property - -Signature: - -```typescript -getKibanaVersion: () => string; -``` diff --git a/docs/development/core/public/kibana-plugin-public.injectedmetadatasetup.getlegacymetadata.md b/docs/development/core/public/kibana-plugin-public.injectedmetadatasetup.getlegacymetadata.md deleted file mode 100644 index 5276bda7abc46..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.injectedmetadatasetup.getlegacymetadata.md +++ /dev/null @@ -1,27 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [InjectedMetadataSetup](./kibana-plugin-public.injectedmetadatasetup.md) > [getLegacyMetadata](./kibana-plugin-public.injectedmetadatasetup.getlegacymetadata.md) - -## InjectedMetadataSetup.getLegacyMetadata property - -Signature: - -```typescript -getLegacyMetadata: () => { - app: unknown; - translations: unknown; - bundleId: string; - nav: LegacyNavLink[]; - version: string; - branch: string; - buildNum: number; - buildSha: string; - basePath: string; - serverName: string; - devMode: boolean; - uiSettings: { - defaults: UiSettingsState; - user?: UiSettingsState | undefined; - }; - }; -``` diff --git a/docs/development/core/public/kibana-plugin-public.injectedmetadatasetup.getplugins.md b/docs/development/core/public/kibana-plugin-public.injectedmetadatasetup.getplugins.md deleted file mode 100644 index 7578d0df81b8d..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.injectedmetadatasetup.getplugins.md +++ /dev/null @@ -1,16 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [InjectedMetadataSetup](./kibana-plugin-public.injectedmetadatasetup.md) > [getPlugins](./kibana-plugin-public.injectedmetadatasetup.getplugins.md) - -## InjectedMetadataSetup.getPlugins property - -An array of frontend plugins in topological order. - -Signature: - -```typescript -getPlugins: () => Array<{ - id: string; - plugin: DiscoveredPlugin; - }>; -``` diff --git a/docs/development/core/public/kibana-plugin-public.injectedmetadatasetup.md b/docs/development/core/public/kibana-plugin-public.injectedmetadatasetup.md deleted file mode 100644 index f6f621cdada45..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.injectedmetadatasetup.md +++ /dev/null @@ -1,27 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [InjectedMetadataSetup](./kibana-plugin-public.injectedmetadatasetup.md) - -## InjectedMetadataSetup interface - -Provides access to the metadata injected by the server into the page - -Signature: - -```typescript -export interface InjectedMetadataSetup -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [getBasePath](./kibana-plugin-public.injectedmetadatasetup.getbasepath.md) | () => string | | -| [getCspConfig](./kibana-plugin-public.injectedmetadatasetup.getcspconfig.md) | () => {`

` warnLegacyBrowsers: boolean;`

` } | | -| [getInjectedVar](./kibana-plugin-public.injectedmetadatasetup.getinjectedvar.md) | (name: string, defaultValue?: any) => unknown | | -| [getInjectedVars](./kibana-plugin-public.injectedmetadatasetup.getinjectedvars.md) | () => {`

` [key: string]: unknown;`

` } | | -| [getKibanaBuildNumber](./kibana-plugin-public.injectedmetadatasetup.getkibanabuildnumber.md) | () => number | | -| [getKibanaVersion](./kibana-plugin-public.injectedmetadatasetup.getkibanaversion.md) | () => string | | -| [getLegacyMetadata](./kibana-plugin-public.injectedmetadatasetup.getlegacymetadata.md) | () => {`

` app: unknown;`

` translations: unknown;`

` bundleId: string;`

` nav: LegacyNavLink[];`

` version: string;`

` branch: string;`

` buildNum: number;`

` buildSha: string;`

` basePath: string;`

` serverName: string;`

` devMode: boolean;`

` uiSettings: {`

` defaults: UiSettingsState;`

` user?: UiSettingsState | undefined;`

` };`

` } | | -| [getPlugins](./kibana-plugin-public.injectedmetadatasetup.getplugins.md) | () => Array<{`

` id: string;`

` plugin: DiscoveredPlugin;`

` }> | An array of frontend plugins in topological order. | - diff --git a/docs/development/core/public/kibana-plugin-public.injectedmetadatastart.md b/docs/development/core/public/kibana-plugin-public.injectedmetadatastart.md deleted file mode 100644 index 0feb6f910a68e..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.injectedmetadatastart.md +++ /dev/null @@ -1,12 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [InjectedMetadataStart](./kibana-plugin-public.injectedmetadatastart.md) - -## InjectedMetadataStart type - - -Signature: - -```typescript -export declare type InjectedMetadataStart = InjectedMetadataSetup; -``` diff --git a/docs/development/core/public/kibana-plugin-public.legacynavlink.euiicontype.md b/docs/development/core/public/kibana-plugin-public.legacynavlink.euiicontype.md index b7c5c1056e517..bf0308e88d506 100644 --- a/docs/development/core/public/kibana-plugin-public.legacynavlink.euiicontype.md +++ b/docs/development/core/public/kibana-plugin-public.legacynavlink.euiicontype.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) > [euiIconType](./kibana-plugin-public.legacynavlink.euiicontype.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) > [euiIconType](./kibana-plugin-public.legacynavlink.euiicontype.md) ## LegacyNavLink.euiIconType property diff --git a/docs/development/core/public/kibana-plugin-public.legacynavlink.icon.md b/docs/development/core/public/kibana-plugin-public.legacynavlink.icon.md index fefc6876ea1fc..5dfe64c3a9610 100644 --- a/docs/development/core/public/kibana-plugin-public.legacynavlink.icon.md +++ b/docs/development/core/public/kibana-plugin-public.legacynavlink.icon.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) > [icon](./kibana-plugin-public.legacynavlink.icon.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) > [icon](./kibana-plugin-public.legacynavlink.icon.md) ## LegacyNavLink.icon property diff --git a/docs/development/core/public/kibana-plugin-public.legacynavlink.id.md b/docs/development/core/public/kibana-plugin-public.legacynavlink.id.md index 3c6640f98dae5..c8d8b025e48ee 100644 --- a/docs/development/core/public/kibana-plugin-public.legacynavlink.id.md +++ b/docs/development/core/public/kibana-plugin-public.legacynavlink.id.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) > [id](./kibana-plugin-public.legacynavlink.id.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) > [id](./kibana-plugin-public.legacynavlink.id.md) ## LegacyNavLink.id property diff --git a/docs/development/core/public/kibana-plugin-public.legacynavlink.md b/docs/development/core/public/kibana-plugin-public.legacynavlink.md index 8cc6aaf06334a..fc0c445f517b3 100644 --- a/docs/development/core/public/kibana-plugin-public.legacynavlink.md +++ b/docs/development/core/public/kibana-plugin-public.legacynavlink.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) ## LegacyNavLink interface diff --git a/docs/development/core/public/kibana-plugin-public.legacynavlink.order.md b/docs/development/core/public/kibana-plugin-public.legacynavlink.order.md index 629916470576f..bfb2a2caad623 100644 --- a/docs/development/core/public/kibana-plugin-public.legacynavlink.order.md +++ b/docs/development/core/public/kibana-plugin-public.legacynavlink.order.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) > [order](./kibana-plugin-public.legacynavlink.order.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) > [order](./kibana-plugin-public.legacynavlink.order.md) ## LegacyNavLink.order property diff --git a/docs/development/core/public/kibana-plugin-public.legacynavlink.title.md b/docs/development/core/public/kibana-plugin-public.legacynavlink.title.md index 2d514b9df3ac0..2cb7a4ebdbc76 100644 --- a/docs/development/core/public/kibana-plugin-public.legacynavlink.title.md +++ b/docs/development/core/public/kibana-plugin-public.legacynavlink.title.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) > [title](./kibana-plugin-public.legacynavlink.title.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) > [title](./kibana-plugin-public.legacynavlink.title.md) ## LegacyNavLink.title property diff --git a/docs/development/core/public/kibana-plugin-public.legacynavlink.url.md b/docs/development/core/public/kibana-plugin-public.legacynavlink.url.md index 6fe3444269370..fc2d55109dc98 100644 --- a/docs/development/core/public/kibana-plugin-public.legacynavlink.url.md +++ b/docs/development/core/public/kibana-plugin-public.legacynavlink.url.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) > [url](./kibana-plugin-public.legacynavlink.url.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) > [url](./kibana-plugin-public.legacynavlink.url.md) ## LegacyNavLink.url property diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md index 435b7fa763749..c5deffd3eca32 100644 --- a/docs/development/core/public/kibana-plugin-public.md +++ b/docs/development/core/public/kibana-plugin-public.md @@ -1,9 +1,15 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) ## kibana-plugin-public package +The Kibana Core APIs for client-side plugins. + +A plugin's `public/index` file must contain a named import, `plugin`, that implements [PluginInitializer](./kibana-plugin-public.plugininitializer.md) which returns an object that implements [Plugin](./kibana-plugin-public.plugin.md). + +The plugin integrates with the core system via lifecycle events: `setup`, `start`, and `stop`. In each lifecycle method, the plugin will receive the corresponding core services available (either [CoreSetup](./kibana-plugin-public.coresetup.md) or [CoreStart](./kibana-plugin-public.corestart.md)) and any interfaces returned by dependency plugins' lifecycle method. Anything returned by the plugin's lifecycle method will be exposed to downstream dependencies when their corresponding lifecycle methods are invoked. + ## Classes | Class | Description | @@ -17,42 +23,40 @@ | --- | --- | | [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) | | | [ApplicationStart](./kibana-plugin-public.applicationstart.md) | | -| [BasePathSetup](./kibana-plugin-public.basepathsetup.md) | Provides access to the 'server.basePath' configuration option in kibana.yml | | [Capabilities](./kibana-plugin-public.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. | | [ChromeBadge](./kibana-plugin-public.chromebadge.md) | | | [ChromeBrand](./kibana-plugin-public.chromebrand.md) | | | [ChromeBreadcrumb](./kibana-plugin-public.chromebreadcrumb.md) | | | [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) | | -| [CoreSetup](./kibana-plugin-public.coresetup.md) | Core services exposed to the setup lifecycle | -| [CoreStart](./kibana-plugin-public.corestart.md) | Core services exposed to the start lifecycle | +| [CoreSetup](./kibana-plugin-public.coresetup.md) | Core services exposed to the Plugin setup lifecycle | +| [CoreStart](./kibana-plugin-public.corestart.md) | Core services exposed to the Plugin start lifecycle | +| [ErrorToastOptions](./kibana-plugin-public.errortoastoptions.md) | | | [FatalErrorInfo](./kibana-plugin-public.fatalerrorinfo.md) | Represents the message and stack of a fatal Error | | [FatalErrorsSetup](./kibana-plugin-public.fatalerrorssetup.md) | FatalErrors stop the Kibana Public Core and displays a fatal error screen with details about the Kibana build and the error. | +| [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md) | | +| [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) | | | [I18nSetup](./kibana-plugin-public.i18nsetup.md) | I18nSetup.Context is required by any localizable React component from @kbn/i18n and @elastic/eui packages and is supposed to be used as the topmost component for any i18n-compatible React tree. | -| [InjectedMetadataSetup](./kibana-plugin-public.injectedmetadatasetup.md) | Provides access to the metadata injected by the server into the page | | [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) | | | [NotificationsSetup](./kibana-plugin-public.notificationssetup.md) | | +| [NotificationsStart](./kibana-plugin-public.notificationsstart.md) | | | [OverlayRef](./kibana-plugin-public.overlayref.md) | | | [OverlayStart](./kibana-plugin-public.overlaystart.md) | | | [Plugin](./kibana-plugin-public.plugin.md) | The interface that should be returned by a PluginInitializer. | | [PluginInitializerContext](./kibana-plugin-public.plugininitializercontext.md) | The available core services passed to a PluginInitializer | -| [PluginSetupContext](./kibana-plugin-public.pluginsetupcontext.md) | The available core services passed to a plugin's Plugin#setup method. | -| [PluginStartContext](./kibana-plugin-public.pluginstartcontext.md) | The available core services passed to a plugin's Plugin#start method. | | [UiSettingsState](./kibana-plugin-public.uisettingsstate.md) | | ## Type Aliases | Type Alias | Description | | --- | --- | -| [BasePathStart](./kibana-plugin-public.basepathstart.md) | Provides access to the 'server.basePath' configuration option in kibana.yml | | [ChromeHelpExtension](./kibana-plugin-public.chromehelpextension.md) | | | [ChromeSetup](./kibana-plugin-public.chromesetup.md) | | | [ChromeStart](./kibana-plugin-public.chromestart.md) | | | [HttpSetup](./kibana-plugin-public.httpsetup.md) | | | [HttpStart](./kibana-plugin-public.httpstart.md) | | | [I18nStart](./kibana-plugin-public.i18nstart.md) | | -| [InjectedMetadataStart](./kibana-plugin-public.injectedmetadatastart.md) | | -| [NotificationsStart](./kibana-plugin-public.notificationsstart.md) | | | [PluginInitializer](./kibana-plugin-public.plugininitializer.md) | The plugin export at the root of a plugin's public directory should conform to this interface. | +| [RecursiveReadonly](./kibana-plugin-public.recursivereadonly.md) | | | [ToastInput](./kibana-plugin-public.toastinput.md) | | | [UiSettingsSetup](./kibana-plugin-public.uisettingssetup.md) | | diff --git a/docs/development/core/public/kibana-plugin-public.notificationssetup.md b/docs/development/core/public/kibana-plugin-public.notificationssetup.md index c40e4aa5ea4c0..7d9dd2e89f851 100644 --- a/docs/development/core/public/kibana-plugin-public.notificationssetup.md +++ b/docs/development/core/public/kibana-plugin-public.notificationssetup.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [NotificationsSetup](./kibana-plugin-public.notificationssetup.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [NotificationsSetup](./kibana-plugin-public.notificationssetup.md) ## NotificationsSetup interface @@ -15,5 +15,5 @@ export interface NotificationsSetup | Property | Type | Description | | --- | --- | --- | -| [toasts](./kibana-plugin-public.notificationssetup.toasts.md) | ToastsApi | | +| [toasts](./kibana-plugin-public.notificationssetup.toasts.md) | ToastsSetup | | diff --git a/docs/development/core/public/kibana-plugin-public.notificationssetup.toasts.md b/docs/development/core/public/kibana-plugin-public.notificationssetup.toasts.md index a4b754bb1aa87..c44ec9ca93e65 100644 --- a/docs/development/core/public/kibana-plugin-public.notificationssetup.toasts.md +++ b/docs/development/core/public/kibana-plugin-public.notificationssetup.toasts.md @@ -1,11 +1,11 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [NotificationsSetup](./kibana-plugin-public.notificationssetup.md) > [toasts](./kibana-plugin-public.notificationssetup.toasts.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [NotificationsSetup](./kibana-plugin-public.notificationssetup.md) > [toasts](./kibana-plugin-public.notificationssetup.toasts.md) ## NotificationsSetup.toasts property Signature: ```typescript -toasts: ToastsApi; +toasts: ToastsSetup; ``` diff --git a/docs/development/core/public/kibana-plugin-public.notificationsstart.md b/docs/development/core/public/kibana-plugin-public.notificationsstart.md index 0510dff50d5cf..acab2d6884418 100644 --- a/docs/development/core/public/kibana-plugin-public.notificationsstart.md +++ b/docs/development/core/public/kibana-plugin-public.notificationsstart.md @@ -1,12 +1,19 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [NotificationsStart](./kibana-plugin-public.notificationsstart.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [NotificationsStart](./kibana-plugin-public.notificationsstart.md) -## NotificationsStart type +## NotificationsStart interface Signature: ```typescript -export declare type NotificationsStart = NotificationsSetup; +export interface NotificationsStart ``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [toasts](./kibana-plugin-public.notificationsstart.toasts.md) | ToastsStart | | + diff --git a/docs/development/core/public/kibana-plugin-public.notificationsstart.toasts.md b/docs/development/core/public/kibana-plugin-public.notificationsstart.toasts.md new file mode 100644 index 0000000000000..db2be3dad8351 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.notificationsstart.toasts.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [NotificationsStart](./kibana-plugin-public.notificationsstart.md) > [toasts](./kibana-plugin-public.notificationsstart.toasts.md) + +## NotificationsStart.toasts property + +Signature: + +```typescript +toasts: ToastsStart; +``` diff --git a/docs/development/core/public/kibana-plugin-public.overlayref.close.md b/docs/development/core/public/kibana-plugin-public.overlayref.close.md index e6e4bf2f7035b..32f17882af304 100644 --- a/docs/development/core/public/kibana-plugin-public.overlayref.close.md +++ b/docs/development/core/public/kibana-plugin-public.overlayref.close.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [OverlayRef](./kibana-plugin-public.overlayref.md) > [close](./kibana-plugin-public.overlayref.close.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [OverlayRef](./kibana-plugin-public.overlayref.md) > [close](./kibana-plugin-public.overlayref.close.md) ## OverlayRef.close() method diff --git a/docs/development/core/public/kibana-plugin-public.overlayref.md b/docs/development/core/public/kibana-plugin-public.overlayref.md index 543a2c52b3619..ba573ccf10bbe 100644 --- a/docs/development/core/public/kibana-plugin-public.overlayref.md +++ b/docs/development/core/public/kibana-plugin-public.overlayref.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [OverlayRef](./kibana-plugin-public.overlayref.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [OverlayRef](./kibana-plugin-public.overlayref.md) ## OverlayRef interface diff --git a/docs/development/core/public/kibana-plugin-public.overlayref.onclose.md b/docs/development/core/public/kibana-plugin-public.overlayref.onclose.md index d8f0351df8622..641b48b2b1ca1 100644 --- a/docs/development/core/public/kibana-plugin-public.overlayref.onclose.md +++ b/docs/development/core/public/kibana-plugin-public.overlayref.onclose.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [OverlayRef](./kibana-plugin-public.overlayref.md) > [onClose](./kibana-plugin-public.overlayref.onclose.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [OverlayRef](./kibana-plugin-public.overlayref.md) > [onClose](./kibana-plugin-public.overlayref.onclose.md) ## OverlayRef.onClose property diff --git a/docs/development/core/public/kibana-plugin-public.overlaystart.md b/docs/development/core/public/kibana-plugin-public.overlaystart.md index 639a2b461d56a..c14ba89a7ffd0 100644 --- a/docs/development/core/public/kibana-plugin-public.overlaystart.md +++ b/docs/development/core/public/kibana-plugin-public.overlaystart.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [OverlayStart](./kibana-plugin-public.overlaystart.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [OverlayStart](./kibana-plugin-public.overlaystart.md) ## OverlayStart interface @@ -15,6 +15,6 @@ export interface OverlayStart | Property | Type | Description | | --- | --- | --- | -| [openFlyout](./kibana-plugin-public.overlaystart.openflyout.md) | (flyoutChildren: React.ReactNode, flyoutProps?: {`

` closeButtonAriaLabel?: string;`

` 'data-test-subj'?: string;`

` }) => OverlayRef | | -| [openModal](./kibana-plugin-public.overlaystart.openmodal.md) | (modalChildren: React.ReactNode, modalProps?: {`

` closeButtonAriaLabel?: string;`

` 'data-test-subj'?: string;`

` }) => OverlayRef | | +| [openFlyout](./kibana-plugin-public.overlaystart.openflyout.md) | (flyoutChildren: React.ReactNode, flyoutProps?: {
closeButtonAriaLabel?: string;
'data-test-subj'?: string;
}) => OverlayRef | | +| [openModal](./kibana-plugin-public.overlaystart.openmodal.md) | (modalChildren: React.ReactNode, modalProps?: {
closeButtonAriaLabel?: string;
'data-test-subj'?: string;
}) => OverlayRef | | diff --git a/docs/development/core/public/kibana-plugin-public.overlaystart.openflyout.md b/docs/development/core/public/kibana-plugin-public.overlaystart.openflyout.md index 33d6ddf1a58e5..6d015d6a34382 100644 --- a/docs/development/core/public/kibana-plugin-public.overlaystart.openflyout.md +++ b/docs/development/core/public/kibana-plugin-public.overlaystart.openflyout.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [OverlayStart](./kibana-plugin-public.overlaystart.md) > [openFlyout](./kibana-plugin-public.overlaystart.openflyout.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [OverlayStart](./kibana-plugin-public.overlaystart.md) > [openFlyout](./kibana-plugin-public.overlaystart.openflyout.md) ## OverlayStart.openFlyout property diff --git a/docs/development/core/public/kibana-plugin-public.overlaystart.openmodal.md b/docs/development/core/public/kibana-plugin-public.overlaystart.openmodal.md index 7d26a7ad6a181..0fc8ba164eaee 100644 --- a/docs/development/core/public/kibana-plugin-public.overlaystart.openmodal.md +++ b/docs/development/core/public/kibana-plugin-public.overlaystart.openmodal.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [OverlayStart](./kibana-plugin-public.overlaystart.md) > [openModal](./kibana-plugin-public.overlaystart.openmodal.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [OverlayStart](./kibana-plugin-public.overlaystart.md) > [openModal](./kibana-plugin-public.overlaystart.openmodal.md) ## OverlayStart.openModal property diff --git a/docs/development/core/public/kibana-plugin-public.plugin.md b/docs/development/core/public/kibana-plugin-public.plugin.md index 0e033a63435a9..77e7fa65f3f85 100644 --- a/docs/development/core/public/kibana-plugin-public.plugin.md +++ b/docs/development/core/public/kibana-plugin-public.plugin.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [Plugin](./kibana-plugin-public.plugin.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [Plugin](./kibana-plugin-public.plugin.md) ## Plugin interface @@ -16,7 +16,7 @@ export interface Plugin(core: PluginSetupContext, plugins: TPluginsSetup) => TSetup | Promise<TSetup> | | -| [start](./kibana-plugin-public.plugin.start.md) | (core: PluginStartContext, plugins: TPluginsStart) => TStart | Promise<TStart> | | +| [setup](./kibana-plugin-public.plugin.setup.md) | (core: CoreSetup, plugins: TPluginsSetup) => TSetup | Promise<TSetup> | | +| [start](./kibana-plugin-public.plugin.start.md) | (core: CoreStart, plugins: TPluginsStart) => TStart | Promise<TStart> | | | [stop](./kibana-plugin-public.plugin.stop.md) | () => void | | diff --git a/docs/development/core/public/kibana-plugin-public.plugin.setup.md b/docs/development/core/public/kibana-plugin-public.plugin.setup.md index 690c36e872947..01223e3f9f24d 100644 --- a/docs/development/core/public/kibana-plugin-public.plugin.setup.md +++ b/docs/development/core/public/kibana-plugin-public.plugin.setup.md @@ -1,11 +1,11 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [Plugin](./kibana-plugin-public.plugin.md) > [setup](./kibana-plugin-public.plugin.setup.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [Plugin](./kibana-plugin-public.plugin.md) > [setup](./kibana-plugin-public.plugin.setup.md) ## Plugin.setup property Signature: ```typescript -setup: (core: PluginSetupContext, plugins: TPluginsSetup) => TSetup | Promise; +setup: (core: CoreSetup, plugins: TPluginsSetup) => TSetup | Promise; ``` diff --git a/docs/development/core/public/kibana-plugin-public.plugin.start.md b/docs/development/core/public/kibana-plugin-public.plugin.start.md index 2b3db2e3dcf47..7e381b4379daa 100644 --- a/docs/development/core/public/kibana-plugin-public.plugin.start.md +++ b/docs/development/core/public/kibana-plugin-public.plugin.start.md @@ -1,11 +1,11 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [Plugin](./kibana-plugin-public.plugin.md) > [start](./kibana-plugin-public.plugin.start.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [Plugin](./kibana-plugin-public.plugin.md) > [start](./kibana-plugin-public.plugin.start.md) ## Plugin.start property Signature: ```typescript -start: (core: PluginStartContext, plugins: TPluginsStart) => TStart | Promise; +start: (core: CoreStart, plugins: TPluginsStart) => TStart | Promise; ``` diff --git a/docs/development/core/public/kibana-plugin-public.plugin.stop.md b/docs/development/core/public/kibana-plugin-public.plugin.stop.md index 7b24a9a9d9b7e..1af971d80c1ba 100644 --- a/docs/development/core/public/kibana-plugin-public.plugin.stop.md +++ b/docs/development/core/public/kibana-plugin-public.plugin.stop.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [Plugin](./kibana-plugin-public.plugin.md) > [stop](./kibana-plugin-public.plugin.stop.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [Plugin](./kibana-plugin-public.plugin.md) > [stop](./kibana-plugin-public.plugin.stop.md) ## Plugin.stop property diff --git a/docs/development/core/public/kibana-plugin-public.plugininitializer.md b/docs/development/core/public/kibana-plugin-public.plugininitializer.md index 2239925980b65..adb389e0fda3a 100644 --- a/docs/development/core/public/kibana-plugin-public.plugininitializer.md +++ b/docs/development/core/public/kibana-plugin-public.plugininitializer.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [PluginInitializer](./kibana-plugin-public.plugininitializer.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [PluginInitializer](./kibana-plugin-public.plugininitializer.md) ## PluginInitializer type diff --git a/docs/development/core/public/kibana-plugin-public.plugininitializercontext.md b/docs/development/core/public/kibana-plugin-public.plugininitializercontext.md index a3c210fe718aa..5dbe464d15618 100644 --- a/docs/development/core/public/kibana-plugin-public.plugininitializercontext.md +++ b/docs/development/core/public/kibana-plugin-public.plugininitializercontext.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [PluginInitializerContext](./kibana-plugin-public.plugininitializercontext.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [PluginInitializerContext](./kibana-plugin-public.plugininitializercontext.md) ## PluginInitializerContext interface diff --git a/docs/development/core/public/kibana-plugin-public.pluginsetupcontext.basepath.md b/docs/development/core/public/kibana-plugin-public.pluginsetupcontext.basepath.md deleted file mode 100644 index b60a447cf3bf4..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.pluginsetupcontext.basepath.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [PluginSetupContext](./kibana-plugin-public.pluginsetupcontext.md) > [basePath](./kibana-plugin-public.pluginsetupcontext.basepath.md) - -## PluginSetupContext.basePath property - -Signature: - -```typescript -basePath: BasePathSetup; -``` diff --git a/docs/development/core/public/kibana-plugin-public.pluginsetupcontext.chrome.md b/docs/development/core/public/kibana-plugin-public.pluginsetupcontext.chrome.md deleted file mode 100644 index 667b4d01554e7..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.pluginsetupcontext.chrome.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [PluginSetupContext](./kibana-plugin-public.pluginsetupcontext.md) > [chrome](./kibana-plugin-public.pluginsetupcontext.chrome.md) - -## PluginSetupContext.chrome property - -Signature: - -```typescript -chrome: ChromeSetup; -``` diff --git a/docs/development/core/public/kibana-plugin-public.pluginsetupcontext.fatalerrors.md b/docs/development/core/public/kibana-plugin-public.pluginsetupcontext.fatalerrors.md deleted file mode 100644 index 4ba8ce4a88178..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.pluginsetupcontext.fatalerrors.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [PluginSetupContext](./kibana-plugin-public.pluginsetupcontext.md) > [fatalErrors](./kibana-plugin-public.pluginsetupcontext.fatalerrors.md) - -## PluginSetupContext.fatalErrors property - -Signature: - -```typescript -fatalErrors: FatalErrorsSetup; -``` diff --git a/docs/development/core/public/kibana-plugin-public.pluginsetupcontext.http.md b/docs/development/core/public/kibana-plugin-public.pluginsetupcontext.http.md deleted file mode 100644 index b273a7740f5f8..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.pluginsetupcontext.http.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [PluginSetupContext](./kibana-plugin-public.pluginsetupcontext.md) > [http](./kibana-plugin-public.pluginsetupcontext.http.md) - -## PluginSetupContext.http property - -Signature: - -```typescript -http: HttpSetup; -``` diff --git a/docs/development/core/public/kibana-plugin-public.pluginsetupcontext.i18n.md b/docs/development/core/public/kibana-plugin-public.pluginsetupcontext.i18n.md deleted file mode 100644 index 04a296aa51c13..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.pluginsetupcontext.i18n.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [PluginSetupContext](./kibana-plugin-public.pluginsetupcontext.md) > [i18n](./kibana-plugin-public.pluginsetupcontext.i18n.md) - -## PluginSetupContext.i18n property - -Signature: - -```typescript -i18n: I18nSetup; -``` diff --git a/docs/development/core/public/kibana-plugin-public.pluginsetupcontext.md b/docs/development/core/public/kibana-plugin-public.pluginsetupcontext.md deleted file mode 100644 index bff0febbeedca..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.pluginsetupcontext.md +++ /dev/null @@ -1,26 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [PluginSetupContext](./kibana-plugin-public.pluginsetupcontext.md) - -## PluginSetupContext interface - -The available core services passed to a plugin's `Plugin#setup` method. - -Signature: - -```typescript -export interface PluginSetupContext -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [basePath](./kibana-plugin-public.pluginsetupcontext.basepath.md) | BasePathSetup | | -| [chrome](./kibana-plugin-public.pluginsetupcontext.chrome.md) | ChromeSetup | | -| [fatalErrors](./kibana-plugin-public.pluginsetupcontext.fatalerrors.md) | FatalErrorsSetup | | -| [http](./kibana-plugin-public.pluginsetupcontext.http.md) | HttpSetup | | -| [i18n](./kibana-plugin-public.pluginsetupcontext.i18n.md) | I18nSetup | | -| [notifications](./kibana-plugin-public.pluginsetupcontext.notifications.md) | NotificationsSetup | | -| [uiSettings](./kibana-plugin-public.pluginsetupcontext.uisettings.md) | UiSettingsSetup | | - diff --git a/docs/development/core/public/kibana-plugin-public.pluginsetupcontext.notifications.md b/docs/development/core/public/kibana-plugin-public.pluginsetupcontext.notifications.md deleted file mode 100644 index 2c2e1a93bc3c9..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.pluginsetupcontext.notifications.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [PluginSetupContext](./kibana-plugin-public.pluginsetupcontext.md) > [notifications](./kibana-plugin-public.pluginsetupcontext.notifications.md) - -## PluginSetupContext.notifications property - -Signature: - -```typescript -notifications: NotificationsSetup; -``` diff --git a/docs/development/core/public/kibana-plugin-public.pluginsetupcontext.uisettings.md b/docs/development/core/public/kibana-plugin-public.pluginsetupcontext.uisettings.md deleted file mode 100644 index cd38fd512b464..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.pluginsetupcontext.uisettings.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [PluginSetupContext](./kibana-plugin-public.pluginsetupcontext.md) > [uiSettings](./kibana-plugin-public.pluginsetupcontext.uisettings.md) - -## PluginSetupContext.uiSettings property - -Signature: - -```typescript -uiSettings: UiSettingsSetup; -``` diff --git a/docs/development/core/public/kibana-plugin-public.pluginstartcontext.application.md b/docs/development/core/public/kibana-plugin-public.pluginstartcontext.application.md deleted file mode 100644 index 9c718893005db..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.pluginstartcontext.application.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [PluginStartContext](./kibana-plugin-public.pluginstartcontext.md) > [application](./kibana-plugin-public.pluginstartcontext.application.md) - -## PluginStartContext.application property - -Signature: - -```typescript -application: Pick; -``` diff --git a/docs/development/core/public/kibana-plugin-public.pluginstartcontext.basepath.md b/docs/development/core/public/kibana-plugin-public.pluginstartcontext.basepath.md deleted file mode 100644 index a4c44fedca5fe..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.pluginstartcontext.basepath.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [PluginStartContext](./kibana-plugin-public.pluginstartcontext.md) > [basePath](./kibana-plugin-public.pluginstartcontext.basepath.md) - -## PluginStartContext.basePath property - -Signature: - -```typescript -basePath: BasePathStart; -``` diff --git a/docs/development/core/public/kibana-plugin-public.pluginstartcontext.chrome.md b/docs/development/core/public/kibana-plugin-public.pluginstartcontext.chrome.md deleted file mode 100644 index fc458dcef622d..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.pluginstartcontext.chrome.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [PluginStartContext](./kibana-plugin-public.pluginstartcontext.md) > [chrome](./kibana-plugin-public.pluginstartcontext.chrome.md) - -## PluginStartContext.chrome property - -Signature: - -```typescript -chrome: ChromeStart; -``` diff --git a/docs/development/core/public/kibana-plugin-public.pluginstartcontext.http.md b/docs/development/core/public/kibana-plugin-public.pluginstartcontext.http.md deleted file mode 100644 index a70a100cc7e4c..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.pluginstartcontext.http.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [PluginStartContext](./kibana-plugin-public.pluginstartcontext.md) > [http](./kibana-plugin-public.pluginstartcontext.http.md) - -## PluginStartContext.http property - -Signature: - -```typescript -http: HttpStart; -``` diff --git a/docs/development/core/public/kibana-plugin-public.pluginstartcontext.i18n.md b/docs/development/core/public/kibana-plugin-public.pluginstartcontext.i18n.md deleted file mode 100644 index 9e1896aa9fc88..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.pluginstartcontext.i18n.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [PluginStartContext](./kibana-plugin-public.pluginstartcontext.md) > [i18n](./kibana-plugin-public.pluginstartcontext.i18n.md) - -## PluginStartContext.i18n property - -Signature: - -```typescript -i18n: I18nStart; -``` diff --git a/docs/development/core/public/kibana-plugin-public.pluginstartcontext.md b/docs/development/core/public/kibana-plugin-public.pluginstartcontext.md deleted file mode 100644 index 05dd4024cc05c..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.pluginstartcontext.md +++ /dev/null @@ -1,26 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [PluginStartContext](./kibana-plugin-public.pluginstartcontext.md) - -## PluginStartContext interface - -The available core services passed to a plugin's `Plugin#start` method. - -Signature: - -```typescript -export interface PluginStartContext -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [application](./kibana-plugin-public.pluginstartcontext.application.md) | Pick<ApplicationStart, 'capabilities'> | | -| [basePath](./kibana-plugin-public.pluginstartcontext.basepath.md) | BasePathStart | | -| [chrome](./kibana-plugin-public.pluginstartcontext.chrome.md) | ChromeStart | | -| [http](./kibana-plugin-public.pluginstartcontext.http.md) | HttpStart | | -| [i18n](./kibana-plugin-public.pluginstartcontext.i18n.md) | I18nStart | | -| [notifications](./kibana-plugin-public.pluginstartcontext.notifications.md) | NotificationsStart | | -| [overlays](./kibana-plugin-public.pluginstartcontext.overlays.md) | OverlayStart | | - diff --git a/docs/development/core/public/kibana-plugin-public.pluginstartcontext.notifications.md b/docs/development/core/public/kibana-plugin-public.pluginstartcontext.notifications.md deleted file mode 100644 index e9972f5e7ff4c..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.pluginstartcontext.notifications.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [PluginStartContext](./kibana-plugin-public.pluginstartcontext.md) > [notifications](./kibana-plugin-public.pluginstartcontext.notifications.md) - -## PluginStartContext.notifications property - -Signature: - -```typescript -notifications: NotificationsStart; -``` diff --git a/docs/development/core/public/kibana-plugin-public.pluginstartcontext.overlays.md b/docs/development/core/public/kibana-plugin-public.pluginstartcontext.overlays.md deleted file mode 100644 index c299389d3c922..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.pluginstartcontext.overlays.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [PluginStartContext](./kibana-plugin-public.pluginstartcontext.md) > [overlays](./kibana-plugin-public.pluginstartcontext.overlays.md) - -## PluginStartContext.overlays property - -Signature: - -```typescript -overlays: OverlayStart; -``` diff --git a/docs/development/core/public/kibana-plugin-public.recursivereadonly.md b/docs/development/core/public/kibana-plugin-public.recursivereadonly.md new file mode 100644 index 0000000000000..fe048494063a0 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.recursivereadonly.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [RecursiveReadonly](./kibana-plugin-public.recursivereadonly.md) + +## RecursiveReadonly type + + +Signature: + +```typescript +export declare type RecursiveReadonly = T extends (...args: any[]) => any ? T : T extends any[] ? RecursiveReadonlyArray : T extends object ? Readonly<{ + [K in keyof T]: RecursiveReadonly; +}> : T; +``` diff --git a/docs/development/core/public/kibana-plugin-public.toastinput.md b/docs/development/core/public/kibana-plugin-public.toastinput.md index 9b40bf1161db5..8e798697fa6c0 100644 --- a/docs/development/core/public/kibana-plugin-public.toastinput.md +++ b/docs/development/core/public/kibana-plugin-public.toastinput.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ToastInput](./kibana-plugin-public.toastinput.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ToastInput](./kibana-plugin-public.toastinput.md) ## ToastInput type @@ -8,5 +8,5 @@ Signature: ```typescript -export declare type ToastInput = string | Pick>; +export declare type ToastInput = string | ToastInputFields | Promise; ``` diff --git a/docs/development/core/public/kibana-plugin-public.toastsapi.(constructor).md b/docs/development/core/public/kibana-plugin-public.toastsapi.(constructor).md new file mode 100644 index 0000000000000..5bfdac19aa45a --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.toastsapi.(constructor).md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ToastsApi](./kibana-plugin-public.toastsapi.md) > [(constructor)](./kibana-plugin-public.toastsapi.(constructor).md) + +## ToastsApi.(constructor) + +Constructs a new instance of the `ToastsApi` class + +Signature: + +```typescript +constructor(deps: { + uiSettings: UiSettingsSetup; + i18n: I18nSetup; + }); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| deps | {
uiSettings: UiSettingsSetup;
i18n: I18nSetup;
} | | + diff --git a/docs/development/core/public/kibana-plugin-public.toastsapi.add.md b/docs/development/core/public/kibana-plugin-public.toastsapi.add.md index 1cec95b06efb7..a59c11d0d5f53 100644 --- a/docs/development/core/public/kibana-plugin-public.toastsapi.add.md +++ b/docs/development/core/public/kibana-plugin-public.toastsapi.add.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ToastsApi](./kibana-plugin-public.toastsapi.md) > [add](./kibana-plugin-public.toastsapi.add.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ToastsApi](./kibana-plugin-public.toastsapi.md) > [add](./kibana-plugin-public.toastsapi.add.md) ## ToastsApi.add() method diff --git a/docs/development/core/public/kibana-plugin-public.toastsapi.adddanger.md b/docs/development/core/public/kibana-plugin-public.toastsapi.adddanger.md index 3a75a55359414..3f9e6b1c389da 100644 --- a/docs/development/core/public/kibana-plugin-public.toastsapi.adddanger.md +++ b/docs/development/core/public/kibana-plugin-public.toastsapi.adddanger.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ToastsApi](./kibana-plugin-public.toastsapi.md) > [addDanger](./kibana-plugin-public.toastsapi.adddanger.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ToastsApi](./kibana-plugin-public.toastsapi.md) > [addDanger](./kibana-plugin-public.toastsapi.adddanger.md) ## ToastsApi.addDanger() method diff --git a/docs/development/core/public/kibana-plugin-public.toastsapi.adderror.md b/docs/development/core/public/kibana-plugin-public.toastsapi.adderror.md new file mode 100644 index 0000000000000..10aaf50ebab4d --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.toastsapi.adderror.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ToastsApi](./kibana-plugin-public.toastsapi.md) > [addError](./kibana-plugin-public.toastsapi.adderror.md) + +## ToastsApi.addError() method + +Signature: + +```typescript +addError(error: Error, options: ErrorToastOptions): Toast; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | Error | | +| options | ErrorToastOptions | | + +Returns: + +`Toast` + diff --git a/docs/development/core/public/kibana-plugin-public.toastsapi.addsuccess.md b/docs/development/core/public/kibana-plugin-public.toastsapi.addsuccess.md index 30fc95455f2df..5aff3229b3c81 100644 --- a/docs/development/core/public/kibana-plugin-public.toastsapi.addsuccess.md +++ b/docs/development/core/public/kibana-plugin-public.toastsapi.addsuccess.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ToastsApi](./kibana-plugin-public.toastsapi.md) > [addSuccess](./kibana-plugin-public.toastsapi.addsuccess.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ToastsApi](./kibana-plugin-public.toastsapi.md) > [addSuccess](./kibana-plugin-public.toastsapi.addsuccess.md) ## ToastsApi.addSuccess() method diff --git a/docs/development/core/public/kibana-plugin-public.toastsapi.addwarning.md b/docs/development/core/public/kibana-plugin-public.toastsapi.addwarning.md index e342ce1b3e294..3b68f0b7fe22a 100644 --- a/docs/development/core/public/kibana-plugin-public.toastsapi.addwarning.md +++ b/docs/development/core/public/kibana-plugin-public.toastsapi.addwarning.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ToastsApi](./kibana-plugin-public.toastsapi.md) > [addWarning](./kibana-plugin-public.toastsapi.addwarning.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ToastsApi](./kibana-plugin-public.toastsapi.md) > [addWarning](./kibana-plugin-public.toastsapi.addwarning.md) ## ToastsApi.addWarning() method diff --git a/docs/development/core/public/kibana-plugin-public.toastsapi.get$.md b/docs/development/core/public/kibana-plugin-public.toastsapi.get$.md index 733b64cd8c4b7..a9273a16329c7 100644 --- a/docs/development/core/public/kibana-plugin-public.toastsapi.get$.md +++ b/docs/development/core/public/kibana-plugin-public.toastsapi.get$.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ToastsApi](./kibana-plugin-public.toastsapi.md) > [get$](./kibana-plugin-public.toastsapi.get$.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ToastsApi](./kibana-plugin-public.toastsapi.md) > [get$](./kibana-plugin-public.toastsapi.get$.md) ## ToastsApi.get$() method diff --git a/docs/development/core/public/kibana-plugin-public.toastsapi.md b/docs/development/core/public/kibana-plugin-public.toastsapi.md index 6ada348a573b7..9fab540281117 100644 --- a/docs/development/core/public/kibana-plugin-public.toastsapi.md +++ b/docs/development/core/public/kibana-plugin-public.toastsapi.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ToastsApi](./kibana-plugin-public.toastsapi.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ToastsApi](./kibana-plugin-public.toastsapi.md) ## ToastsApi class @@ -11,14 +11,22 @@ export declare class ToastsApi ``` +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(deps)](./kibana-plugin-public.toastsapi.(constructor).md) | | Constructs a new instance of the ToastsApi class | + ## Methods | Method | Modifiers | Description | | --- | --- | --- | | [add(toastOrTitle)](./kibana-plugin-public.toastsapi.add.md) | | | | [addDanger(toastOrTitle)](./kibana-plugin-public.toastsapi.adddanger.md) | | | +| [addError(error, options)](./kibana-plugin-public.toastsapi.adderror.md) | | | | [addSuccess(toastOrTitle)](./kibana-plugin-public.toastsapi.addsuccess.md) | | | | [addWarning(toastOrTitle)](./kibana-plugin-public.toastsapi.addwarning.md) | | | | [get$()](./kibana-plugin-public.toastsapi.get$.md) | | | +| [registerOverlays(overlays)](./kibana-plugin-public.toastsapi.registeroverlays.md) | | | | [remove(toast)](./kibana-plugin-public.toastsapi.remove.md) | | | diff --git a/docs/development/core/public/kibana-plugin-public.toastsapi.registeroverlays.md b/docs/development/core/public/kibana-plugin-public.toastsapi.registeroverlays.md new file mode 100644 index 0000000000000..31601e9b215bc --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.toastsapi.registeroverlays.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ToastsApi](./kibana-plugin-public.toastsapi.md) > [registerOverlays](./kibana-plugin-public.toastsapi.registeroverlays.md) + +## ToastsApi.registerOverlays() method + +Signature: + +```typescript +registerOverlays(overlays: OverlayStart): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| overlays | OverlayStart | | + +Returns: + +`void` + diff --git a/docs/development/core/public/kibana-plugin-public.toastsapi.remove.md b/docs/development/core/public/kibana-plugin-public.toastsapi.remove.md index 8d3341bac2576..822677a42b630 100644 --- a/docs/development/core/public/kibana-plugin-public.toastsapi.remove.md +++ b/docs/development/core/public/kibana-plugin-public.toastsapi.remove.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ToastsApi](./kibana-plugin-public.toastsapi.md) > [remove](./kibana-plugin-public.toastsapi.remove.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ToastsApi](./kibana-plugin-public.toastsapi.md) > [remove](./kibana-plugin-public.toastsapi.remove.md) ## ToastsApi.remove() method diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient.(constructor).md b/docs/development/core/public/kibana-plugin-public.uisettingsclient.(constructor).md new file mode 100644 index 0000000000000..fd89f5fda6989 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.uisettingsclient.(constructor).md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [(constructor)](./kibana-plugin-public.uisettingsclient.(constructor).md) + +## UiSettingsClient.(constructor) + +Constructs a new instance of the `UiSettingsClient` class + +Signature: + +```typescript +constructor(params: UiSettingsClientParams); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| params | UiSettingsClientParams | | + diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient.get$.md b/docs/development/core/public/kibana-plugin-public.uisettingsclient.get$.md index 1dcece3569d90..4841e52c8e668 100644 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclient.get$.md +++ b/docs/development/core/public/kibana-plugin-public.uisettingsclient.get$.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [get$](./kibana-plugin-public.uisettingsclient.get$.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [get$](./kibana-plugin-public.uisettingsclient.get$.md) ## UiSettingsClient.get$() method diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient.get.md b/docs/development/core/public/kibana-plugin-public.uisettingsclient.get.md index 531c0c8a3270e..03fa38575b6b8 100644 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclient.get.md +++ b/docs/development/core/public/kibana-plugin-public.uisettingsclient.get.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [get](./kibana-plugin-public.uisettingsclient.get.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [get](./kibana-plugin-public.uisettingsclient.get.md) ## UiSettingsClient.get() method diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient.getall.md b/docs/development/core/public/kibana-plugin-public.uisettingsclient.getall.md index 7b6a3c7edd641..5eaf06e7dd682 100644 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclient.getall.md +++ b/docs/development/core/public/kibana-plugin-public.uisettingsclient.getall.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [getAll](./kibana-plugin-public.uisettingsclient.getall.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [getAll](./kibana-plugin-public.uisettingsclient.getall.md) ## UiSettingsClient.getAll() method diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient.getsaved$.md b/docs/development/core/public/kibana-plugin-public.uisettingsclient.getsaved$.md index 90b2ebcf29383..da5062bc337c1 100644 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclient.getsaved$.md +++ b/docs/development/core/public/kibana-plugin-public.uisettingsclient.getsaved$.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [getSaved$](./kibana-plugin-public.uisettingsclient.getsaved$.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [getSaved$](./kibana-plugin-public.uisettingsclient.getsaved$.md) ## UiSettingsClient.getSaved$() method diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient.getupdate$.md b/docs/development/core/public/kibana-plugin-public.uisettingsclient.getupdate$.md index 9d92a20d7b63c..16dcfc7ba1f37 100644 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclient.getupdate$.md +++ b/docs/development/core/public/kibana-plugin-public.uisettingsclient.getupdate$.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [getUpdate$](./kibana-plugin-public.uisettingsclient.getupdate$.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [getUpdate$](./kibana-plugin-public.uisettingsclient.getupdate$.md) ## UiSettingsClient.getUpdate$() method diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient.getupdateerrors$.md b/docs/development/core/public/kibana-plugin-public.uisettingsclient.getupdateerrors$.md index acabf0f930b70..1251bad87d5d7 100644 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclient.getupdateerrors$.md +++ b/docs/development/core/public/kibana-plugin-public.uisettingsclient.getupdateerrors$.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [getUpdateErrors$](./kibana-plugin-public.uisettingsclient.getupdateerrors$.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [getUpdateErrors$](./kibana-plugin-public.uisettingsclient.getupdateerrors$.md) ## UiSettingsClient.getUpdateErrors$() method diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient.iscustom.md b/docs/development/core/public/kibana-plugin-public.uisettingsclient.iscustom.md index c5d4057f5fa71..8855e39d7e8f3 100644 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclient.iscustom.md +++ b/docs/development/core/public/kibana-plugin-public.uisettingsclient.iscustom.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [isCustom](./kibana-plugin-public.uisettingsclient.iscustom.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [isCustom](./kibana-plugin-public.uisettingsclient.iscustom.md) ## UiSettingsClient.isCustom() method diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient.isdeclared.md b/docs/development/core/public/kibana-plugin-public.uisettingsclient.isdeclared.md index cc7cd6d36a4ca..61b9d3a11a1af 100644 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclient.isdeclared.md +++ b/docs/development/core/public/kibana-plugin-public.uisettingsclient.isdeclared.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [isDeclared](./kibana-plugin-public.uisettingsclient.isdeclared.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [isDeclared](./kibana-plugin-public.uisettingsclient.isdeclared.md) ## UiSettingsClient.isDeclared() method diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient.isdefault.md b/docs/development/core/public/kibana-plugin-public.uisettingsclient.isdefault.md index c25ae1beb2843..09a04f99e8285 100644 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclient.isdefault.md +++ b/docs/development/core/public/kibana-plugin-public.uisettingsclient.isdefault.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [isDefault](./kibana-plugin-public.uisettingsclient.isdefault.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [isDefault](./kibana-plugin-public.uisettingsclient.isdefault.md) ## UiSettingsClient.isDefault() method diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient.isoverridden.md b/docs/development/core/public/kibana-plugin-public.uisettingsclient.isoverridden.md index 3871a3f7919bb..5311ffbf40d95 100644 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclient.isoverridden.md +++ b/docs/development/core/public/kibana-plugin-public.uisettingsclient.isoverridden.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [isOverridden](./kibana-plugin-public.uisettingsclient.isoverridden.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [isOverridden](./kibana-plugin-public.uisettingsclient.isoverridden.md) ## UiSettingsClient.isOverridden() method diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient.md b/docs/development/core/public/kibana-plugin-public.uisettingsclient.md index 985b42cd0063b..48ebb065b4865 100644 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclient.md +++ b/docs/development/core/public/kibana-plugin-public.uisettingsclient.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) ## UiSettingsClient class @@ -11,6 +11,12 @@ export declare class UiSettingsClient ``` +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(params)](./kibana-plugin-public.uisettingsclient.(constructor).md) | | Constructs a new instance of the UiSettingsClient class | + ## Methods | Method | Modifiers | Description | diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient.overridelocaldefault.md b/docs/development/core/public/kibana-plugin-public.uisettingsclient.overridelocaldefault.md index 09076f8e72e87..b94fe72fff102 100644 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclient.overridelocaldefault.md +++ b/docs/development/core/public/kibana-plugin-public.uisettingsclient.overridelocaldefault.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [overrideLocalDefault](./kibana-plugin-public.uisettingsclient.overridelocaldefault.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [overrideLocalDefault](./kibana-plugin-public.uisettingsclient.overridelocaldefault.md) ## UiSettingsClient.overrideLocalDefault() method diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient.remove.md b/docs/development/core/public/kibana-plugin-public.uisettingsclient.remove.md index e9387ebd79878..3d07e75449639 100644 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclient.remove.md +++ b/docs/development/core/public/kibana-plugin-public.uisettingsclient.remove.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [remove](./kibana-plugin-public.uisettingsclient.remove.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [remove](./kibana-plugin-public.uisettingsclient.remove.md) ## UiSettingsClient.remove() method diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient.set.md b/docs/development/core/public/kibana-plugin-public.uisettingsclient.set.md index c80d84a1970cc..ad1d97b8fe9b3 100644 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclient.set.md +++ b/docs/development/core/public/kibana-plugin-public.uisettingsclient.set.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [set](./kibana-plugin-public.uisettingsclient.set.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [set](./kibana-plugin-public.uisettingsclient.set.md) ## UiSettingsClient.set() method diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient.stop.md b/docs/development/core/public/kibana-plugin-public.uisettingsclient.stop.md index 123458a58d396..215a94544d2d0 100644 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclient.stop.md +++ b/docs/development/core/public/kibana-plugin-public.uisettingsclient.stop.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [stop](./kibana-plugin-public.uisettingsclient.stop.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [stop](./kibana-plugin-public.uisettingsclient.stop.md) ## UiSettingsClient.stop() method diff --git a/docs/development/core/public/kibana-plugin-public.uisettingssetup.md b/docs/development/core/public/kibana-plugin-public.uisettingssetup.md index 01ee95569ee36..ea8e9449fe596 100644 --- a/docs/development/core/public/kibana-plugin-public.uisettingssetup.md +++ b/docs/development/core/public/kibana-plugin-public.uisettingssetup.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsSetup](./kibana-plugin-public.uisettingssetup.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsSetup](./kibana-plugin-public.uisettingssetup.md) ## UiSettingsSetup type diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsstate.md b/docs/development/core/public/kibana-plugin-public.uisettingsstate.md index a7da052ed349c..4754898f05cb0 100644 --- a/docs/development/core/public/kibana-plugin-public.uisettingsstate.md +++ b/docs/development/core/public/kibana-plugin-public.uisettingsstate.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsState](./kibana-plugin-public.uisettingsstate.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsState](./kibana-plugin-public.uisettingsstate.md) ## UiSettingsState interface diff --git a/docs/development/core/server/kibana-plugin-server.apicaller.md b/docs/development/core/server/kibana-plugin-server.apicaller.md index a5bc9101fdf88..c4696551f0a49 100644 --- a/docs/development/core/server/kibana-plugin-server.apicaller.md +++ b/docs/development/core/server/kibana-plugin-server.apicaller.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [APICaller](./kibana-plugin-server.apicaller.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [APICaller](./kibana-plugin-server.apicaller.md) ## APICaller type diff --git a/docs/development/core/server/kibana-plugin-server.authenticationhandler.md b/docs/development/core/server/kibana-plugin-server.authenticationhandler.md index cb09dc0cd6051..88d199fc1b536 100644 --- a/docs/development/core/server/kibana-plugin-server.authenticationhandler.md +++ b/docs/development/core/server/kibana-plugin-server.authenticationhandler.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthenticationHandler](./kibana-plugin-server.authenticationhandler.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthenticationHandler](./kibana-plugin-server.authenticationhandler.md) ## AuthenticationHandler type @@ -8,5 +8,5 @@ Signature: ```typescript -export declare type AuthenticationHandler = (request: Request, sessionStorage: SessionStorage, t: AuthToolkit) => Promise; +export declare type AuthenticationHandler = (request: Readonly, t: AuthToolkit) => AuthResult | Promise; ``` diff --git a/docs/development/core/server/kibana-plugin-server.authtoolkit.authenticated.md b/docs/development/core/server/kibana-plugin-server.authtoolkit.authenticated.md index d0f1e07c47484..e28950653b60d 100644 --- a/docs/development/core/server/kibana-plugin-server.authtoolkit.authenticated.md +++ b/docs/development/core/server/kibana-plugin-server.authtoolkit.authenticated.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthToolkit](./kibana-plugin-server.authtoolkit.md) > [authenticated](./kibana-plugin-server.authtoolkit.authenticated.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthToolkit](./kibana-plugin-server.authtoolkit.md) > [authenticated](./kibana-plugin-server.authtoolkit.authenticated.md) ## AuthToolkit.authenticated property @@ -9,5 +9,5 @@ Authentication is successful with given credentials, allow request to pass throu Signature: ```typescript -authenticated: (credentials: any) => AuthResult; +authenticated: (state?: object) => AuthResult; ``` diff --git a/docs/development/core/server/kibana-plugin-server.authtoolkit.md b/docs/development/core/server/kibana-plugin-server.authtoolkit.md index d7e2e39b44d4c..f32f7076f0119 100644 --- a/docs/development/core/server/kibana-plugin-server.authtoolkit.md +++ b/docs/development/core/server/kibana-plugin-server.authtoolkit.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthToolkit](./kibana-plugin-server.authtoolkit.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthToolkit](./kibana-plugin-server.authtoolkit.md) ## AuthToolkit interface @@ -16,7 +16,7 @@ export interface AuthToolkit | Property | Type | Description | | --- | --- | --- | -| [authenticated](./kibana-plugin-server.authtoolkit.authenticated.md) | (credentials: any) => AuthResult | Authentication is successful with given credentials, allow request to pass through | +| [authenticated](./kibana-plugin-server.authtoolkit.authenticated.md) | (state?: object) => AuthResult | Authentication is successful with given credentials, allow request to pass through | | [redirected](./kibana-plugin-server.authtoolkit.redirected.md) | (url: string) => AuthResult | Authentication requires to interrupt request handling and redirect to a configured url | -| [rejected](./kibana-plugin-server.authtoolkit.rejected.md) | (error: Error, options?: {`

` statusCode?: number;`

` }) => AuthResult | Authentication is unsuccessful, fail the request with specified error. | +| [rejected](./kibana-plugin-server.authtoolkit.rejected.md) | (error: Error, options?: {
statusCode?: number;
}) => AuthResult | Authentication is unsuccessful, fail the request with specified error. | diff --git a/docs/development/core/server/kibana-plugin-server.authtoolkit.redirected.md b/docs/development/core/server/kibana-plugin-server.authtoolkit.redirected.md index 3eb331e6c9302..eb07b1c4b0f64 100644 --- a/docs/development/core/server/kibana-plugin-server.authtoolkit.redirected.md +++ b/docs/development/core/server/kibana-plugin-server.authtoolkit.redirected.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthToolkit](./kibana-plugin-server.authtoolkit.md) > [redirected](./kibana-plugin-server.authtoolkit.redirected.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthToolkit](./kibana-plugin-server.authtoolkit.md) > [redirected](./kibana-plugin-server.authtoolkit.redirected.md) ## AuthToolkit.redirected property diff --git a/docs/development/core/server/kibana-plugin-server.authtoolkit.rejected.md b/docs/development/core/server/kibana-plugin-server.authtoolkit.rejected.md index 949cd5dd2adc2..bc353c7df9fba 100644 --- a/docs/development/core/server/kibana-plugin-server.authtoolkit.rejected.md +++ b/docs/development/core/server/kibana-plugin-server.authtoolkit.rejected.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthToolkit](./kibana-plugin-server.authtoolkit.md) > [rejected](./kibana-plugin-server.authtoolkit.rejected.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthToolkit](./kibana-plugin-server.authtoolkit.md) > [rejected](./kibana-plugin-server.authtoolkit.rejected.md) ## AuthToolkit.rejected property diff --git a/docs/development/core/server/kibana-plugin-server.callapioptions.md b/docs/development/core/server/kibana-plugin-server.callapioptions.md index f9e39637f1e51..c4955929c7519 100644 --- a/docs/development/core/server/kibana-plugin-server.callapioptions.md +++ b/docs/development/core/server/kibana-plugin-server.callapioptions.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [CallAPIOptions](./kibana-plugin-server.callapioptions.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CallAPIOptions](./kibana-plugin-server.callapioptions.md) ## CallAPIOptions interface diff --git a/docs/development/core/server/kibana-plugin-server.callapioptions.wrap401errors.md b/docs/development/core/server/kibana-plugin-server.callapioptions.wrap401errors.md index e47b3ad2196e7..4436a07d0b28b 100644 --- a/docs/development/core/server/kibana-plugin-server.callapioptions.wrap401errors.md +++ b/docs/development/core/server/kibana-plugin-server.callapioptions.wrap401errors.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [CallAPIOptions](./kibana-plugin-server.callapioptions.md) > [wrap401Errors](./kibana-plugin-server.callapioptions.wrap401errors.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CallAPIOptions](./kibana-plugin-server.callapioptions.md) > [wrap401Errors](./kibana-plugin-server.callapioptions.wrap401errors.md) ## CallAPIOptions.wrap401Errors property diff --git a/docs/development/core/server/kibana-plugin-server.clusterclient.(constructor).md b/docs/development/core/server/kibana-plugin-server.clusterclient.(constructor).md new file mode 100644 index 0000000000000..82046df278a68 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.clusterclient.(constructor).md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ClusterClient](./kibana-plugin-server.clusterclient.md) > [(constructor)](./kibana-plugin-server.clusterclient.(constructor).md) + +## ClusterClient.(constructor) + +Constructs a new instance of the `ClusterClient` class + +Signature: + +```typescript +constructor(config: ElasticsearchClientConfig, log: Logger); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| config | ElasticsearchClientConfig | | +| log | Logger | | + diff --git a/docs/development/core/server/kibana-plugin-server.clusterclient.asscoped.md b/docs/development/core/server/kibana-plugin-server.clusterclient.asscoped.md index a13b0c3fb78d2..d0f7a4c93c69d 100644 --- a/docs/development/core/server/kibana-plugin-server.clusterclient.asscoped.md +++ b/docs/development/core/server/kibana-plugin-server.clusterclient.asscoped.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [ClusterClient](./kibana-plugin-server.clusterclient.md) > [asScoped](./kibana-plugin-server.clusterclient.asscoped.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ClusterClient](./kibana-plugin-server.clusterclient.md) > [asScoped](./kibana-plugin-server.clusterclient.asscoped.md) ## ClusterClient.asScoped() method @@ -18,7 +18,7 @@ asScoped(req?: { | Parameter | Type | Description | | --- | --- | --- | -| req | {`

` headers?: Headers;`

` } | Request the ScopedClusterClient instance will be scoped to. | +| req | {
headers?: Headers;
} | Request the ScopedClusterClient instance will be scoped to. | Returns: diff --git a/docs/development/core/server/kibana-plugin-server.clusterclient.callasinternaluser.md b/docs/development/core/server/kibana-plugin-server.clusterclient.callasinternaluser.md index 1a708a951052d..b00e83dd43d52 100644 --- a/docs/development/core/server/kibana-plugin-server.clusterclient.callasinternaluser.md +++ b/docs/development/core/server/kibana-plugin-server.clusterclient.callasinternaluser.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [ClusterClient](./kibana-plugin-server.clusterclient.md) > [callAsInternalUser](./kibana-plugin-server.clusterclient.callasinternaluser.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ClusterClient](./kibana-plugin-server.clusterclient.md) > [callAsInternalUser](./kibana-plugin-server.clusterclient.callasinternaluser.md) ## ClusterClient.callAsInternalUser property diff --git a/docs/development/core/server/kibana-plugin-server.clusterclient.close.md b/docs/development/core/server/kibana-plugin-server.clusterclient.close.md index cc2af5224b307..6030f4372e8e0 100644 --- a/docs/development/core/server/kibana-plugin-server.clusterclient.close.md +++ b/docs/development/core/server/kibana-plugin-server.clusterclient.close.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [ClusterClient](./kibana-plugin-server.clusterclient.md) > [close](./kibana-plugin-server.clusterclient.close.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ClusterClient](./kibana-plugin-server.clusterclient.md) > [close](./kibana-plugin-server.clusterclient.close.md) ## ClusterClient.close() method diff --git a/docs/development/core/server/kibana-plugin-server.clusterclient.md b/docs/development/core/server/kibana-plugin-server.clusterclient.md index 4b88daf79a5c2..89b30379e38b8 100644 --- a/docs/development/core/server/kibana-plugin-server.clusterclient.md +++ b/docs/development/core/server/kibana-plugin-server.clusterclient.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [ClusterClient](./kibana-plugin-server.clusterclient.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ClusterClient](./kibana-plugin-server.clusterclient.md) ## ClusterClient class @@ -12,6 +12,12 @@ Represents an Elasticsearch cluster API client and allows to call API on behalf export declare class ClusterClient ``` +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(config, log)](./kibana-plugin-server.clusterclient.(constructor).md) | | Constructs a new instance of the ClusterClient class | + ## Properties | Property | Modifiers | Type | Description | diff --git a/docs/development/core/server/kibana-plugin-server.configservice.atpath.md b/docs/development/core/server/kibana-plugin-server.configservice.atpath.md deleted file mode 100644 index 5ae66deb74856..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.configservice.atpath.md +++ /dev/null @@ -1,25 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigService](./kibana-plugin-server.configservice.md) > [atPath](./kibana-plugin-server.configservice.atpath.md) - -## ConfigService.atPath() method - -Reads the subset of the config at the specified `path` and validates it against the static `schema` on the given `ConfigClass`. - -Signature: - -```typescript -atPath, TConfig>(path: ConfigPath, ConfigClass: ConfigWithSchema): Observable; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| path | ConfigPath | The path to the desired subset of the config. | -| ConfigClass | ConfigWithSchema<TSchema, TConfig> | A class (not an instance of a class) that contains a static schema that we validate the config at the given path against. | - -Returns: - -`Observable` - diff --git a/docs/development/core/server/kibana-plugin-server.configservice.getconfig$.md b/docs/development/core/server/kibana-plugin-server.configservice.getconfig$.md deleted file mode 100644 index 6a9e288a8160e..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.configservice.getconfig$.md +++ /dev/null @@ -1,17 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigService](./kibana-plugin-server.configservice.md) > [getConfig$](./kibana-plugin-server.configservice.getconfig$.md) - -## ConfigService.getConfig$() method - -Returns the full config object observable. This is not intended for "normal use", but for features that \_need\_ access to the full object. - -Signature: - -```typescript -getConfig$(): Observable; -``` -Returns: - -`Observable` - diff --git a/docs/development/core/server/kibana-plugin-server.configservice.getunusedpaths.md b/docs/development/core/server/kibana-plugin-server.configservice.getunusedpaths.md deleted file mode 100644 index 9026672abb78e..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.configservice.getunusedpaths.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigService](./kibana-plugin-server.configservice.md) > [getUnusedPaths](./kibana-plugin-server.configservice.getunusedpaths.md) - -## ConfigService.getUnusedPaths() method - -Signature: - -```typescript -getUnusedPaths(): Promise; -``` -Returns: - -`Promise` - diff --git a/docs/development/core/server/kibana-plugin-server.configservice.getusedpaths.md b/docs/development/core/server/kibana-plugin-server.configservice.getusedpaths.md deleted file mode 100644 index a29b075a8b3e4..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.configservice.getusedpaths.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigService](./kibana-plugin-server.configservice.md) > [getUsedPaths](./kibana-plugin-server.configservice.getusedpaths.md) - -## ConfigService.getUsedPaths() method - -Signature: - -```typescript -getUsedPaths(): Promise; -``` -Returns: - -`Promise` - diff --git a/docs/development/core/server/kibana-plugin-server.configservice.isenabledatpath.md b/docs/development/core/server/kibana-plugin-server.configservice.isenabledatpath.md deleted file mode 100644 index 08c9985145f35..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.configservice.isenabledatpath.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigService](./kibana-plugin-server.configservice.md) > [isEnabledAtPath](./kibana-plugin-server.configservice.isenabledatpath.md) - -## ConfigService.isEnabledAtPath() method - -Signature: - -```typescript -isEnabledAtPath(path: ConfigPath): Promise; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| path | ConfigPath | | - -Returns: - -`Promise` - diff --git a/docs/development/core/server/kibana-plugin-server.configservice.md b/docs/development/core/server/kibana-plugin-server.configservice.md deleted file mode 100644 index 34a2bd9cecaab..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.configservice.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigService](./kibana-plugin-server.configservice.md) - -## ConfigService class - - -Signature: - -```typescript -export declare class ConfigService -``` - -## Methods - -| Method | Modifiers | Description | -| --- | --- | --- | -| [atPath(path, ConfigClass)](./kibana-plugin-server.configservice.atpath.md) | | Reads the subset of the config at the specified path and validates it against the static schema on the given ConfigClass. | -| [getConfig$()](./kibana-plugin-server.configservice.getconfig$.md) | | Returns the full config object observable. This is not intended for "normal use", but for features that \_need\_ access to the full object. | -| [getUnusedPaths()](./kibana-plugin-server.configservice.getunusedpaths.md) | | | -| [getUsedPaths()](./kibana-plugin-server.configservice.getusedpaths.md) | | | -| [isEnabledAtPath(path)](./kibana-plugin-server.configservice.isenabledatpath.md) | | | -| [optionalAtPath(path, ConfigClass)](./kibana-plugin-server.configservice.optionalatpath.md) | | Same as atPath, but returns undefined if there is no config at the specified path.[ConfigService.atPath()](./kibana-plugin-server.configservice.atpath.md) | - diff --git a/docs/development/core/server/kibana-plugin-server.configservice.optionalatpath.md b/docs/development/core/server/kibana-plugin-server.configservice.optionalatpath.md deleted file mode 100644 index 8efc64568f830..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.configservice.optionalatpath.md +++ /dev/null @@ -1,27 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigService](./kibana-plugin-server.configservice.md) > [optionalAtPath](./kibana-plugin-server.configservice.optionalatpath.md) - -## ConfigService.optionalAtPath() method - -Same as `atPath`, but returns `undefined` if there is no config at the specified path. - -[ConfigService.atPath()](./kibana-plugin-server.configservice.atpath.md) - -Signature: - -```typescript -optionalAtPath, TConfig>(path: ConfigPath, ConfigClass: ConfigWithSchema): Observable; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| path | ConfigPath | | -| ConfigClass | ConfigWithSchema<TSchema, TConfig> | | - -Returns: - -`Observable` - diff --git a/docs/development/core/server/kibana-plugin-server.configservice.setschema.md b/docs/development/core/server/kibana-plugin-server.configservice.setschema.md deleted file mode 100644 index 3db6b071dbdad..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.configservice.setschema.md +++ /dev/null @@ -1,25 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigService](./kibana-plugin-server.configservice.md) > [setSchema](./kibana-plugin-server.configservice.setschema.md) - -## ConfigService.setSchema() method - -Set config schema for a path and performs its validation - -Signature: - -```typescript -setSchema(path: ConfigPath, schema: Type): Promise; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| path | ConfigPath | | -| schema | Type<unknown> | | - -Returns: - -`Promise` - diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.elasticsearch.md b/docs/development/core/server/kibana-plugin-server.coresetup.elasticsearch.md index 8bbec09d409f4..d837b614e58a0 100644 --- a/docs/development/core/server/kibana-plugin-server.coresetup.elasticsearch.md +++ b/docs/development/core/server/kibana-plugin-server.coresetup.elasticsearch.md @@ -1,11 +1,14 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [CoreSetup](./kibana-plugin-server.coresetup.md) > [elasticsearch](./kibana-plugin-server.coresetup.elasticsearch.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CoreSetup](./kibana-plugin-server.coresetup.md) > [elasticsearch](./kibana-plugin-server.coresetup.elasticsearch.md) ## CoreSetup.elasticsearch property Signature: ```typescript -elasticsearch: ElasticsearchServiceSetup; +elasticsearch: { + adminClient$: Observable; + dataClient$: Observable; + }; ``` diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.http.md b/docs/development/core/server/kibana-plugin-server.coresetup.http.md index c64034f88af66..d6b87006da533 100644 --- a/docs/development/core/server/kibana-plugin-server.coresetup.http.md +++ b/docs/development/core/server/kibana-plugin-server.coresetup.http.md @@ -1,11 +1,18 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [CoreSetup](./kibana-plugin-server.coresetup.md) > [http](./kibana-plugin-server.coresetup.http.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CoreSetup](./kibana-plugin-server.coresetup.md) > [http](./kibana-plugin-server.coresetup.http.md) ## CoreSetup.http property Signature: ```typescript -http: HttpServiceSetup; +http: { + registerOnPreAuth: HttpServiceSetup['registerOnPreAuth']; + registerAuth: HttpServiceSetup['registerAuth']; + registerOnPostAuth: HttpServiceSetup['registerOnPostAuth']; + getBasePathFor: HttpServiceSetup['getBasePathFor']; + setBasePathFor: HttpServiceSetup['setBasePathFor']; + createNewServer: HttpServiceSetup['createNewServer']; + }; ``` diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.md b/docs/development/core/server/kibana-plugin-server.coresetup.md index 5c9f3be3866b7..a361707664b87 100644 --- a/docs/development/core/server/kibana-plugin-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-server.coresetup.md @@ -1,9 +1,10 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [CoreSetup](./kibana-plugin-server.coresetup.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CoreSetup](./kibana-plugin-server.coresetup.md) ## CoreSetup interface +Context passed to the plugins `setup` method. Signature: @@ -15,7 +16,6 @@ export interface CoreSetup | Property | Type | Description | | --- | --- | --- | -| [elasticsearch](./kibana-plugin-server.coresetup.elasticsearch.md) | ElasticsearchServiceSetup | | -| [http](./kibana-plugin-server.coresetup.http.md) | HttpServiceSetup | | -| [plugins](./kibana-plugin-server.coresetup.plugins.md) | PluginsServiceSetup | | +| [elasticsearch](./kibana-plugin-server.coresetup.elasticsearch.md) | {
adminClient$: Observable<ClusterClient>;
dataClient$: Observable<ClusterClient>;
} | | +| [http](./kibana-plugin-server.coresetup.http.md) | {
registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];
registerAuth: HttpServiceSetup['registerAuth'];
registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];
getBasePathFor: HttpServiceSetup['getBasePathFor'];
setBasePathFor: HttpServiceSetup['setBasePathFor'];
createNewServer: HttpServiceSetup['createNewServer'];
} | | diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.plugins.md b/docs/development/core/server/kibana-plugin-server.coresetup.plugins.md deleted file mode 100644 index 0030f60287574..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.coresetup.plugins.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [CoreSetup](./kibana-plugin-server.coresetup.md) > [plugins](./kibana-plugin-server.coresetup.plugins.md) - -## CoreSetup.plugins property - -Signature: - -```typescript -plugins: PluginsServiceSetup; -``` diff --git a/docs/development/core/server/kibana-plugin-server.corestart.http.md b/docs/development/core/server/kibana-plugin-server.corestart.http.md deleted file mode 100644 index a12fcbaab27cc..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.corestart.http.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [CoreStart](./kibana-plugin-server.corestart.md) > [http](./kibana-plugin-server.corestart.http.md) - -## CoreStart.http property - -Signature: - -```typescript -http: HttpServiceStart; -``` diff --git a/docs/development/core/server/kibana-plugin-server.corestart.md b/docs/development/core/server/kibana-plugin-server.corestart.md index 119036da61ee1..da80ae8be93af 100644 --- a/docs/development/core/server/kibana-plugin-server.corestart.md +++ b/docs/development/core/server/kibana-plugin-server.corestart.md @@ -1,19 +1,13 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [CoreStart](./kibana-plugin-server.corestart.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CoreStart](./kibana-plugin-server.corestart.md) ## CoreStart interface +Context passed to the plugins `start` method. + Signature: ```typescript export interface CoreStart ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [http](./kibana-plugin-server.corestart.http.md) | HttpServiceStart | | -| [plugins](./kibana-plugin-server.corestart.plugins.md) | PluginsServiceStart | | - diff --git a/docs/development/core/server/kibana-plugin-server.corestart.plugins.md b/docs/development/core/server/kibana-plugin-server.corestart.plugins.md deleted file mode 100644 index 709d666b1ea23..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.corestart.plugins.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [CoreStart](./kibana-plugin-server.corestart.md) > [plugins](./kibana-plugin-server.corestart.plugins.md) - -## CoreStart.plugins property - -Signature: - -```typescript -plugins: PluginsServiceStart; -``` diff --git a/docs/development/core/server/kibana-plugin-server.discoveredplugin.configpath.md b/docs/development/core/server/kibana-plugin-server.discoveredplugin.configpath.md new file mode 100644 index 0000000000000..b0830b8d72238 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.discoveredplugin.configpath.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [DiscoveredPlugin](./kibana-plugin-server.discoveredplugin.md) > [configPath](./kibana-plugin-server.discoveredplugin.configpath.md) + +## DiscoveredPlugin.configPath property + +Root configuration path used by the plugin, defaults to "id". + +Signature: + +```typescript +readonly configPath: ConfigPath; +``` diff --git a/docs/development/core/server/kibana-plugin-server.discoveredplugin.id.md b/docs/development/core/server/kibana-plugin-server.discoveredplugin.id.md new file mode 100644 index 0000000000000..54287ba69556f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.discoveredplugin.id.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [DiscoveredPlugin](./kibana-plugin-server.discoveredplugin.md) > [id](./kibana-plugin-server.discoveredplugin.id.md) + +## DiscoveredPlugin.id property + +Identifier of the plugin. + +Signature: + +```typescript +readonly id: PluginName; +``` diff --git a/docs/development/core/server/kibana-plugin-server.discoveredplugin.md b/docs/development/core/server/kibana-plugin-server.discoveredplugin.md new file mode 100644 index 0000000000000..71f8725b57ea3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.discoveredplugin.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [DiscoveredPlugin](./kibana-plugin-server.discoveredplugin.md) + +## DiscoveredPlugin interface + +Small container object used to expose information about discovered plugins that may or may not have been started. + +Signature: + +```typescript +export interface DiscoveredPlugin +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [configPath](./kibana-plugin-server.discoveredplugin.configpath.md) | ConfigPath | Root configuration path used by the plugin, defaults to "id". | +| [id](./kibana-plugin-server.discoveredplugin.id.md) | PluginName | Identifier of the plugin. | +| [optionalPlugins](./kibana-plugin-server.discoveredplugin.optionalplugins.md) | ReadonlyArray<PluginName> | An optional list of the other plugins that if installed and enabled \*\*may be\*\* leveraged by this plugin for some additional functionality but otherwise are not required for this plugin to work properly. | +| [requiredPlugins](./kibana-plugin-server.discoveredplugin.requiredplugins.md) | ReadonlyArray<PluginName> | An optional list of the other plugins that \*\*must be\*\* installed and enabled for this plugin to function properly. | + diff --git a/docs/development/core/server/kibana-plugin-server.discoveredplugin.optionalplugins.md b/docs/development/core/server/kibana-plugin-server.discoveredplugin.optionalplugins.md new file mode 100644 index 0000000000000..92fa7c7eea882 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.discoveredplugin.optionalplugins.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [DiscoveredPlugin](./kibana-plugin-server.discoveredplugin.md) > [optionalPlugins](./kibana-plugin-server.discoveredplugin.optionalplugins.md) + +## DiscoveredPlugin.optionalPlugins property + +An optional list of the other plugins that if installed and enabled \*\*may be\*\* leveraged by this plugin for some additional functionality but otherwise are not required for this plugin to work properly. + +Signature: + +```typescript +readonly optionalPlugins: ReadonlyArray; +``` diff --git a/docs/development/core/server/kibana-plugin-server.discoveredplugin.requiredplugins.md b/docs/development/core/server/kibana-plugin-server.discoveredplugin.requiredplugins.md new file mode 100644 index 0000000000000..04a394fe7e1f1 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.discoveredplugin.requiredplugins.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [DiscoveredPlugin](./kibana-plugin-server.discoveredplugin.md) > [requiredPlugins](./kibana-plugin-server.discoveredplugin.requiredplugins.md) + +## DiscoveredPlugin.requiredPlugins property + +An optional list of the other plugins that \*\*must be\*\* installed and enabled for this plugin to function properly. + +Signature: + +```typescript +readonly requiredPlugins: ReadonlyArray; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchclientconfig.md b/docs/development/core/server/kibana-plugin-server.elasticsearchclientconfig.md index 3cf3aadec4411..cd42a84f621a9 100644 --- a/docs/development/core/server/kibana-plugin-server.elasticsearchclientconfig.md +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchclientconfig.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchClientConfig](./kibana-plugin-server.elasticsearchclientconfig.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchClientConfig](./kibana-plugin-server.elasticsearchclientconfig.md) ## ElasticsearchClientConfig type diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.adminclient$.md b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.adminclient$.md index 1bae2818d3c2e..e3cabf9ebf892 100644 --- a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.adminclient$.md +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.adminclient$.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) > [adminClient$](./kibana-plugin-server.elasticsearchservicesetup.adminclient$.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) > [adminClient$](./kibana-plugin-server.elasticsearchservicesetup.adminclient$.md) ## ElasticsearchServiceSetup.adminClient$ property diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.createclient.md b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.createclient.md index 96830e2000f8b..8b1ce883c4a14 100644 --- a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.createclient.md +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.createclient.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) > [createClient](./kibana-plugin-server.elasticsearchservicesetup.createclient.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) > [createClient](./kibana-plugin-server.elasticsearchservicesetup.createclient.md) ## ElasticsearchServiceSetup.createClient property diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.dataclient$.md b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.dataclient$.md index b440a9ef73a47..bb02082f76a12 100644 --- a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.dataclient$.md +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.dataclient$.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) > [dataClient$](./kibana-plugin-server.elasticsearchservicesetup.dataclient$.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) > [dataClient$](./kibana-plugin-server.elasticsearchservicesetup.dataclient$.md) ## ElasticsearchServiceSetup.dataClient$ property diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.legacy.md b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.legacy.md index 2440a441b2689..6040b407dd415 100644 --- a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.legacy.md +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.legacy.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) > [legacy](./kibana-plugin-server.elasticsearchservicesetup.legacy.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) > [legacy](./kibana-plugin-server.elasticsearchservicesetup.legacy.md) ## ElasticsearchServiceSetup.legacy property diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.md b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.md index 90abe2201f1a7..3197869d6d663 100644 --- a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.md +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) ## ElasticsearchServiceSetup interface @@ -18,5 +18,5 @@ export interface ElasticsearchServiceSetup | [adminClient$](./kibana-plugin-server.elasticsearchservicesetup.adminclient$.md) | Observable<ClusterClient> | | | [createClient](./kibana-plugin-server.elasticsearchservicesetup.createclient.md) | (type: string, config: ElasticsearchClientConfig) => ClusterClient | | | [dataClient$](./kibana-plugin-server.elasticsearchservicesetup.dataclient$.md) | Observable<ClusterClient> | | -| [legacy](./kibana-plugin-server.elasticsearchservicesetup.legacy.md) | {`

` readonly config$: Observable<ElasticsearchConfig>;`

` } | | +| [legacy](./kibana-plugin-server.elasticsearchservicesetup.legacy.md) | {
readonly config$: Observable<ElasticsearchConfig>;
} | | diff --git a/docs/development/core/server/kibana-plugin-server.headers.md b/docs/development/core/server/kibana-plugin-server.headers.md index 0f63bf18f7c25..83259efe8b79d 100644 --- a/docs/development/core/server/kibana-plugin-server.headers.md +++ b/docs/development/core/server/kibana-plugin-server.headers.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Headers](./kibana-plugin-server.headers.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Headers](./kibana-plugin-server.headers.md) ## Headers type diff --git a/docs/development/core/server/kibana-plugin-server.httpservicesetup.createnewserver.md b/docs/development/core/server/kibana-plugin-server.httpservicesetup.createnewserver.md new file mode 100644 index 0000000000000..e41684ea2b784 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpservicesetup.createnewserver.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) > [createNewServer](./kibana-plugin-server.httpservicesetup.createnewserver.md) + +## HttpServiceSetup.createNewServer property + +Signature: + +```typescript +createNewServer: (cfg: Partial) => Promise; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpservicesetup.md b/docs/development/core/server/kibana-plugin-server.httpservicesetup.md index 36335c0d5f4cd..ec4a2537b8404 100644 --- a/docs/development/core/server/kibana-plugin-server.httpservicesetup.md +++ b/docs/development/core/server/kibana-plugin-server.httpservicesetup.md @@ -1,12 +1,19 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) -## HttpServiceSetup type +## HttpServiceSetup interface Signature: ```typescript -export declare type HttpServiceSetup = HttpServerSetup; +export interface HttpServiceSetup extends HttpServerSetup ``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [createNewServer](./kibana-plugin-server.httpservicesetup.createnewserver.md) | (cfg: Partial<HttpConfig>) => Promise<HttpServerSetup> | | + diff --git a/docs/development/core/server/kibana-plugin-server.httpservicestart.islistening.md b/docs/development/core/server/kibana-plugin-server.httpservicestart.islistening.md index 5d27db32fc946..b86fd76180a24 100644 --- a/docs/development/core/server/kibana-plugin-server.httpservicestart.islistening.md +++ b/docs/development/core/server/kibana-plugin-server.httpservicestart.islistening.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServiceStart](./kibana-plugin-server.httpservicestart.md) > [isListening](./kibana-plugin-server.httpservicestart.islistening.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServiceStart](./kibana-plugin-server.httpservicestart.md) > [isListening](./kibana-plugin-server.httpservicestart.islistening.md) ## HttpServiceStart.isListening property diff --git a/docs/development/core/server/kibana-plugin-server.httpservicestart.md b/docs/development/core/server/kibana-plugin-server.httpservicestart.md index 33e43061bdedf..dbcbbe787a17a 100644 --- a/docs/development/core/server/kibana-plugin-server.httpservicestart.md +++ b/docs/development/core/server/kibana-plugin-server.httpservicestart.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServiceStart](./kibana-plugin-server.httpservicestart.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServiceStart](./kibana-plugin-server.httpservicestart.md) ## HttpServiceStart interface diff --git a/docs/development/core/server/kibana-plugin-server.internalcorestart.http.md b/docs/development/core/server/kibana-plugin-server.internalcorestart.http.md new file mode 100644 index 0000000000000..0e2a49ae17968 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.internalcorestart.http.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [InternalCoreStart](./kibana-plugin-server.internalcorestart.md) > [http](./kibana-plugin-server.internalcorestart.http.md) + +## InternalCoreStart.http property + +Signature: + +```typescript +http: HttpServiceStart; +``` diff --git a/docs/development/core/server/kibana-plugin-server.internalcorestart.md b/docs/development/core/server/kibana-plugin-server.internalcorestart.md new file mode 100644 index 0000000000000..5f6c6617e641c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.internalcorestart.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [InternalCoreStart](./kibana-plugin-server.internalcorestart.md) + +## InternalCoreStart interface + + +Signature: + +```typescript +export interface InternalCoreStart +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [http](./kibana-plugin-server.internalcorestart.http.md) | HttpServiceStart | | +| [plugins](./kibana-plugin-server.internalcorestart.plugins.md) | PluginsServiceStart | | + diff --git a/docs/development/core/server/kibana-plugin-server.internalcorestart.plugins.md b/docs/development/core/server/kibana-plugin-server.internalcorestart.plugins.md new file mode 100644 index 0000000000000..1f6e17325e31d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.internalcorestart.plugins.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [InternalCoreStart](./kibana-plugin-server.internalcorestart.md) > [plugins](./kibana-plugin-server.internalcorestart.plugins.md) + +## InternalCoreStart.plugins property + +Signature: + +```typescript +plugins: PluginsServiceStart; +``` diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.(constructor).md b/docs/development/core/server/kibana-plugin-server.kibanarequest.(constructor).md new file mode 100644 index 0000000000000..f29493c1e5036 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.(constructor).md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [(constructor)](./kibana-plugin-server.kibanarequest.(constructor).md) + +## KibanaRequest.(constructor) + +Constructs a new instance of the `KibanaRequest` class + +Signature: + +```typescript +constructor(request: Request, params: Params, query: Query, body: Body); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| request | Request | | +| params | Params | | +| query | Query | | +| body | Body | | + diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.body.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.body.md index 60ef0a023702a..b1284f58c6815 100644 --- a/docs/development/core/server/kibana-plugin-server.kibanarequest.body.md +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.body.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [body](./kibana-plugin-server.kibanarequest.body.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [body](./kibana-plugin-server.kibanarequest.body.md) ## KibanaRequest.body property diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.from.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.from.md deleted file mode 100644 index cdf7ea1ebfe43..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.kibanarequest.from.md +++ /dev/null @@ -1,25 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [from](./kibana-plugin-server.kibanarequest.from.md) - -## KibanaRequest.from() method - -Factory for creating requests. Validates the request before creating an instance of a KibanaRequest. - -Signature: - -```typescript -static from

(req: Request, routeSchemas: RouteSchemas | undefined): KibanaRequest; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| req | Request | | -| routeSchemas | RouteSchemas<P, Q, B> | undefined | | - -Returns: - -`KibanaRequest` - diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.getfilteredheaders.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.getfilteredheaders.md index c17a416fed2d6..ca0918c53562f 100644 --- a/docs/development/core/server/kibana-plugin-server.kibanarequest.getfilteredheaders.md +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.getfilteredheaders.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [getFilteredHeaders](./kibana-plugin-server.kibanarequest.getfilteredheaders.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [getFilteredHeaders](./kibana-plugin-server.kibanarequest.getfilteredheaders.md) ## KibanaRequest.getFilteredHeaders() method diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.headers.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.headers.md index f5c38cc752ab3..3c95f91514822 100644 --- a/docs/development/core/server/kibana-plugin-server.kibanarequest.headers.md +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.headers.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [headers](./kibana-plugin-server.kibanarequest.headers.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [headers](./kibana-plugin-server.kibanarequest.headers.md) ## KibanaRequest.headers property diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.md index f7f7707b5657e..a09632febe531 100644 --- a/docs/development/core/server/kibana-plugin-server.kibanarequest.md +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.md @@ -1,9 +1,10 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) ## KibanaRequest class +Kibana specific abstraction for an incoming request. Signature: @@ -11,6 +12,12 @@ export declare class KibanaRequest ``` +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(request, params, query, body)](./kibana-plugin-server.kibanarequest.(constructor).md) | | Constructs a new instance of the KibanaRequest class | + ## Properties | Property | Modifiers | Type | Description | @@ -18,14 +25,13 @@ export declare class KibanaRequestBody | | | [headers](./kibana-plugin-server.kibanarequest.headers.md) | | Headers | | | [params](./kibana-plugin-server.kibanarequest.params.md) | | Params | | -| [path](./kibana-plugin-server.kibanarequest.path.md) | | string | | | [query](./kibana-plugin-server.kibanarequest.query.md) | | Query | | +| [route](./kibana-plugin-server.kibanarequest.route.md) | | RecursiveReadonly<KibanaRequestRoute> | | +| [url](./kibana-plugin-server.kibanarequest.url.md) | | Url | | ## Methods | Method | Modifiers | Description | | --- | --- | --- | -| [from(req, routeSchemas)](./kibana-plugin-server.kibanarequest.from.md) | static | Factory for creating requests. Validates the request before creating an instance of a KibanaRequest. | | [getFilteredHeaders(headersToKeep)](./kibana-plugin-server.kibanarequest.getfilteredheaders.md) | | | -| [unstable\_getIncomingMessage()](./kibana-plugin-server.kibanarequest.unstable_getincomingmessage.md) | | | diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.params.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.params.md index ef26aac2e6904..c8902be737d81 100644 --- a/docs/development/core/server/kibana-plugin-server.kibanarequest.params.md +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.params.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [params](./kibana-plugin-server.kibanarequest.params.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [params](./kibana-plugin-server.kibanarequest.params.md) ## KibanaRequest.params property diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.path.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.path.md deleted file mode 100644 index ebf6fb28b98ea..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.kibanarequest.path.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [path](./kibana-plugin-server.kibanarequest.path.md) - -## KibanaRequest.path property - -Signature: - -```typescript -readonly path: string; -``` diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.query.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.query.md index 69fb3faed84a2..30a5739676403 100644 --- a/docs/development/core/server/kibana-plugin-server.kibanarequest.query.md +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.query.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [query](./kibana-plugin-server.kibanarequest.query.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [query](./kibana-plugin-server.kibanarequest.query.md) ## KibanaRequest.query property diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.route.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.route.md new file mode 100644 index 0000000000000..301eeef1b6bb5 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.route.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [route](./kibana-plugin-server.kibanarequest.route.md) + +## KibanaRequest.route property + +Signature: + +```typescript +readonly route: RecursiveReadonly; +``` diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.unstable_getincomingmessage.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.unstable_getincomingmessage.md deleted file mode 100644 index d4f3c1b54a6cd..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.kibanarequest.unstable_getincomingmessage.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [unstable\_getIncomingMessage](./kibana-plugin-server.kibanarequest.unstable_getincomingmessage.md) - -## KibanaRequest.unstable\_getIncomingMessage() method - -Signature: - -```typescript -unstable_getIncomingMessage(): import("http").IncomingMessage; -``` -Returns: - -`import("http").IncomingMessage` - diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.url.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.url.md new file mode 100644 index 0000000000000..b8bd46199763e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.url.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [url](./kibana-plugin-server.kibanarequest.url.md) + +## KibanaRequest.url property + +Signature: + +```typescript +readonly url: Url; +``` diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequestroute.md b/docs/development/core/server/kibana-plugin-server.kibanarequestroute.md new file mode 100644 index 0000000000000..b92fe45d19edb --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.kibanarequestroute.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequestRoute](./kibana-plugin-server.kibanarequestroute.md) + +## KibanaRequestRoute interface + +Request specific route information exposed to a handler. + +Signature: + +```typescript +export interface KibanaRequestRoute +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [method](./kibana-plugin-server.kibanarequestroute.method.md) | RouteMethod | 'patch' | 'options' | | +| [options](./kibana-plugin-server.kibanarequestroute.options.md) | Required<RouteConfigOptions> | | +| [path](./kibana-plugin-server.kibanarequestroute.path.md) | string | | + diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequestroute.method.md b/docs/development/core/server/kibana-plugin-server.kibanarequestroute.method.md new file mode 100644 index 0000000000000..c003b06e128e4 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.kibanarequestroute.method.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequestRoute](./kibana-plugin-server.kibanarequestroute.md) > [method](./kibana-plugin-server.kibanarequestroute.method.md) + +## KibanaRequestRoute.method property + +Signature: + +```typescript +method: RouteMethod | 'patch' | 'options'; +``` diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequestroute.options.md b/docs/development/core/server/kibana-plugin-server.kibanarequestroute.options.md new file mode 100644 index 0000000000000..98c898449a5b1 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.kibanarequestroute.options.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequestRoute](./kibana-plugin-server.kibanarequestroute.md) > [options](./kibana-plugin-server.kibanarequestroute.options.md) + +## KibanaRequestRoute.options property + +Signature: + +```typescript +options: Required; +``` diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequestroute.path.md b/docs/development/core/server/kibana-plugin-server.kibanarequestroute.path.md new file mode 100644 index 0000000000000..17d4b588e6d44 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.kibanarequestroute.path.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequestRoute](./kibana-plugin-server.kibanarequestroute.md) > [path](./kibana-plugin-server.kibanarequestroute.path.md) + +## KibanaRequestRoute.path property + +Signature: + +```typescript +path: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.logger.debug.md b/docs/development/core/server/kibana-plugin-server.logger.debug.md index 659c2af4d9614..9a775896f618f 100644 --- a/docs/development/core/server/kibana-plugin-server.logger.debug.md +++ b/docs/development/core/server/kibana-plugin-server.logger.debug.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Logger](./kibana-plugin-server.logger.md) > [debug](./kibana-plugin-server.logger.debug.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Logger](./kibana-plugin-server.logger.md) > [debug](./kibana-plugin-server.logger.debug.md) ## Logger.debug() method diff --git a/docs/development/core/server/kibana-plugin-server.logger.error.md b/docs/development/core/server/kibana-plugin-server.logger.error.md index a02b3b6e7f23b..482770d267095 100644 --- a/docs/development/core/server/kibana-plugin-server.logger.error.md +++ b/docs/development/core/server/kibana-plugin-server.logger.error.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Logger](./kibana-plugin-server.logger.md) > [error](./kibana-plugin-server.logger.error.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Logger](./kibana-plugin-server.logger.md) > [error](./kibana-plugin-server.logger.error.md) ## Logger.error() method diff --git a/docs/development/core/server/kibana-plugin-server.logger.fatal.md b/docs/development/core/server/kibana-plugin-server.logger.fatal.md index b179171b8c304..68f502a54f560 100644 --- a/docs/development/core/server/kibana-plugin-server.logger.fatal.md +++ b/docs/development/core/server/kibana-plugin-server.logger.fatal.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Logger](./kibana-plugin-server.logger.md) > [fatal](./kibana-plugin-server.logger.fatal.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Logger](./kibana-plugin-server.logger.md) > [fatal](./kibana-plugin-server.logger.fatal.md) ## Logger.fatal() method diff --git a/docs/development/core/server/kibana-plugin-server.logger.info.md b/docs/development/core/server/kibana-plugin-server.logger.info.md index 55e01e12660a0..28a15f538f739 100644 --- a/docs/development/core/server/kibana-plugin-server.logger.info.md +++ b/docs/development/core/server/kibana-plugin-server.logger.info.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Logger](./kibana-plugin-server.logger.md) > [info](./kibana-plugin-server.logger.info.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Logger](./kibana-plugin-server.logger.md) > [info](./kibana-plugin-server.logger.info.md) ## Logger.info() method diff --git a/docs/development/core/server/kibana-plugin-server.logger.md b/docs/development/core/server/kibana-plugin-server.logger.md index b252d99071116..96f327a798485 100644 --- a/docs/development/core/server/kibana-plugin-server.logger.md +++ b/docs/development/core/server/kibana-plugin-server.logger.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Logger](./kibana-plugin-server.logger.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Logger](./kibana-plugin-server.logger.md) ## Logger interface diff --git a/docs/development/core/server/kibana-plugin-server.logger.trace.md b/docs/development/core/server/kibana-plugin-server.logger.trace.md index 935e8e0e753e8..e7aeeec21243b 100644 --- a/docs/development/core/server/kibana-plugin-server.logger.trace.md +++ b/docs/development/core/server/kibana-plugin-server.logger.trace.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Logger](./kibana-plugin-server.logger.md) > [trace](./kibana-plugin-server.logger.trace.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Logger](./kibana-plugin-server.logger.md) > [trace](./kibana-plugin-server.logger.trace.md) ## Logger.trace() method diff --git a/docs/development/core/server/kibana-plugin-server.logger.warn.md b/docs/development/core/server/kibana-plugin-server.logger.warn.md index ba25d4584af95..10e5cd5612fb2 100644 --- a/docs/development/core/server/kibana-plugin-server.logger.warn.md +++ b/docs/development/core/server/kibana-plugin-server.logger.warn.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Logger](./kibana-plugin-server.logger.md) > [warn](./kibana-plugin-server.logger.warn.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Logger](./kibana-plugin-server.logger.md) > [warn](./kibana-plugin-server.logger.warn.md) ## Logger.warn() method diff --git a/docs/development/core/server/kibana-plugin-server.loggerfactory.get.md b/docs/development/core/server/kibana-plugin-server.loggerfactory.get.md index 81f2fac40279e..b38820f6ba4ba 100644 --- a/docs/development/core/server/kibana-plugin-server.loggerfactory.get.md +++ b/docs/development/core/server/kibana-plugin-server.loggerfactory.get.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [LoggerFactory](./kibana-plugin-server.loggerfactory.md) > [get](./kibana-plugin-server.loggerfactory.get.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [LoggerFactory](./kibana-plugin-server.loggerfactory.md) > [get](./kibana-plugin-server.loggerfactory.get.md) ## LoggerFactory.get() method diff --git a/docs/development/core/server/kibana-plugin-server.loggerfactory.md b/docs/development/core/server/kibana-plugin-server.loggerfactory.md index 86547eb7dc5ba..07d5a4c012c4a 100644 --- a/docs/development/core/server/kibana-plugin-server.loggerfactory.md +++ b/docs/development/core/server/kibana-plugin-server.loggerfactory.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [LoggerFactory](./kibana-plugin-server.loggerfactory.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [LoggerFactory](./kibana-plugin-server.loggerfactory.md) ## LoggerFactory interface diff --git a/docs/development/core/server/kibana-plugin-server.logmeta.md b/docs/development/core/server/kibana-plugin-server.logmeta.md index 9f807ce0fc8fa..268cb7419db16 100644 --- a/docs/development/core/server/kibana-plugin-server.logmeta.md +++ b/docs/development/core/server/kibana-plugin-server.logmeta.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [LogMeta](./kibana-plugin-server.logmeta.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [LogMeta](./kibana-plugin-server.logmeta.md) ## LogMeta interface diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index f3e11fca6b280..7b7d3a9f0662e 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -1,16 +1,21 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) ## kibana-plugin-server package +The Kibana Core APIs for server-side plugins. + +A plugin's `server/index` file must contain a named import, `plugin`, that implements [PluginInitializer](./kibana-plugin-server.plugininitializer.md) which returns an object that implements [Plugin](./kibana-plugin-server.plugin.md). + +The plugin integrates with the core system via lifecycle events: `setup`, `start`, and `stop`. In each lifecycle method, the plugin will receive the corresponding core services available (either [CoreSetup](./kibana-plugin-server.coresetup.md) or [CoreStart](./kibana-plugin-server.corestart.md)) and any interfaces returned by dependency plugins' lifecycle method. Anything returned by the plugin's lifecycle method will be exposed to downstream dependencies when their corresponding lifecycle methods are invoked. + ## Classes | Class | Description | | --- | --- | | [ClusterClient](./kibana-plugin-server.clusterclient.md) | Represents an Elasticsearch cluster API client and allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)). | -| [ConfigService](./kibana-plugin-server.configservice.md) | | -| [KibanaRequest](./kibana-plugin-server.kibanarequest.md) | | +| [KibanaRequest](./kibana-plugin-server.kibanarequest.md) | Kibana specific abstraction for an incoming request. | | [Router](./kibana-plugin-server.router.md) | | | [ScopedClusterClient](./kibana-plugin-server.scopedclusterclient.md) | Serves the same purpose as "normal" ClusterClient but exposes additional callAsCurrentUser method that doesn't use credentials of the Kibana internal user (as callAsInternalUser does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API | @@ -20,18 +25,26 @@ | --- | --- | | [AuthToolkit](./kibana-plugin-server.authtoolkit.md) | A tool set defining an outcome of Auth interceptor for incoming request. | | [CallAPIOptions](./kibana-plugin-server.callapioptions.md) | The set of options that defines how API call should be made and result be processed. | -| [CoreSetup](./kibana-plugin-server.coresetup.md) | | -| [CoreStart](./kibana-plugin-server.corestart.md) | | +| [CoreSetup](./kibana-plugin-server.coresetup.md) | Context passed to the plugins setup method. | +| [CoreStart](./kibana-plugin-server.corestart.md) | Context passed to the plugins start method. | +| [DiscoveredPlugin](./kibana-plugin-server.discoveredplugin.md) | Small container object used to expose information about discovered plugins that may or may not have been started. | | [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) | | +| [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | | | [HttpServiceStart](./kibana-plugin-server.httpservicestart.md) | | +| [InternalCoreStart](./kibana-plugin-server.internalcorestart.md) | | +| [KibanaRequestRoute](./kibana-plugin-server.kibanarequestroute.md) | Request specific route information exposed to a handler. | | [Logger](./kibana-plugin-server.logger.md) | Logger exposes all the necessary methods to log any type of information and this is the interface used by the logging consumers including plugins. | | [LoggerFactory](./kibana-plugin-server.loggerfactory.md) | The single purpose of LoggerFactory interface is to define a way to retrieve a context-based logger instance. | | [LogMeta](./kibana-plugin-server.logmeta.md) | Contextual metadata | -| [OnRequestToolkit](./kibana-plugin-server.onrequesttoolkit.md) | A tool set defining an outcome of OnRequest interceptor for incoming request. | +| [OnPostAuthToolkit](./kibana-plugin-server.onpostauthtoolkit.md) | A tool set defining an outcome of OnPostAuth interceptor for incoming request. | +| [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. | | [Plugin](./kibana-plugin-server.plugin.md) | The interface that should be returned by a PluginInitializer. | | [PluginInitializerContext](./kibana-plugin-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. | -| [PluginSetupContext](./kibana-plugin-server.pluginsetupcontext.md) | Context passed to the plugins setup method. | -| [PluginStartContext](./kibana-plugin-server.pluginstartcontext.md) | Context passed to the plugins start method. | +| [PluginsServiceSetup](./kibana-plugin-server.pluginsservicesetup.md) | | +| [PluginsServiceStart](./kibana-plugin-server.pluginsservicestart.md) | | +| [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md) | Route specific configuration. | +| [SessionStorage](./kibana-plugin-server.sessionstorage.md) | Provides an interface to store and retrieve data across requests. | +| [SessionStorageFactory](./kibana-plugin-server.sessionstoragefactory.md) | SessionStorage factory to bind one to an incoming request | ## Type Aliases @@ -41,8 +54,10 @@ | [AuthenticationHandler](./kibana-plugin-server.authenticationhandler.md) | | | [ElasticsearchClientConfig](./kibana-plugin-server.elasticsearchclientconfig.md) | | | [Headers](./kibana-plugin-server.headers.md) | | -| [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | | -| [OnRequestHandler](./kibana-plugin-server.onrequesthandler.md) | | +| [OnPostAuthHandler](./kibana-plugin-server.onpostauthhandler.md) | | +| [OnPreAuthHandler](./kibana-plugin-server.onpreauthhandler.md) | | | [PluginInitializer](./kibana-plugin-server.plugininitializer.md) | The plugin export at the root of a plugin's server directory should conform to this interface. | | [PluginName](./kibana-plugin-server.pluginname.md) | Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays that use it as a key or value more obvious. | +| [RecursiveReadonly](./kibana-plugin-server.recursivereadonly.md) | | +| [RouteMethod](./kibana-plugin-server.routemethod.md) | The set of common HTTP methods supported by Kibana routing. | diff --git a/docs/development/core/server/kibana-plugin-server.onpostauthhandler.md b/docs/development/core/server/kibana-plugin-server.onpostauthhandler.md new file mode 100644 index 0000000000000..b28f55233d548 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpostauthhandler.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPostAuthHandler](./kibana-plugin-server.onpostauthhandler.md) + +## OnPostAuthHandler type + + +Signature: + +```typescript +export declare type OnPostAuthHandler = (request: KibanaRequest, t: OnPostAuthToolkit) => OnPostAuthResult | Promise; +``` diff --git a/docs/development/core/server/kibana-plugin-server.onpostauthtoolkit.md b/docs/development/core/server/kibana-plugin-server.onpostauthtoolkit.md new file mode 100644 index 0000000000000..b9d7a1463a200 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpostauthtoolkit.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPostAuthToolkit](./kibana-plugin-server.onpostauthtoolkit.md) + +## OnPostAuthToolkit interface + +A tool set defining an outcome of OnPostAuth interceptor for incoming request. + +Signature: + +```typescript +export interface OnPostAuthToolkit +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [next](./kibana-plugin-server.onpostauthtoolkit.next.md) | () => OnPostAuthResult | To pass request to the next handler | +| [redirected](./kibana-plugin-server.onpostauthtoolkit.redirected.md) | (url: string) => OnPostAuthResult | To interrupt request handling and redirect to a configured url | +| [rejected](./kibana-plugin-server.onpostauthtoolkit.rejected.md) | (error: Error, options?: {
statusCode?: number;
}) => OnPostAuthResult | Fail the request with specified error. | + diff --git a/docs/development/core/server/kibana-plugin-server.onpostauthtoolkit.next.md b/docs/development/core/server/kibana-plugin-server.onpostauthtoolkit.next.md new file mode 100644 index 0000000000000..cc9120defa442 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpostauthtoolkit.next.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPostAuthToolkit](./kibana-plugin-server.onpostauthtoolkit.md) > [next](./kibana-plugin-server.onpostauthtoolkit.next.md) + +## OnPostAuthToolkit.next property + +To pass request to the next handler + +Signature: + +```typescript +next: () => OnPostAuthResult; +``` diff --git a/docs/development/core/server/kibana-plugin-server.onpostauthtoolkit.redirected.md b/docs/development/core/server/kibana-plugin-server.onpostauthtoolkit.redirected.md new file mode 100644 index 0000000000000..94eab27724c8c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpostauthtoolkit.redirected.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPostAuthToolkit](./kibana-plugin-server.onpostauthtoolkit.md) > [redirected](./kibana-plugin-server.onpostauthtoolkit.redirected.md) + +## OnPostAuthToolkit.redirected property + +To interrupt request handling and redirect to a configured url + +Signature: + +```typescript +redirected: (url: string) => OnPostAuthResult; +``` diff --git a/docs/development/core/server/kibana-plugin-server.onpostauthtoolkit.rejected.md b/docs/development/core/server/kibana-plugin-server.onpostauthtoolkit.rejected.md new file mode 100644 index 0000000000000..00efb4fde305e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpostauthtoolkit.rejected.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPostAuthToolkit](./kibana-plugin-server.onpostauthtoolkit.md) > [rejected](./kibana-plugin-server.onpostauthtoolkit.rejected.md) + +## OnPostAuthToolkit.rejected property + +Fail the request with specified error. + +Signature: + +```typescript +rejected: (error: Error, options?: { + statusCode?: number; + }) => OnPostAuthResult; +``` diff --git a/docs/development/core/server/kibana-plugin-server.onpreauthhandler.md b/docs/development/core/server/kibana-plugin-server.onpreauthhandler.md new file mode 100644 index 0000000000000..8374f83fc810c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpreauthhandler.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreAuthHandler](./kibana-plugin-server.onpreauthhandler.md) + +## OnPreAuthHandler type + + +Signature: + +```typescript +export declare type OnPreAuthHandler = (request: KibanaRequest, t: OnPreAuthToolkit) => OnPreAuthResult | Promise; +``` diff --git a/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.md b/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.md new file mode 100644 index 0000000000000..787c9010372e0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md) + +## OnPreAuthToolkit interface + +A tool set defining an outcome of OnPreAuth interceptor for incoming request. + +Signature: + +```typescript +export interface OnPreAuthToolkit +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [next](./kibana-plugin-server.onpreauthtoolkit.next.md) | () => OnPreAuthResult | To pass request to the next handler | +| [redirected](./kibana-plugin-server.onpreauthtoolkit.redirected.md) | (url: string, options?: {
forward: boolean;
}) => OnPreAuthResult | To interrupt request handling and redirect to a configured url. If "options.forwarded" = true, request will be forwarded to another url right on the server. | +| [rejected](./kibana-plugin-server.onpreauthtoolkit.rejected.md) | (error: Error, options?: {
statusCode?: number;
}) => OnPreAuthResult | Fail the request with specified error. | + diff --git a/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.next.md b/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.next.md new file mode 100644 index 0000000000000..9281e5879ce9b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.next.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md) > [next](./kibana-plugin-server.onpreauthtoolkit.next.md) + +## OnPreAuthToolkit.next property + +To pass request to the next handler + +Signature: + +```typescript +next: () => OnPreAuthResult; +``` diff --git a/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.redirected.md b/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.redirected.md new file mode 100644 index 0000000000000..77deb5b61c4e2 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.redirected.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md) > [redirected](./kibana-plugin-server.onpreauthtoolkit.redirected.md) + +## OnPreAuthToolkit.redirected property + +To interrupt request handling and redirect to a configured url. If "options.forwarded" = true, request will be forwarded to another url right on the server. + +Signature: + +```typescript +redirected: (url: string, options?: { + forward: boolean; + }) => OnPreAuthResult; +``` diff --git a/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.rejected.md b/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.rejected.md new file mode 100644 index 0000000000000..1fd79d0b5766b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.rejected.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md) > [rejected](./kibana-plugin-server.onpreauthtoolkit.rejected.md) + +## OnPreAuthToolkit.rejected property + +Fail the request with specified error. + +Signature: + +```typescript +rejected: (error: Error, options?: { + statusCode?: number; + }) => OnPreAuthResult; +``` diff --git a/docs/development/core/server/kibana-plugin-server.onrequesthandler.md b/docs/development/core/server/kibana-plugin-server.onrequesthandler.md deleted file mode 100644 index 5d90e399db676..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.onrequesthandler.md +++ /dev/null @@ -1,12 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnRequestHandler](./kibana-plugin-server.onrequesthandler.md) - -## OnRequestHandler type - - -Signature: - -```typescript -export declare type OnRequestHandler = (req: KibanaRequest, t: OnRequestToolkit) => OnRequestResult | Promise; -``` diff --git a/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.md b/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.md deleted file mode 100644 index e6a79a13dd436..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.md +++ /dev/null @@ -1,23 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnRequestToolkit](./kibana-plugin-server.onrequesttoolkit.md) - -## OnRequestToolkit interface - -A tool set defining an outcome of OnRequest interceptor for incoming request. - -Signature: - -```typescript -export interface OnRequestToolkit -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [next](./kibana-plugin-server.onrequesttoolkit.next.md) | () => OnRequestResult | To pass request to the next handler | -| [redirected](./kibana-plugin-server.onrequesttoolkit.redirected.md) | (url: string) => OnRequestResult | To interrupt request handling and redirect to a configured url | -| [rejected](./kibana-plugin-server.onrequesttoolkit.rejected.md) | (error: Error, options?: {`

` statusCode?: number;`

` }) => OnRequestResult | Fail the request with specified error. | -| [setUrl](./kibana-plugin-server.onrequesttoolkit.seturl.md) | (newUrl: string | Url) => void | Change url for an incoming request. | - diff --git a/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.next.md b/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.next.md deleted file mode 100644 index 976e3b1a2db87..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.next.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnRequestToolkit](./kibana-plugin-server.onrequesttoolkit.md) > [next](./kibana-plugin-server.onrequesttoolkit.next.md) - -## OnRequestToolkit.next property - -To pass request to the next handler - -Signature: - -```typescript -next: () => OnRequestResult; -``` diff --git a/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.redirected.md b/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.redirected.md deleted file mode 100644 index 311398845bd59..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.redirected.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnRequestToolkit](./kibana-plugin-server.onrequesttoolkit.md) > [redirected](./kibana-plugin-server.onrequesttoolkit.redirected.md) - -## OnRequestToolkit.redirected property - -To interrupt request handling and redirect to a configured url - -Signature: - -```typescript -redirected: (url: string) => OnRequestResult; -``` diff --git a/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.rejected.md b/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.rejected.md deleted file mode 100644 index 447d9b3fb9be5..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.rejected.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnRequestToolkit](./kibana-plugin-server.onrequesttoolkit.md) > [rejected](./kibana-plugin-server.onrequesttoolkit.rejected.md) - -## OnRequestToolkit.rejected property - -Fail the request with specified error. - -Signature: - -```typescript -rejected: (error: Error, options?: { - statusCode?: number; - }) => OnRequestResult; -``` diff --git a/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.seturl.md b/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.seturl.md deleted file mode 100644 index 0f20cbdb18d96..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.seturl.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnRequestToolkit](./kibana-plugin-server.onrequesttoolkit.md) > [setUrl](./kibana-plugin-server.onrequesttoolkit.seturl.md) - -## OnRequestToolkit.setUrl property - -Change url for an incoming request. - -Signature: - -```typescript -setUrl: (newUrl: string | Url) => void; -``` diff --git a/docs/development/core/server/kibana-plugin-server.plugin.md b/docs/development/core/server/kibana-plugin-server.plugin.md index 6743d031647e8..035aa16014942 100644 --- a/docs/development/core/server/kibana-plugin-server.plugin.md +++ b/docs/development/core/server/kibana-plugin-server.plugin.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Plugin](./kibana-plugin-server.plugin.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Plugin](./kibana-plugin-server.plugin.md) ## Plugin interface @@ -16,7 +16,7 @@ export interface Plugin(core: PluginSetupContext, plugins: TPluginsSetup) => TSetup | Promise<TSetup> | | -| [start](./kibana-plugin-server.plugin.start.md) | (core: PluginStartContext, plugins: TPluginsStart) => TStart | Promise<TStart> | | +| [setup](./kibana-plugin-server.plugin.setup.md) | (core: CoreSetup, plugins: TPluginsSetup) => TSetup | Promise<TSetup> | | +| [start](./kibana-plugin-server.plugin.start.md) | (core: CoreStart, plugins: TPluginsStart) => TStart | Promise<TStart> | | | [stop](./kibana-plugin-server.plugin.stop.md) | () => void | | diff --git a/docs/development/core/server/kibana-plugin-server.plugin.setup.md b/docs/development/core/server/kibana-plugin-server.plugin.setup.md index 19845912c4eaf..e11c1f331b956 100644 --- a/docs/development/core/server/kibana-plugin-server.plugin.setup.md +++ b/docs/development/core/server/kibana-plugin-server.plugin.setup.md @@ -1,11 +1,11 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Plugin](./kibana-plugin-server.plugin.md) > [setup](./kibana-plugin-server.plugin.setup.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Plugin](./kibana-plugin-server.plugin.md) > [setup](./kibana-plugin-server.plugin.setup.md) ## Plugin.setup property Signature: ```typescript -setup: (core: PluginSetupContext, plugins: TPluginsSetup) => TSetup | Promise; +setup: (core: CoreSetup, plugins: TPluginsSetup) => TSetup | Promise; ``` diff --git a/docs/development/core/server/kibana-plugin-server.plugin.start.md b/docs/development/core/server/kibana-plugin-server.plugin.start.md index b9798632f5925..ff5d81b5513a4 100644 --- a/docs/development/core/server/kibana-plugin-server.plugin.start.md +++ b/docs/development/core/server/kibana-plugin-server.plugin.start.md @@ -1,11 +1,11 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Plugin](./kibana-plugin-server.plugin.md) > [start](./kibana-plugin-server.plugin.start.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Plugin](./kibana-plugin-server.plugin.md) > [start](./kibana-plugin-server.plugin.start.md) ## Plugin.start property Signature: ```typescript -start: (core: PluginStartContext, plugins: TPluginsStart) => TStart | Promise; +start: (core: CoreStart, plugins: TPluginsStart) => TStart | Promise; ``` diff --git a/docs/development/core/server/kibana-plugin-server.plugin.stop.md b/docs/development/core/server/kibana-plugin-server.plugin.stop.md index 51260096b5966..00dbc824627bf 100644 --- a/docs/development/core/server/kibana-plugin-server.plugin.stop.md +++ b/docs/development/core/server/kibana-plugin-server.plugin.stop.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Plugin](./kibana-plugin-server.plugin.md) > [stop](./kibana-plugin-server.plugin.stop.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Plugin](./kibana-plugin-server.plugin.md) > [stop](./kibana-plugin-server.plugin.stop.md) ## Plugin.stop property diff --git a/docs/development/core/server/kibana-plugin-server.plugininitializer.md b/docs/development/core/server/kibana-plugin-server.plugininitializer.md index b4f31e0eddbf1..402b4001ce633 100644 --- a/docs/development/core/server/kibana-plugin-server.plugininitializer.md +++ b/docs/development/core/server/kibana-plugin-server.plugininitializer.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginInitializer](./kibana-plugin-server.plugininitializer.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginInitializer](./kibana-plugin-server.plugininitializer.md) ## PluginInitializer type diff --git a/docs/development/core/server/kibana-plugin-server.plugininitializercontext.config.md b/docs/development/core/server/kibana-plugin-server.plugininitializercontext.config.md index fb4fddf584f43..7c92784c6c38a 100644 --- a/docs/development/core/server/kibana-plugin-server.plugininitializercontext.config.md +++ b/docs/development/core/server/kibana-plugin-server.plugininitializercontext.config.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginInitializerContext](./kibana-plugin-server.plugininitializercontext.md) > [config](./kibana-plugin-server.plugininitializercontext.config.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginInitializerContext](./kibana-plugin-server.plugininitializercontext.md) > [config](./kibana-plugin-server.plugininitializercontext.config.md) ## PluginInitializerContext.config property @@ -8,7 +8,7 @@ ```typescript config: { - create: , Config>(ConfigClass: ConfigWithSchema) => Observable; - createIfExists: , Config>(ConfigClass: ConfigWithSchema) => Observable; + create: () => Observable; + createIfExists: () => Observable; }; ``` diff --git a/docs/development/core/server/kibana-plugin-server.plugininitializercontext.env.md b/docs/development/core/server/kibana-plugin-server.plugininitializercontext.env.md index bffd38360d716..fde398faf132e 100644 --- a/docs/development/core/server/kibana-plugin-server.plugininitializercontext.env.md +++ b/docs/development/core/server/kibana-plugin-server.plugininitializercontext.env.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginInitializerContext](./kibana-plugin-server.plugininitializercontext.md) > [env](./kibana-plugin-server.plugininitializercontext.env.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginInitializerContext](./kibana-plugin-server.plugininitializercontext.md) > [env](./kibana-plugin-server.plugininitializercontext.env.md) ## PluginInitializerContext.env property diff --git a/docs/development/core/server/kibana-plugin-server.plugininitializercontext.logger.md b/docs/development/core/server/kibana-plugin-server.plugininitializercontext.logger.md index 6d8803aafceea..688560f324d17 100644 --- a/docs/development/core/server/kibana-plugin-server.plugininitializercontext.logger.md +++ b/docs/development/core/server/kibana-plugin-server.plugininitializercontext.logger.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginInitializerContext](./kibana-plugin-server.plugininitializercontext.md) > [logger](./kibana-plugin-server.plugininitializercontext.logger.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginInitializerContext](./kibana-plugin-server.plugininitializercontext.md) > [logger](./kibana-plugin-server.plugininitializercontext.logger.md) ## PluginInitializerContext.logger property diff --git a/docs/development/core/server/kibana-plugin-server.plugininitializercontext.md b/docs/development/core/server/kibana-plugin-server.plugininitializercontext.md index 6e35d28e2ef8f..eeeccdf56f72b 100644 --- a/docs/development/core/server/kibana-plugin-server.plugininitializercontext.md +++ b/docs/development/core/server/kibana-plugin-server.plugininitializercontext.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginInitializerContext](./kibana-plugin-server.plugininitializercontext.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginInitializerContext](./kibana-plugin-server.plugininitializercontext.md) ## PluginInitializerContext interface @@ -16,7 +16,7 @@ export interface PluginInitializerContext | Property | Type | Description | | --- | --- | --- | -| [config](./kibana-plugin-server.plugininitializercontext.config.md) | {`

` create: <Schema extends Type<any>, Config>(ConfigClass: ConfigWithSchema<Schema, Config>) => Observable<Config>;`

` createIfExists: <Schema extends Type<any>, Config>(ConfigClass: ConfigWithSchema<Schema, Config>) => Observable<Config | undefined>;`

` } | | -| [env](./kibana-plugin-server.plugininitializercontext.env.md) | {`

` mode: EnvironmentMode;`

` } | | +| [config](./kibana-plugin-server.plugininitializercontext.config.md) | {
create: <Schema>() => Observable<Schema>;
createIfExists: <Schema>() => Observable<Schema | undefined>;
} | | +| [env](./kibana-plugin-server.plugininitializercontext.env.md) | {
mode: EnvironmentMode;
} | | | [logger](./kibana-plugin-server.plugininitializercontext.logger.md) | LoggerFactory | | diff --git a/docs/development/core/server/kibana-plugin-server.pluginname.md b/docs/development/core/server/kibana-plugin-server.pluginname.md index 07b37fa66b41d..02121c10d6b1d 100644 --- a/docs/development/core/server/kibana-plugin-server.pluginname.md +++ b/docs/development/core/server/kibana-plugin-server.pluginname.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginName](./kibana-plugin-server.pluginname.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginName](./kibana-plugin-server.pluginname.md) ## PluginName type diff --git a/docs/development/core/server/kibana-plugin-server.pluginsetupcontext.elasticsearch.md b/docs/development/core/server/kibana-plugin-server.pluginsetupcontext.elasticsearch.md deleted file mode 100644 index 34d35465fb816..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.pluginsetupcontext.elasticsearch.md +++ /dev/null @@ -1,14 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginSetupContext](./kibana-plugin-server.pluginsetupcontext.md) > [elasticsearch](./kibana-plugin-server.pluginsetupcontext.elasticsearch.md) - -## PluginSetupContext.elasticsearch property - -Signature: - -```typescript -elasticsearch: { - adminClient$: Observable; - dataClient$: Observable; - }; -``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginsetupcontext.http.md b/docs/development/core/server/kibana-plugin-server.pluginsetupcontext.http.md deleted file mode 100644 index 0ca7fa2a88294..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.pluginsetupcontext.http.md +++ /dev/null @@ -1,16 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginSetupContext](./kibana-plugin-server.pluginsetupcontext.md) > [http](./kibana-plugin-server.pluginsetupcontext.http.md) - -## PluginSetupContext.http property - -Signature: - -```typescript -http: { - registerAuth: HttpServiceSetup['registerAuth']; - registerOnRequest: HttpServiceSetup['registerOnRequest']; - getBasePathFor: HttpServiceSetup['getBasePathFor']; - setBasePathFor: HttpServiceSetup['setBasePathFor']; - }; -``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginsetupcontext.md b/docs/development/core/server/kibana-plugin-server.pluginsetupcontext.md deleted file mode 100644 index 8878edb18230f..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.pluginsetupcontext.md +++ /dev/null @@ -1,21 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginSetupContext](./kibana-plugin-server.pluginsetupcontext.md) - -## PluginSetupContext interface - -Context passed to the plugins `setup` method. - -Signature: - -```typescript -export interface PluginSetupContext -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [elasticsearch](./kibana-plugin-server.pluginsetupcontext.elasticsearch.md) | {`

` adminClient$: Observable<ClusterClient>;`

` dataClient$: Observable<ClusterClient>;`

` } | | -| [http](./kibana-plugin-server.pluginsetupcontext.http.md) | {`

` registerAuth: HttpServiceSetup['registerAuth'];`

` registerOnRequest: HttpServiceSetup['registerOnRequest'];`

` getBasePathFor: HttpServiceSetup['getBasePathFor'];`

` setBasePathFor: HttpServiceSetup['setBasePathFor'];`

` } | | - diff --git a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.contracts.md b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.contracts.md new file mode 100644 index 0000000000000..90eb5daade31f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.contracts.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginsServiceSetup](./kibana-plugin-server.pluginsservicesetup.md) > [contracts](./kibana-plugin-server.pluginsservicesetup.contracts.md) + +## PluginsServiceSetup.contracts property + +Signature: + +```typescript +contracts: Map; +``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.md b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.md new file mode 100644 index 0000000000000..2b3ff9a2cd419 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginsServiceSetup](./kibana-plugin-server.pluginsservicesetup.md) + +## PluginsServiceSetup interface + + +Signature: + +```typescript +export interface PluginsServiceSetup +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [contracts](./kibana-plugin-server.pluginsservicesetup.contracts.md) | Map<PluginName, unknown> | | +| [uiPlugins](./kibana-plugin-server.pluginsservicesetup.uiplugins.md) | {
public: Map<PluginName, DiscoveredPlugin>;
internal: Map<PluginName, DiscoveredPluginInternal>;
} | | + diff --git a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uiplugins.md b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uiplugins.md new file mode 100644 index 0000000000000..fa286dfb59092 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uiplugins.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginsServiceSetup](./kibana-plugin-server.pluginsservicesetup.md) > [uiPlugins](./kibana-plugin-server.pluginsservicesetup.uiplugins.md) + +## PluginsServiceSetup.uiPlugins property + +Signature: + +```typescript +uiPlugins: { + public: Map; + internal: Map; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginsservicestart.contracts.md b/docs/development/core/server/kibana-plugin-server.pluginsservicestart.contracts.md new file mode 100644 index 0000000000000..694ca647883bd --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginsservicestart.contracts.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginsServiceStart](./kibana-plugin-server.pluginsservicestart.md) > [contracts](./kibana-plugin-server.pluginsservicestart.contracts.md) + +## PluginsServiceStart.contracts property + +Signature: + +```typescript +contracts: Map; +``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginsservicestart.md b/docs/development/core/server/kibana-plugin-server.pluginsservicestart.md new file mode 100644 index 0000000000000..4ac66afbd7a3e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginsservicestart.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginsServiceStart](./kibana-plugin-server.pluginsservicestart.md) + +## PluginsServiceStart interface + + +Signature: + +```typescript +export interface PluginsServiceStart +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [contracts](./kibana-plugin-server.pluginsservicestart.contracts.md) | Map<PluginName, unknown> | | + diff --git a/docs/development/core/server/kibana-plugin-server.pluginstartcontext.md b/docs/development/core/server/kibana-plugin-server.pluginstartcontext.md deleted file mode 100644 index 5ff2b9f16198f..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.pluginstartcontext.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginStartContext](./kibana-plugin-server.pluginstartcontext.md) - -## PluginStartContext interface - -Context passed to the plugins `start` method. - -Signature: - -```typescript -export interface PluginStartContext -``` diff --git a/docs/development/core/server/kibana-plugin-server.recursivereadonly.md b/docs/development/core/server/kibana-plugin-server.recursivereadonly.md new file mode 100644 index 0000000000000..562fb9131c7bb --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.recursivereadonly.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RecursiveReadonly](./kibana-plugin-server.recursivereadonly.md) + +## RecursiveReadonly type + + +Signature: + +```typescript +export declare type RecursiveReadonly = T extends (...args: any[]) => any ? T : T extends any[] ? RecursiveReadonlyArray : T extends object ? Readonly<{ + [K in keyof T]: RecursiveReadonly; +}> : T; +``` diff --git a/docs/development/core/server/kibana-plugin-server.routeconfigoptions.authrequired.md b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.authrequired.md new file mode 100644 index 0000000000000..3fb4426c407cd --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.authrequired.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md) > [authRequired](./kibana-plugin-server.routeconfigoptions.authrequired.md) + +## RouteConfigOptions.authRequired property + +A flag shows that authentication for a route: enabled when true disabled when false + +Enabled by default. + +Signature: + +```typescript +authRequired?: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md new file mode 100644 index 0000000000000..c2a64d018d321 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md) + +## RouteConfigOptions interface + +Route specific configuration. + +Signature: + +```typescript +export interface RouteConfigOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [authRequired](./kibana-plugin-server.routeconfigoptions.authrequired.md) | boolean | A flag shows that authentication for a route: enabled when true disabled when falseEnabled by default. | +| [tags](./kibana-plugin-server.routeconfigoptions.tags.md) | ReadonlyArray<string> | Additional metadata tag strings to attach to the route. | + diff --git a/docs/development/core/server/kibana-plugin-server.routeconfigoptions.tags.md b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.tags.md new file mode 100644 index 0000000000000..f3c7d986b8c0b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.tags.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md) > [tags](./kibana-plugin-server.routeconfigoptions.tags.md) + +## RouteConfigOptions.tags property + +Additional metadata tag strings to attach to the route. + +Signature: + +```typescript +tags?: ReadonlyArray; +``` diff --git a/docs/development/core/server/kibana-plugin-server.routemethod.md b/docs/development/core/server/kibana-plugin-server.routemethod.md new file mode 100644 index 0000000000000..dd1a050708bb3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routemethod.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteMethod](./kibana-plugin-server.routemethod.md) + +## RouteMethod type + +The set of common HTTP methods supported by Kibana routing. + +Signature: + +```typescript +export declare type RouteMethod = 'get' | 'post' | 'put' | 'delete'; +``` diff --git a/docs/development/core/server/kibana-plugin-server.router.(constructor).md b/docs/development/core/server/kibana-plugin-server.router.(constructor).md new file mode 100644 index 0000000000000..5f8e1e5e293ab --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.router.(constructor).md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [(constructor)](./kibana-plugin-server.router.(constructor).md) + +## Router.(constructor) + +Constructs a new instance of the `Router` class + +Signature: + +```typescript +constructor(path: string); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| path | string | | + diff --git a/docs/development/core/server/kibana-plugin-server.router.delete.md b/docs/development/core/server/kibana-plugin-server.router.delete.md index b98a2c24c090c..cd49f80baaf70 100644 --- a/docs/development/core/server/kibana-plugin-server.router.delete.md +++ b/docs/development/core/server/kibana-plugin-server.router.delete.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [delete](./kibana-plugin-server.router.delete.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [delete](./kibana-plugin-server.router.delete.md) ## Router.delete() method diff --git a/docs/development/core/server/kibana-plugin-server.router.get.md b/docs/development/core/server/kibana-plugin-server.router.get.md index b9c16eed73923..ab8e7c8c5a65d 100644 --- a/docs/development/core/server/kibana-plugin-server.router.get.md +++ b/docs/development/core/server/kibana-plugin-server.router.get.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [get](./kibana-plugin-server.router.get.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [get](./kibana-plugin-server.router.get.md) ## Router.get() method diff --git a/docs/development/core/server/kibana-plugin-server.router.getroutes.md b/docs/development/core/server/kibana-plugin-server.router.getroutes.md index 4599728a620ca..3e4785a3a7c6c 100644 --- a/docs/development/core/server/kibana-plugin-server.router.getroutes.md +++ b/docs/development/core/server/kibana-plugin-server.router.getroutes.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [getRoutes](./kibana-plugin-server.router.getroutes.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [getRoutes](./kibana-plugin-server.router.getroutes.md) ## Router.getRoutes() method diff --git a/docs/development/core/server/kibana-plugin-server.router.md b/docs/development/core/server/kibana-plugin-server.router.md index f30826c7413b1..52193bbc553c7 100644 --- a/docs/development/core/server/kibana-plugin-server.router.md +++ b/docs/development/core/server/kibana-plugin-server.router.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) ## Router class @@ -11,6 +11,12 @@ export declare class Router ``` +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(path)](./kibana-plugin-server.router.(constructor).md) | | Constructs a new instance of the Router class | + ## Properties | Property | Modifiers | Type | Description | diff --git a/docs/development/core/server/kibana-plugin-server.router.path.md b/docs/development/core/server/kibana-plugin-server.router.path.md index 050047593a8cf..bc799e6abfad5 100644 --- a/docs/development/core/server/kibana-plugin-server.router.path.md +++ b/docs/development/core/server/kibana-plugin-server.router.path.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [path](./kibana-plugin-server.router.path.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [path](./kibana-plugin-server.router.path.md) ## Router.path property diff --git a/docs/development/core/server/kibana-plugin-server.router.post.md b/docs/development/core/server/kibana-plugin-server.router.post.md index d815b58c46029..a499a46b1ee79 100644 --- a/docs/development/core/server/kibana-plugin-server.router.post.md +++ b/docs/development/core/server/kibana-plugin-server.router.post.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [post](./kibana-plugin-server.router.post.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [post](./kibana-plugin-server.router.post.md) ## Router.post() method diff --git a/docs/development/core/server/kibana-plugin-server.router.put.md b/docs/development/core/server/kibana-plugin-server.router.put.md index ebc62bb70fec0..7b1337279cca9 100644 --- a/docs/development/core/server/kibana-plugin-server.router.put.md +++ b/docs/development/core/server/kibana-plugin-server.router.put.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [put](./kibana-plugin-server.router.put.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [put](./kibana-plugin-server.router.put.md) ## Router.put() method diff --git a/docs/development/core/server/kibana-plugin-server.router.routes.md b/docs/development/core/server/kibana-plugin-server.router.routes.md index 2cccf62cdda78..c825bfe72d236 100644 --- a/docs/development/core/server/kibana-plugin-server.router.routes.md +++ b/docs/development/core/server/kibana-plugin-server.router.routes.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [routes](./kibana-plugin-server.router.routes.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [routes](./kibana-plugin-server.router.routes.md) ## Router.routes property diff --git a/docs/development/core/server/kibana-plugin-server.scopedclusterclient.(constructor).md b/docs/development/core/server/kibana-plugin-server.scopedclusterclient.(constructor).md new file mode 100644 index 0000000000000..94b49e43a113c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.scopedclusterclient.(constructor).md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ScopedClusterClient](./kibana-plugin-server.scopedclusterclient.md) > [(constructor)](./kibana-plugin-server.scopedclusterclient.(constructor).md) + +## ScopedClusterClient.(constructor) + +Constructs a new instance of the `ScopedClusterClient` class + +Signature: + +```typescript +constructor(internalAPICaller: APICaller, scopedAPICaller: APICaller, headers?: Record | undefined); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| internalAPICaller | APICaller | | +| scopedAPICaller | APICaller | | +| headers | Record<string, string | string[] | undefined> | undefined | | + diff --git a/docs/development/core/server/kibana-plugin-server.scopedclusterclient.callascurrentuser.md b/docs/development/core/server/kibana-plugin-server.scopedclusterclient.callascurrentuser.md index 65799bdd8185f..4e462612ba103 100644 --- a/docs/development/core/server/kibana-plugin-server.scopedclusterclient.callascurrentuser.md +++ b/docs/development/core/server/kibana-plugin-server.scopedclusterclient.callascurrentuser.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [ScopedClusterClient](./kibana-plugin-server.scopedclusterclient.md) > [callAsCurrentUser](./kibana-plugin-server.scopedclusterclient.callascurrentuser.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ScopedClusterClient](./kibana-plugin-server.scopedclusterclient.md) > [callAsCurrentUser](./kibana-plugin-server.scopedclusterclient.callascurrentuser.md) ## ScopedClusterClient.callAsCurrentUser() method diff --git a/docs/development/core/server/kibana-plugin-server.scopedclusterclient.callasinternaluser.md b/docs/development/core/server/kibana-plugin-server.scopedclusterclient.callasinternaluser.md index 79314b7da3e13..e79abfcf81db0 100644 --- a/docs/development/core/server/kibana-plugin-server.scopedclusterclient.callasinternaluser.md +++ b/docs/development/core/server/kibana-plugin-server.scopedclusterclient.callasinternaluser.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [ScopedClusterClient](./kibana-plugin-server.scopedclusterclient.md) > [callAsInternalUser](./kibana-plugin-server.scopedclusterclient.callasinternaluser.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ScopedClusterClient](./kibana-plugin-server.scopedclusterclient.md) > [callAsInternalUser](./kibana-plugin-server.scopedclusterclient.callasinternaluser.md) ## ScopedClusterClient.callAsInternalUser() method diff --git a/docs/development/core/server/kibana-plugin-server.scopedclusterclient.md b/docs/development/core/server/kibana-plugin-server.scopedclusterclient.md index b3afd06b8572f..5ad474c3ce909 100644 --- a/docs/development/core/server/kibana-plugin-server.scopedclusterclient.md +++ b/docs/development/core/server/kibana-plugin-server.scopedclusterclient.md @@ -1,6 +1,6 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [ScopedClusterClient](./kibana-plugin-server.scopedclusterclient.md) +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ScopedClusterClient](./kibana-plugin-server.scopedclusterclient.md) ## ScopedClusterClient class @@ -12,6 +12,12 @@ Serves the same purpose as "normal" `ClusterClient` but exposes additional `call export declare class ScopedClusterClient ``` +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(internalAPICaller, scopedAPICaller, headers)](./kibana-plugin-server.scopedclusterclient.(constructor).md) | | Constructs a new instance of the ScopedClusterClient class | + ## Methods | Method | Modifiers | Description | diff --git a/docs/development/core/server/kibana-plugin-server.sessionstorage.clear.md b/docs/development/core/server/kibana-plugin-server.sessionstorage.clear.md new file mode 100644 index 0000000000000..1f5813e181548 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.sessionstorage.clear.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SessionStorage](./kibana-plugin-server.sessionstorage.md) > [clear](./kibana-plugin-server.sessionstorage.clear.md) + +## SessionStorage.clear() method + +Clears current session. + +Signature: + +```typescript +clear(): void; +``` +Returns: + +`void` + diff --git a/docs/development/core/server/kibana-plugin-server.sessionstorage.get.md b/docs/development/core/server/kibana-plugin-server.sessionstorage.get.md new file mode 100644 index 0000000000000..26c63884ee71a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.sessionstorage.get.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SessionStorage](./kibana-plugin-server.sessionstorage.md) > [get](./kibana-plugin-server.sessionstorage.get.md) + +## SessionStorage.get() method + +Retrieves session value from the session storage. + +Signature: + +```typescript +get(): Promise; +``` +Returns: + +`Promise` + diff --git a/docs/development/core/server/kibana-plugin-server.sessionstorage.md b/docs/development/core/server/kibana-plugin-server.sessionstorage.md new file mode 100644 index 0000000000000..02e48c1dd3dc4 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.sessionstorage.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SessionStorage](./kibana-plugin-server.sessionstorage.md) + +## SessionStorage interface + +Provides an interface to store and retrieve data across requests. + +Signature: + +```typescript +export interface SessionStorage +``` + +## Methods + +| Method | Description | +| --- | --- | +| [clear()](./kibana-plugin-server.sessionstorage.clear.md) | Clears current session. | +| [get()](./kibana-plugin-server.sessionstorage.get.md) | Retrieves session value from the session storage. | +| [set(sessionValue)](./kibana-plugin-server.sessionstorage.set.md) | Puts current session value into the session storage. | + diff --git a/docs/development/core/server/kibana-plugin-server.sessionstorage.set.md b/docs/development/core/server/kibana-plugin-server.sessionstorage.set.md new file mode 100644 index 0000000000000..7e3a2a4361244 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.sessionstorage.set.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SessionStorage](./kibana-plugin-server.sessionstorage.md) > [set](./kibana-plugin-server.sessionstorage.set.md) + +## SessionStorage.set() method + +Puts current session value into the session storage. + +Signature: + +```typescript +set(sessionValue: T): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| sessionValue | T | value to put | + +Returns: + +`void` + diff --git a/docs/development/core/server/kibana-plugin-server.sessionstoragefactory.asscoped.md b/docs/development/core/server/kibana-plugin-server.sessionstoragefactory.asscoped.md new file mode 100644 index 0000000000000..ed107ae50899b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.sessionstoragefactory.asscoped.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SessionStorageFactory](./kibana-plugin-server.sessionstoragefactory.md) > [asScoped](./kibana-plugin-server.sessionstoragefactory.asscoped.md) + +## SessionStorageFactory.asScoped property + +Signature: + +```typescript +asScoped: (request: Readonly | KibanaRequest) => SessionStorage; +``` diff --git a/docs/development/core/server/kibana-plugin-server.sessionstoragefactory.md b/docs/development/core/server/kibana-plugin-server.sessionstoragefactory.md new file mode 100644 index 0000000000000..8f6f58902fde4 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.sessionstoragefactory.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SessionStorageFactory](./kibana-plugin-server.sessionstoragefactory.md) + +## SessionStorageFactory interface + +SessionStorage factory to bind one to an incoming request + +Signature: + +```typescript +export interface SessionStorageFactory +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [asScoped](./kibana-plugin-server.sessionstoragefactory.asscoped.md) | (request: Readonly<Request> | KibanaRequest) => SessionStorage<T> | | + diff --git a/docs/development/plugin-development.asciidoc b/docs/development/plugin-development.asciidoc index 5727c4186c8d0..691fdb0412fd2 100644 --- a/docs/development/plugin-development.asciidoc +++ b/docs/development/plugin-development.asciidoc @@ -8,6 +8,7 @@ The Kibana plugin interfaces are in a state of constant development. We cannot * <> * <> +* <> * <> * <> @@ -15,6 +16,8 @@ include::plugin/development-plugin-resources.asciidoc[] include::plugin/development-uiexports.asciidoc[] +include::plugin/development-plugin-feature-registration.asciidoc[] + include::plugin/development-plugin-functional-tests.asciidoc[] include::plugin/development-plugin-localization.asciidoc[] diff --git a/docs/development/plugin/development-plugin-feature-registration.asciidoc b/docs/development/plugin/development-plugin-feature-registration.asciidoc new file mode 100644 index 0000000000000..2babd020a8e25 --- /dev/null +++ b/docs/development/plugin/development-plugin-feature-registration.asciidoc @@ -0,0 +1,192 @@ +[[development-plugin-feature-registration]] +=== Plugin feature registration + +If your plugin will be used with {kib}’s default distribution, then you have the ability to register the features that your plugin provides. Features are typically apps in {kib}; once registered, you can toggle them via Spaces, and secure them via Roles when security is enabled. + +==== UI Capabilities + +Registering features also gives your plugin access to “UI Capabilities”. These capabilities are boolean flags that you can use to conditionally render your interface, based on the current user’s permissions. For example, you can hide or disable a Save button if the current user is not authorized. + +==== Registering a feature + +Feature registration is controlled via the built-in `xpack_main` plugin. To register a feature, call `xpack_main`'s `registerFeature` function from your plugin’s `init` function, and provide the appropriate details: + +["source","javascript"] +----------- +init(server) { + const xpackMainPlugin = server.plugins.xpack_main; + xpackMainPlugin.registerFeature({ + // feature details here. + }); +} +----------- + +===== Feature details +Registering a feature consists of the following fields. For more information, consult the {repo}blob/{branch}/x-pack/plugins/xpack_main/server/lib/feature_registry/feature_registry.ts[feature registry interface]. + + +[cols="1a, 1a, 1a, 1a"] +|=== +|Field name |Data type |Example |Description + +|`id` (required) +|`string` +|`"sample_feature"` +|A unique identifier for your feature. Usually, the ID of your plugin is sufficient. + +|`name` (required) +|`string` +|`"Sample Feature"` +|A human readable name for your feature. + +|`app` (required) +|`string[]` +|`["sample_app", "kibana"]` +|An array of applications this feature enables. Typically, all of your plugin’s apps (from `uiExports`) will be included here. + +|`privileges` (required) +|{repo}blob/{branch}/x-pack/plugins/xpack_main/server/lib/feature_registry/feature_registry.ts[`FeatureWithAllOrReadPrivileges`]. +|see examples below +|The set of privileges this feature requires to function. + +|`icon` +|`string` +|"discoverApp" +|An https://elastic.github.io/eui/#/display/icons[EUI Icon] to use for this feature. + +|`navLinkId` +|`string` +|"sample_app" +|The ID of the navigation link associated with your feature. +|=== + +===== Privilege definition +The `privileges` section of feature registration allows plugins to implement read/write and read-only modes for their applications. + +For a full explanation of fields and options, consult the {repo}blob/{branch}/x-pack/plugins/xpack_main/server/lib/feature_registry/feature_registry.ts[feature registry interface]. + +==== Using UI Capabilities + +UI Capabilities are available to your public (client) plugin code. These capabilities are read-only, and are used to inform the UI. This object is namespaced by feature id. For example, if your feature id is “foo”, then your UI Capabilities are stored at `uiCapabilities.foo`. +To access capabilities, import them from `ui/capabilities`: + +["source","javascript"] +----------- +import { uiCapabilities } from ‘ui/capabilities’; + +const canUserSave = uiCapabilities.foo.save; +if (canUserSave) { + // show save button +} +----------- + +==== Example 1: Canvas Application +["source","javascript"] +----------- +init(server) { + const xpackMainPlugin = server.plugins.xpack_main; + xpackMainPlugin.registerFeature({ + id: 'canvas', + name: 'Canvas', + icon: 'canvasApp', + navLinkId: 'canvas', + app: ['canvas', 'kibana'], + catalogue: ['canvas'], + privileges: { + all: { + savedObject: { + all: ['canvas-workpad'], + read: ['index-pattern'], + }, + ui: ['save'], + }, + read: { + savedObject: { + all: [], + read: ['index-pattern', 'canvas-workpad'], + }, + ui: [], + }, + }, + }); +} +----------- + +This shows how the Canvas application might register itself as a Kibana feature. +Note that it specifies different `savedObject` access levels for each privilege: + +- Users with read/write access (`all` privilege) need to be able to read/write `canvas-workpad` saved objects, and they need read-only access to `index-pattern` saved objects. +- Users with read-only access (`read` privilege) do not need to have read/write access to any saved objects, but instead get read-only access to `index-pattern` and `canvas-workpad` saved objects. + +Additionally, Canvas registers the `canvas` UI app and `canvas` catalogue entry. This tells Kibana that these entities are available for users with either the `read` or `all` privilege. + +The `all` privilege defines a single “save” UI Capability. To access this in the UI, Canvas could: + +["source","javascript"] +----------- +import { uiCapabilities } from ‘ui/capabilities’; + +const canUserSave = uiCapabilities.canvas.save; +if (canUserSave) { + // show save button +} +----------- + +Because the `read` privilege does not define the `save` capability, users with read-only access will have their `uiCapabilities.canvas.save` flag set to `false`. + +==== Example 2: Dev Tools + +["source","javascript"] +----------- +init(server) { + const xpackMainPlugin = server.plugins.xpack_main; + xpackMainPlugin.registerFeature({ + id: 'dev_tools', + name: i18n.translate('xpack.main.featureRegistry.devToolsFeatureName', { + defaultMessage: 'Dev Tools', + }), + icon: 'devToolsApp', + navLinkId: 'kibana:dev_tools', + app: ['kibana'], + catalogue: ['console', 'searchprofiler', 'grokdebugger'], + privileges: { + all: { + api: ['console'], + savedObject: { + all: [], + read: [], + }, + ui: ['show'], + }, + read: { + api: ['console'], + savedObject: { + all: [], + read: [], + }, + ui: ['show'], + }, + }, + privilegesTooltip: i18n.translate('xpack.main.featureRegistry.devToolsPrivilegesTooltip', { + defaultMessage: + 'User should also be granted the appropriate Elasticsearch cluster and index privileges', + }), + }); +} +----------- + +Unlike the Canvas example above, Dev Tools does not require access to any saved objects to function. Dev Tools does specify an API endpoint, however. When this is configured, the Security plugin will automatically authorize access to any server API route that is tagged with `access:console`, similar to the following: + +["source","javascript"] +----------- +server.route({ + path: '/api/console/proxy', + method: 'POST', + config: { + tags: ['access:console'], + handler: async (req, h) => { + // ... + } + } +}); +----------- diff --git a/docs/development/plugin/development-plugin-localization.asciidoc b/docs/development/plugin/development-plugin-localization.asciidoc index b215de82aa921..ff497ec40e30e 100644 --- a/docs/development/plugin/development-plugin-localization.asciidoc +++ b/docs/development/plugin/development-plugin-localization.asciidoc @@ -13,6 +13,7 @@ You must add a `translations` directory at the root of your plugin. This directo . ├── translations │ ├── en.json +│ ├── ja-JP.json │ └── zh-CN.json └── .i18nrc.json ----------- @@ -40,7 +41,8 @@ To use Kibana i18n tooling, create a `.i18nrc.json` file with the following conf "exclude": [ ], "translations": [ - "translations/zh-CN.json" + "translations/zh-CN.json", + "translations/ja-JP.json" ] } ----------- @@ -107,7 +109,7 @@ export const HELLO_WORLD = i18n.translate('hello.wonderful.world', { }); ----------- -Full details are {repo}tree/6.7/packages/kbn-i18n#vanilla-js[here]. +Full details are {repo}tree/master/packages/kbn-i18n#vanilla-js[here]. [float] ===== i18n for React @@ -131,15 +133,14 @@ export const Component = () => { }; ----------- -Full details are {repo}tree/6.7/packages/kbn-i18n#react[here]. +Full details are {repo}tree/master/packages/kbn-i18n#react[here]. [float] ===== i18n for Angular -AngularJS wrapper has 4 entities: translation `provider`, `service`, `directive` and `filter`. Both the directive and the filter use the translation `service` with i18n engine under the hood. - +You are encouraged to use `i18n.translate()` by statically importing `i18n` from `@kbn/i18n` wherever possible in your Angular code. Angular wrappers use the translation `service` with the i18n engine under the hood. The translation directive has the following syntax: ["source","js"] @@ -152,7 +153,7 @@ The translation directive has the following syntax: > ----------- -Full details are {repo}tree/6.7/packages/kbn-i18n#angularjs[here]. +Full details are {repo}tree/master/packages/kbn-i18n#angularjs[here]. [float] diff --git a/docs/development/security/rbac.asciidoc b/docs/development/security/rbac.asciidoc index 5dba824f73d2c..b967dabf0684f 100644 --- a/docs/development/security/rbac.asciidoc +++ b/docs/development/security/rbac.asciidoc @@ -70,7 +70,7 @@ The application is created by concatenating the prefix of `kibana-` with the val } ---------------------------------- -Roles that grant <> should be managed using the <> or the *Management* / *Security* / *Roles* page, not directly using the {es} {ref}/security-api.html#security-role-apis[role management API]. This role can then be assigned to users using the {es} +Roles that grant <> should be managed using the <> or the *Management -> Security -> Roles* page, not directly using the {es} {ref}/security-api.html#security-role-apis[role management API]. This role can then be assigned to users using the {es} {ref}/security-api.html#security-user-apis[user management APIs]. [[development-rbac-authorization]] diff --git a/docs/discover/images/read-only-badge.png b/docs/discover/images/read-only-badge.png new file mode 100644 index 0000000000000..b46b9576fa3ff Binary files /dev/null and b/docs/discover/images/read-only-badge.png differ diff --git a/docs/discover/search.asciidoc b/docs/discover/search.asciidoc index 4511264278df3..9286b3ce31ba5 100644 --- a/docs/discover/search.asciidoc +++ b/docs/discover/search.asciidoc @@ -60,6 +60,16 @@ Saving searches enables you to reload them into Discover and use them as the bas for <>. Saving a search saves both the search query string and the currently selected index pattern. +[role="xpack"] +[[discover-read-only-access]] +==== Read only access +When you have insufficient privileges to save searches, the following indicator in Kibana will be +displayed and the *Save* button won't be visible. For more information on granting access to +Kibana see <>. + +[role="screenshot"] +image::discover/images/read-only-badge.png[Example of Discover's read only access indicator in Kibana's header] + ==== Saving a Search To save the current search: diff --git a/docs/graph/configuring-graph.asciidoc b/docs/graph/configuring-graph.asciidoc index 78ac70ea1a0f1..e012e5a30566b 100644 --- a/docs/graph/configuring-graph.asciidoc +++ b/docs/graph/configuring-graph.asciidoc @@ -21,8 +21,8 @@ might be deleted after a workspace is saved, there's no practical basis for checking permissions for the data in a saved workspace. For this reason, you can configure the save policy for graph workspaces to -ensure appropriate handling of your data. You can allow users to save -only the configuration information for a graph, require users to +ensure appropriate handling of your data. You can allow all users to save +only the configuration information for a graph, require all users to explicitly include the workspace data, or completely disable the ability to save a workspace. @@ -47,6 +47,16 @@ default policy. Only the configuration is saved unless the user explicitly selects the include data option. +[float] +=== Using Security to Grant Access +You can also use security to grant read only or all access to different roles. +When security is used to grant read only access, the following indicator in Kibana +will be displayed. For more information on granting access to Kibana see +<>. + +[role="screenshot"] +image::graph/images/graph-read-only-badge.png[Example of Graph's read only access indicator in Kibana's header] + [float] [[disable-drill-down]] === Disabling Drill Down Configuration diff --git a/docs/graph/images/graph-read-only-badge.png b/docs/graph/images/graph-read-only-badge.png new file mode 100644 index 0000000000000..a4d517cd5fe70 Binary files /dev/null and b/docs/graph/images/graph-read-only-badge.png differ diff --git a/docs/images/code-blame.png b/docs/images/code-blame.png new file mode 100644 index 0000000000000..c04e1b12c58f5 Binary files /dev/null and b/docs/images/code-blame.png differ diff --git a/docs/images/code-full-text-search.png b/docs/images/code-full-text-search.png new file mode 100644 index 0000000000000..4d4d3d21a24f6 Binary files /dev/null and b/docs/images/code-full-text-search.png differ diff --git a/docs/images/code-history.png b/docs/images/code-history.png new file mode 100644 index 0000000000000..853fcc8fa6adc Binary files /dev/null and b/docs/images/code-history.png differ diff --git a/docs/images/code-import-repo.png b/docs/images/code-import-repo.png new file mode 100644 index 0000000000000..5d8cf60336b38 Binary files /dev/null and b/docs/images/code-import-repo.png differ diff --git a/docs/images/code-lang-server-status.png b/docs/images/code-lang-server-status.png new file mode 100644 index 0000000000000..f1fd0263558be Binary files /dev/null and b/docs/images/code-lang-server-status.png differ diff --git a/docs/images/code-lang-server-tab.png b/docs/images/code-lang-server-tab.png new file mode 100644 index 0000000000000..bb4a6a15444aa Binary files /dev/null and b/docs/images/code-lang-server-tab.png differ diff --git a/docs/images/code-quick-search.png b/docs/images/code-quick-search.png new file mode 100644 index 0000000000000..e6274b77dcbaa Binary files /dev/null and b/docs/images/code-quick-search.png differ diff --git a/docs/images/code-repo-management.png b/docs/images/code-repo-management.png new file mode 100644 index 0000000000000..6344bffc9f9f7 Binary files /dev/null and b/docs/images/code-repo-management.png differ diff --git a/docs/images/code-search-filter.png b/docs/images/code-search-filter.png new file mode 100644 index 0000000000000..f9b09ad2d71ec Binary files /dev/null and b/docs/images/code-search-filter.png differ diff --git a/docs/images/code-semantic-nav.png b/docs/images/code-semantic-nav.png new file mode 100644 index 0000000000000..664225804ce22 Binary files /dev/null and b/docs/images/code-semantic-nav.png differ diff --git a/docs/images/code-starter-root.png b/docs/images/code-starter-root.png new file mode 100644 index 0000000000000..a2e3a579fe2f2 Binary files /dev/null and b/docs/images/code-starter-root.png differ diff --git a/docs/images/code-symbol-table.png b/docs/images/code-symbol-table.png new file mode 100644 index 0000000000000..d3bb4c6eef0c1 Binary files /dev/null and b/docs/images/code-symbol-table.png differ diff --git a/docs/images/dashboard-read-only-badge.png b/docs/images/dashboard-read-only-badge.png new file mode 100644 index 0000000000000..b61d587f7ef1d Binary files /dev/null and b/docs/images/dashboard-read-only-badge.png differ diff --git a/docs/images/management-index-read-only-badge.png b/docs/images/management-index-read-only-badge.png new file mode 100644 index 0000000000000..54e685894f5e1 Binary files /dev/null and b/docs/images/management-index-read-only-badge.png differ diff --git a/docs/images/security_base_all.png b/docs/images/security_base_all.png new file mode 100644 index 0000000000000..2aef42132ef21 Binary files /dev/null and b/docs/images/security_base_all.png differ diff --git a/docs/images/settings-read-only-badge.png b/docs/images/settings-read-only-badge.png new file mode 100644 index 0000000000000..449d5199ccb16 Binary files /dev/null and b/docs/images/settings-read-only-badge.png differ diff --git a/docs/images/spaces_secure_all_privileges.png b/docs/images/spaces_secure_all_privileges.png deleted file mode 100644 index 0bafffbb81b09..0000000000000 Binary files a/docs/images/spaces_secure_all_privileges.png and /dev/null differ diff --git a/docs/images/spaces_secure_specific_spaces.png b/docs/images/spaces_secure_specific_spaces.png deleted file mode 100644 index 8f7228f3f4221..0000000000000 Binary files a/docs/images/spaces_secure_specific_spaces.png and /dev/null differ diff --git a/docs/images/timelion-read-only-badge.png b/docs/images/timelion-read-only-badge.png new file mode 100644 index 0000000000000..19ffbfed6335a Binary files /dev/null and b/docs/images/timelion-read-only-badge.png differ diff --git a/docs/index.asciidoc b/docs/index.asciidoc index dd29e95d794a4..a07922b0af113 100644 --- a/docs/index.asciidoc +++ b/docs/index.asciidoc @@ -44,6 +44,8 @@ include::ml/index.asciidoc[] include::maps/index.asciidoc[] +include::code/index.asciidoc[] + include::infrastructure/index.asciidoc[] include::logs/index.asciidoc[] diff --git a/docs/infrastructure/images/read-only-badge.png b/docs/infrastructure/images/read-only-badge.png new file mode 100644 index 0000000000000..7911a21e4985f Binary files /dev/null and b/docs/infrastructure/images/read-only-badge.png differ diff --git a/docs/infrastructure/index.asciidoc b/docs/infrastructure/index.asciidoc index f0e6e01ebd739..c841fd73c14a9 100644 --- a/docs/infrastructure/index.asciidoc +++ b/docs/infrastructure/index.asciidoc @@ -59,6 +59,18 @@ changes performed via Configure source are specific to that space. You can therefore easily make different subsets of the data available by creating multiple spaces with different data source configurations. +[float] +[[infra-read-only-access]] +=== Read only access +When you have insufficient privileges to change the source configuration, the following +indicator in Kibana will be displayed. The buttons to change the source configuration +won't be visible. For more information on granting access to +Kibana see <>. + +[role="screenshot"] +image::infrastructure/images/read-only-badge.png[Example of Infrastructure's read only access indicator in Kibana's header] + + [float] === Configuration file diff --git a/docs/logs/images/read-only-badge.png b/docs/logs/images/read-only-badge.png new file mode 100644 index 0000000000000..ab7cc296477dc Binary files /dev/null and b/docs/logs/images/read-only-badge.png differ diff --git a/docs/logs/index.asciidoc b/docs/logs/index.asciidoc index 4a4a864adba30..f28c13963a750 100644 --- a/docs/logs/index.asciidoc +++ b/docs/logs/index.asciidoc @@ -59,6 +59,17 @@ changes performed via Configure source are specific to that space. You can therefore easily make different subsets of the data available by creating multiple spaces with different data source configurations. +[float] +[[logs-read-only-access]] +=== Read only access +When you have insufficient privileges to change the source configuration, the following +indicator in Kibana will be displayed. The buttons to change the source configuration +won't be visible. For more information on granting access to +Kibana see <>. + +[role="screenshot"] +image::logs/images/read-only-badge.png[Example of Logs' read only access indicator in Kibana's header] + [float] === Configuration file diff --git a/docs/management.asciidoc b/docs/management.asciidoc index a042aa351a57d..0b1ac4c8ae624 100644 --- a/docs/management.asciidoc +++ b/docs/management.asciidoc @@ -41,5 +41,6 @@ include::management/managing-beats.asciidoc[] include::management/managing-remote-clusters.asciidoc[] +include::management/snapshot-restore/index.asciidoc[] diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index ccf1d3428f175..6fa0c1d8da972 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -9,7 +9,19 @@ for displayed decimal values. . Scroll or search for the setting you want to modify. . Enter a new value for the setting. +[float] +[[settings-read-only-access]] +=== [xpack]#Read only access# +When you have insufficient privileges to edit advanced settings, the following +indicator in Kibana will be displayed. The buttons to edit settings won't be visible. +For more information on granting access to Kibana see <>. + +[role="screenshot"] +image::images/settings-read-only-badge.png[Example of Advanced Settings Management's read only access indicator in Kibana's header] + +[float] [[kibana-settings-reference]] +=== Kibana settings reference WARNING: Modifying a setting can affect {kib} performance and cause problems that are diff --git a/docs/management/index-patterns.asciidoc b/docs/management/index-patterns.asciidoc index 29182b85ec268..efd2968aad277 100644 --- a/docs/management/index-patterns.asciidoc +++ b/docs/management/index-patterns.asciidoc @@ -10,6 +10,17 @@ you cannot search them. Use caution when modifying advanced options, as it's possible to set values that are incompatible with one another. * Configure Kibana for a production environment +[float] +[[index-patterns-read-only-access]] +=== [xpack]#Read only access# +When you have insufficient privileges to create or save index patterns, the following +indicator in Kibana will be displayed. The buttons to create new index patterns or save +existing index patterns won't be visible. For more information on granting access to +Kibana see <>. + +[role="screenshot"] +image::images/management-index-read-only-badge.png[Example of Index Pattern Management's read only access indicator in Kibana's header] + [float] [[settings-create-pattern]] == Creating an Index Pattern to Connect to Elasticsearch diff --git a/docs/management/snapshot-restore/images/create_snapshot.png b/docs/management/snapshot-restore/images/create_snapshot.png new file mode 100755 index 0000000000000..5e33eb7c3dcfc Binary files /dev/null and b/docs/management/snapshot-restore/images/create_snapshot.png differ diff --git a/docs/management/snapshot-restore/images/register_repo.png b/docs/management/snapshot-restore/images/register_repo.png new file mode 100755 index 0000000000000..784c8022fddb4 Binary files /dev/null and b/docs/management/snapshot-restore/images/register_repo.png differ diff --git a/docs/management/snapshot-restore/images/repository_list.png b/docs/management/snapshot-restore/images/repository_list.png new file mode 100755 index 0000000000000..f153dd555d442 Binary files /dev/null and b/docs/management/snapshot-restore/images/repository_list.png differ diff --git a/docs/management/snapshot-restore/images/snapshot_details.png b/docs/management/snapshot-restore/images/snapshot_details.png new file mode 100755 index 0000000000000..c98faad53dc69 Binary files /dev/null and b/docs/management/snapshot-restore/images/snapshot_details.png differ diff --git a/docs/management/snapshot-restore/images/verify_repository.png b/docs/management/snapshot-restore/images/verify_repository.png new file mode 100755 index 0000000000000..af25576e00c06 Binary files /dev/null and b/docs/management/snapshot-restore/images/verify_repository.png differ diff --git a/docs/management/snapshot-restore/index.asciidoc b/docs/management/snapshot-restore/index.asciidoc new file mode 100644 index 0000000000000..0008db578987d --- /dev/null +++ b/docs/management/snapshot-restore/index.asciidoc @@ -0,0 +1,135 @@ +[role="xpack"] +[[snapshot-repositories]] +== Snapshot repositories + +Use *Snapshot Repositories* to store backups of your +{es} indices and clusters. Snapshots are important because they provide +a copy of your data in case something goes wrong. If you need to roll +back to an older version of your data, you can restore a snapshot from the repository. + +Before using this feature, you should be familiar with how snapshots work. +{ref}/modules-snapshots.html[Snapshot and Restore] is a good source for +more detailed information. + +To get started, go to *Management > Elasticsearch > Snapshot Repositories*. You +begin with an overview of your repositories. You can then use the UI to +drill down into your repositories and snapshots. + +[role="screenshot"] +image:management/snapshot-restore/images/repository_list.png["Repository list"] + +[float] +=== Registering a repository + +You must have a registry before you can save your snapshots. If you don’t see a +repository in the list, navigate to *Register repository* to create one. + +[role="screenshot"] +image:management/snapshot-restore/images/register_repo.png["Register repository"] + +{kib} supports three types of repositories on startup: + +* *Shared file system.* Uses a shared file system to store the snapshots. +* *Read-only url.* Provides a read-only alternative to access snapshot data +in another repository. +* *Source-only.* Enables storage of minimal, source-only snapshots. +A source-only repository can take up to 50% less disk space. + +For more information on these repositories and the settings that you can +configure, see {ref}/modules-snapshots.html#_repositories[Repositories]. +To add support for additional types, see +{ref}/modules-snapshots.html#_repository_plugins[Repository plugins]. + +A best practice is to register a repository for each major version of +{es}. If you register the same snapshot repository with multiple clusters, +give only one cluster write access to the repository. All other clusters +connected to that repository should have read-only access. + +[float] +=== Creating a snapshot + +Use the Kibana <> to create your snapshots. The +{ref}//modules-snapshots.html#_snapshot[snapshot API] +takes the current state and data in your index or cluster and saves it to a +shared repository. + +The snapshot process is "smart." Your first snapshot is a complete copy of data. +All subsequent snapshots save the changes between the existing snapshots and +the new data. + +For an overview of the snapshots in your cluster, go to the *Snapshots* tab in *Snapshot Repositories*. +You can then drill down into the details for each snapshot. + +[role="screenshot"] +image:management/snapshot-restore/images/snapshot_details.png["Snapshot details"] + +[float] +=== Example: Register a shared file system repository + +This example shows how to register a shared file system repository +and store snapshots. + +[float] +==== Register the repository location + +You must register the location of the repository in the `path.repo` setting on +your master and data nodes. You can do this in one of two ways: + +* Edit your `elasticsearch.yml` to include the `path.repo` setting. + +* Pass the `path.repo` setting when you start Elasticsearch. ++ +`bin/elasticsearch -E path.repo=/tmp/es-backups` + +[float] +==== Register the repository + +Use *Snapshot Repositories* to register a repository. + + +. Go to *Management > Elasticsearch > Snapshot Repositories*. +. Click *Register a repository*. +. Enter a name for your repository. For example, `my_backup`. +. Set *Repository type* to Shared file system. ++ +[role="screenshot"] +image:management/snapshot-restore/images/register_repo.png["Register repository"] + +. Click *Next*. +. In *Location*, enter the path to the snapshot repository, `/tmp/es-backups`. +. In *Chunk size*, enter 100mb so that snapshot files are not bigger than that size. +. Use the defaults for all other fields. +. Click *Register*. ++ +Your new repository is listed on the *Repositories* tab. ++ +. Inspect the details for the repository. ++ +The repository currently doesn’t have any snapshots. ++ +. Click *Verify repository* to view the repository connection status. ++ +[role="screenshot"] +image:management/snapshot-restore/images/verify_repository.png["Verify repository"] + +[float] +==== Add a snapshot to the repository +Use the {ref}//modules-snapshots.html#_snapshot[snapshot API] to create a snapshot. + +. Go to *Dev Tools > Console*. +. Create the snapshot. ++ +In this example, the snapshot name is `2019-04-25_snapshot`. You can also +use {ref}//date-math-index-names.html[date math expression] for the snapshot name. ++ +[role="screenshot"] +image:management/snapshot-restore/images/create_snapshot.png["Create snapshot"] ++ +. Open *Snapshot Repositories*. ++ +Your new snapshot is available in the *Snapshots* tab. + + + + + diff --git a/docs/maps/connect-to-ems.asciidoc b/docs/maps/connect-to-ems.asciidoc index cad845cf82958..75018e6dd4d97 100644 --- a/docs/maps/connect-to-ems.asciidoc +++ b/docs/maps/connect-to-ems.asciidoc @@ -4,14 +4,10 @@ https://www.elastic.co/elastic-maps-service[Elastic Maps Service (EMS)] is a service that hosts tile layers and vector shapes of administrative boundaries. -If you are using Kibana's out-of-the box settings, the **Maps** application is already configured to use EMS. +If you are using Kibana's out-of-the-box settings, the **Maps** application is already configured to use EMS. -Requests to EMS are made with two methods: - -* Requests for tile layer meta data, vector shapes meta data, and vector shapes are proxied through the Kibana server. -* Requests for EMS tiles are made directly from the browser to EMS. - -NOTE: The legacy tilemap and regionmap visualizations make requests directly from the browser to EMS. +The **Maps** application makes requests directly from the browser to EMS. +To proxy EMS requests through the Kibana server, set `map.proxyElasticMapsServiceInMaps` to `true` in your <> file. [float] diff --git a/docs/ml/creating-jobs.asciidoc b/docs/ml/creating-jobs.asciidoc index 181de3ea97b68..d88df4d8e14e8 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 95d3f55da21db..a7571be6d70fd 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/monitoring/cluster-alerts.asciidoc b/docs/monitoring/cluster-alerts.asciidoc index fb7d0c31c5189..2aab7c38b52e6 100644 --- a/docs/monitoring/cluster-alerts.asciidoc +++ b/docs/monitoring/cluster-alerts.asciidoc @@ -17,6 +17,8 @@ different versions in the same cluster. versions running against the same {es} cluster. * Logstash Version Mismatch. You have Logstash nodes with different versions reporting stats to the same monitoring cluster. +* {es} Nodes Changed. You have {es} nodes that were recently added or removed. +* {es} License Expiration. The cluster's license is about to expire. + -- If you do not preserve the data directory when upgrading a {kib} or diff --git a/docs/monitoring/monitoring-metricbeat.asciidoc b/docs/monitoring/monitoring-metricbeat.asciidoc index f009710792922..53a9a98751657 100644 --- a/docs/monitoring/monitoring-metricbeat.asciidoc +++ b/docs/monitoring/monitoring-metricbeat.asciidoc @@ -15,9 +15,12 @@ image::monitoring/images/metricbeat.png[Example monitoring architecture] To learn about monitoring in general, see {stack-ov}/xpack-monitoring.html[Monitoring the {stack}]. +//NOTE: The tagged regions are re-used in the Stack Overview. + . Disable the default collection of {kib} monitoring metrics. + + -- +// tag::disable-kibana-collection[] Add the following setting in the {kib} configuration file (`kibana.yml`): [source,yaml] @@ -26,7 +29,7 @@ xpack.monitoring.kibana.collection.enabled: false ---------------------------------- Leave the `xpack.monitoring.enabled` set to its default value (`true`). - +// end::disable-kibana-collection[] For more information, see <>. -- @@ -79,12 +82,13 @@ For more information, see {ref}/monitoring-settings.html[Monitoring settings in and {ref}/cluster-update-settings.html[Cluster update settings]. -- -. {metricbeat-ref}/metricbeat-installation.html[Install {metricbeat}] on the +. {metricbeat-ref}/metricbeat-installation.html[Install {metricbeat}] on the same server as {kib}. -. Enable the {kib} module in {metricbeat}. + +. Enable the {kib} {xpack} module in {metricbeat}. + + -- +// tag::enable-kibana-module[] For example, to enable the default configuration in the `modules.d` directory, run the following command: @@ -96,14 +100,38 @@ metricbeat modules enable kibana-xpack For more information, see {metricbeat-ref}/configuration-metricbeat.html[Specify which modules to run] and {metricbeat-ref}/metricbeat-module-kibana.html[{kib} module]. +// end::enable-kibana-module[] +-- + +. Configure the {kib} {xpack} module in {metricbeat}. + ++ -- +// tag::configure-kibana-module[] +The `modules.d/kibana-xpack.yml` file contains the following settings: -. By default the module will collect {kib} monitoring metrics from `http://localhost:5601`. -If the local {kib} instance has a different address, you must specify it via the `hosts` setting -in the `modules.d/kibana-xpack.yml` file. +[source,yaml] +---------------------------------- +- module: kibana + metricsets: + - stats + period: 10s + hosts: ["localhost:5601"] + #basepath: "" + #username: "user" + #password: "secret" + xpack.enabled: true +---------------------------------- -. If the Elastic {security-features} are enabled, you must also provide a user -ID and password so that {metricbeat} can collect metrics successfully. +By default, the module collects {kib} monitoring metrics from `localhost:5601`. +If that host and port number are not correct, you must update the `hosts` +setting. If you configured {kib} to use encrypted communications, you must +access it via HTTPS. For example, use a `hosts` setting like +`https://localhost:5601`. +// end::configure-kibana-module[] + +// tag::remote-monitoring-user[] +If the Elastic {security-features} are enabled, you must also provide a user +ID and password so that {metricbeat} can collect metrics successfully: .. Create a user on the production cluster that has the `remote_monitoring_collector` {stack-ov}/built-in-roles.html[built-in role]. @@ -112,22 +140,24 @@ Alternatively, use the `remote_monitoring_user` .. Add the `username` and `password` settings to the {kib} module configuration file. -+ +// end::remote-monitoring-user[] -- -For example, add the following settings in the `modules.d/kibana-xpack.yml` file: -[source,yaml] ----------------------------------- -- module: kibana - ... - username: remote_monitoring_user - password: YOUR_PASSWORD ----------------------------------- +. Optional: Disable the system module in {metricbeat}. ++ -- +// tag::disable-system-module[] +By default, the {metricbeat-ref}/metricbeat-module-system.html[system module] is +enabled. The information it collects, however, is not shown on the *Monitoring* +page in {kib}. Unless you want to use that information for other purposes, run +the following command: -. If you configured {kib} to use <>, -you must access it via HTTPS. For example, use a `hosts` setting like -`https://localhost:5601` in the `modules.d/kibana-xpack.yml` file. +["source","sh",subs="attributes,callouts"] +---------------------------------------------------------------------- +metricbeat modules disable system +---------------------------------------------------------------------- +// end::disable-system-module[] +-- . Identify where to send the monitoring data. + + @@ -144,42 +174,39 @@ configuration file (`metricbeat.yml`): [source,yaml] ---------------------------------- output.elasticsearch: + # Array of hosts to connect to. hosts: ["http://es-mon-1:9200", "http://es-mon2:9200"] <1> + + # Optional protocol and basic auth credentials. + #protocol: "https" + #username: "elastic" + #password: "changeme" ---------------------------------- <1> In this example, the data is stored on a monitoring cluster with nodes -`es-mon-1` and `es-mon-2`. +`es-mon-1` and `es-mon-2`. -For more information about these configuration options, see -{metricbeat-ref}/elasticsearch-output.html[Configure the {es} output]. +If you configured the monitoring cluster to use encrypted communications, you +must access it via HTTPS. For example, use a `hosts` setting like +`https://es-mon-1:9200`. --- +IMPORTANT: The {es} {monitor-features} use ingest pipelines, therefore the +cluster that stores the monitoring data must have at least one ingest node. -. If the {es} {security-features} are enabled on the monitoring cluster, you +If the {es} {security-features} are enabled on the monitoring cluster, you must provide a valid user ID and password so that {metricbeat} can send metrics -successfully. +successfully: -... Create a user on the monitoring cluster that has the +.. Create a user on the monitoring cluster that has the `remote_monitoring_agent` {stack-ov}/built-in-roles.html[built-in role]. Alternatively, use the `remote_monitoring_user` {stack-ov}/built-in-users.html[built-in user]. .. Add the `username` and `password` settings to the {es} output information in -the {metricbeat} configuration file (`metricbeat.yml`): -+ --- -[source,yaml] ----------------------------------- -output.elasticsearch: - ... - username: remote_monitoring_user - password: YOUR_PASSWORD ----------------------------------- --- +the {metricbeat} configuration file. -. If you configured the monitoring cluster to use -{ref}/configuring-tls.html[encrypted communications], you must access it via -HTTPS. For example, use a `hosts` setting like `https://es-mon-1:9200` in the -`metricbeat.yml` file. +For more information about these configuration options, see +{metricbeat-ref}/elasticsearch-output.html[Configure the {es} output]. +-- . {metricbeat-ref}/metricbeat-starting.html[Start {metricbeat}]. diff --git a/docs/security/authentication/index.asciidoc b/docs/security/authentication/index.asciidoc index b8b5da97e9f6d..f8ac2dc14eae6 100644 --- a/docs/security/authentication/index.asciidoc +++ b/docs/security/authentication/index.asciidoc @@ -10,6 +10,7 @@ - <> - <> - <> +- <> [[basic-authentication]] ==== Basic Authentication @@ -80,32 +81,92 @@ IMPORTANT: The {kib} user-facing origin should be the same in {kib}, {es}, and t Users will be able to log in to {kib} via SAML Single Sign-On by navigating directly to the {kib} URL. Users who aren't authenticated are redirected to the Identity Provider for login. Most Identity Providers maintain a long-lived session—users who logged in to a different application using the same Identity Provider in the same browser are automatically authenticated. An exception is if {es} or the Identity Provider is configured to force user to re-authenticate. This login scenario is called _Service Provider initiated login_. [float] -===== Access and Refresh Tokens +===== SAML and Basic Authentication -Once the user logs in to {kib} via SAML Single Sign-On, {es} issues access and refresh tokens that {kib} encrypts and stores them in its own session cookie. This way, the user isn't redirected to the Identity Provider for every request that requires authentication. It also means that the {kib} SAML session depends on the `xpack.security.sessionTimeout` setting and the user is automatically logged out if the session expires. An access token that is stored in the session cookie can expire, in which case {kib} will automatically renew it with a one-time-use refresh token and store it in the same cookie. +SAML support in {kib} is designed to be the primary (or sole) authentication method for users of that {kib} instance. However, you can configure both SAML and Basic authentication for the same {kib} instance: -{kib} can only determine if an access token has expired if it receives a request that requires authentication. If both access and refresh tokens have already expired (for example, after 24 hours of inactivity), {kib} initiates a new SAML "handshake" and redirects the user to the Identity Provider. Depending on {es} and the Identity Provider configuration, the user might be asked to re-enter credentials. +[source,yaml] +-------------------------------------------------------------------------------- +xpack.security.authProviders: [saml, basic] +-------------------------------------------------------------------------------- +-- -If {kib} can't redirect the user to the Identity Provider (for example, for AJAX/XHR requests), an error indicates that both access and refresh tokens are expired. Reloading the current {kib} page fixes the error. +The order of `saml` and `basic` is important. Users who open {kib} will go through the SAML Single Sign-On process unless the direct Basic authentication `/login` link is used. This might be the case for {kib} or {es} admins whose accounts aren't linked to the Single Sign-On users database. Or, when the `Authorization: Basic base64(username:password)` HTTP header is included in the request (for example, by reverse proxy). -[float] -===== Local and Global Logout +Basic authentication is supported _only_ if `basic` authentication provider is explicitly declared in `xpack.security.authProviders` setting in addition to `saml`. + +[[oidc]] +==== OpenID Connect Single Sign-On -During logout, both the {kib} session cookie and access/refresh token pair are invalidated. Even if the cookie has been leaked, it can't be re-used after logout. This is known as SAML "local" logout. +Similar to SAML, authentication with OpenID Connect allows users to log in to {kib} using an OpenID Connect Provider such as Google, or Okta. OpenID Connect +should also be configured in {es}, see {xpack-ref}/saml-guide.html[Configuring OpenID Connect Single-Sign-On on the Elastic Stack] for more details. -{kib} can also initiate a SAML "global" logout or _Single Logout_ if it's supported by the Identity Provider and not explicitly disabled by {es}. In this case, the user is redirected to the Identity Provider for log out of all applications associated with the active Identity Provider session. +Set the configuration values in `kibana.yml` as follows: + +. Enable the SAML authentication: ++ +[source,yaml] +-------------------------------------------------------------------------------- +xpack.security.authProviders: [oidc] +-------------------------------------------------------------------------------- + +. {kib} needs to specify which OpenID Connect realm in {es} should be used, in case there are more than one configured there. ++ +[source,yaml] +-------------------------------------------------------------------------------- +xpack.security.authc.oidc.realm: oidc1 +-------------------------------------------------------------------------------- + +. {kib} supports Third Party initiated Single Sign On, which might start with an external application instructing the user's +browser to perform a "non-safe" `POST` HTTP method. This request will not include CSRF protection HTTP headers that are +required by {kib}. If you want to use Third Party initiated SSO , then you must disable the CSRF check for this endpoint. ++ +[source,yaml] +-------------------------------------------------------------------------------- +server.xsrf.whitelist: [/api/security/v1/oidc] +-------------------------------------------------------------------------------- [float] -===== SAML and Basic Authentication +===== OpenID Connect and Basic Authentication -SAML support in {kib} is designed to be the primary (or sole) authentication method for users of that {kib} instance. However, you can configure both SAML and Basic authentication for the same {kib} instance: +Similar to SAML, OpenID Connect support in {kib} is designed to be the primary (or sole) authentication method for users +of that {kib} instance. However, you can configure both OpenID Connect and Basic authentication for the same {kib} instance: [source,yaml] -------------------------------------------------------------------------------- -xpack.security.authProviders: [saml, basic] +xpack.security.authProviders: [oidc, basic] -------------------------------------------------------------------------------- --- -The order of `saml` and `basic` is important. Users who open {kib} will go through the SAML Single Sign-On process unless the direct Basic authentication `/login` link is used. This might be the case for {kib} or {es} admins whose accounts aren't linked to the Single Sign-On users database. Or, when the `Authorization: Basic base64(username:password)` HTTP header is included in the request (for example, by reverse proxy). +Users will be able to access the login page and use Basic authentication by navigating to the `/login` URL. -Basic authentication is supported _only_ if `basic` authentication provider is explicitly declared in `xpack.security.authProviders` setting in addition to `saml`. +[float] +==== Single Sign-On provider details + +The following sections apply both to <> and <> + +[float] +===== Access and Refresh Tokens + +Once the user logs in to {kib} Single Sign-On, either using SAML or OpenID Connect, {es} issues access and refresh tokens +that {kib} encrypts and stores them in its own session cookie. This way, the user isn't redirected to the Identity Provider +for every request that requires authentication. It also means that the {kib} session depends on the `xpack.security.sessionTimeout` +setting and the user is automatically logged out if the session expires. An access token that is stored in the session cookie +can expire, in which case {kib} will automatically renew it with a one-time-use refresh token and store it in the same cookie. + +{kib} can only determine if an access token has expired if it receives a request that requires authentication. If both access +and refresh tokens have already expired (for example, after 24 hours of inactivity), {kib} initiates a new "handshake" and +redirects the user to the external authentication provider (SAML Identity Provider or OpenID Connect Provider) +Depending on {es} and the external authentication provider configuration, the user might be asked to re-enter credentials. + +If {kib} can't redirect the user to the external authentication provider (for example, for AJAX/XHR requests), an error +indicates that both access and refresh tokens are expired. Reloading the current {kib} page fixes the error. + +[float] +===== Local and Global Logout + +During logout, both the {kib} session cookie and access/refresh token pair are invalidated. Even if the cookie has been +leaked, it can't be re-used after logout. This is known as "local" logout. + +{kib} can also initiate a "global" logout or _Single Logout_ if it's supported by the external authentication provider and not +explicitly disabled by {es}. In this case, the user is redirected to the external authentication provider for log out of +all applications associated with the active provider session. diff --git a/docs/security/authorization/index.asciidoc b/docs/security/authorization/index.asciidoc index bdcebb284ffae..6fc0f3f1367da 100644 --- a/docs/security/authorization/index.asciidoc +++ b/docs/security/authorization/index.asciidoc @@ -1,21 +1,92 @@ [role="xpack"] [[xpack-security-authorization]] -=== Authorization -Authorizing users to use {kib} in simple configurations is as easy as assigning the user -either the `kibana_user` or `kibana_dashboard_only_user` reserved role. If you're running -a single tenant of {kib} against your {es} cluster, and you're not controlling access to individual spaces, then this is sufficient and no other action is required. +=== Granting access to {kib} +The Elastic Stack comes with the `kibana_user` {stack-ov}/built-in-roles.html[built-in role], which you can use to grant access to all Kibana features in all spaces. To grant users access to a subset of spaces or features, you can create a custom role that grants the desired Kibana privileges. -==== Spaces +When you assign a user multiple roles, the user receives a union of the roles’ privileges. Therefore, assigning the `kibana_user` role in addition to a custom role that grants Kibana privileges is ineffective because `kibana_user` has access to all the features in all spaces. -If you want to control individual spaces in {kib}, do **not** use the `kibana_user` or `kibana_dashboard_only_user` roles. Users with these roles are able to access all spaces in Kibana. Instead, create your own roles that grant access to specific spaces. +NOTE: When running multiple tenants of Kibana by changing the `kibana.index` in your `kibana.yml`, you cannot use `kibana_user` to grant access. You must create custom roles that authorize the user for that specific tenant. Although multi-tenant installations are supported, the recommended approach to securing access to Kibana segments is to grant users access to specific spaces. -==== Multi-tenant {kib} +[role="xpack"] +=== {kib} role management + +To create a role that grants {kib} privileges, go to **Management -> Security -> Roles** and click **Create role**. + +==== Adding {kib} privileges + +To assign {kib} privileges to the role, click **Add space privilege** in the Kibana section. + +[role="screenshot"] +image::security/images/add-space-privileges.png[Add space privileges] + +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**. + +[IMPORTANT] +If a feature is hidden using the Spaces disabled features, it will remain hidden even if the user has the necessary privileges. + +When using the **Customize by feature** option, you can choose either **All**, **Read** or **None** for access to each feature. As new features are added to Kibana, roles that use the custom option do not automatically get access to the new features. You must manually update the roles. + +NOTE: Machine Learning and Stack Monitoring rely on built-in roles to grant access. When a user is assigned the appropriate roles, the Machine Learning and Stack Monitoring application are available; otherwise, these applications are not visible. + +To apply your changes, click **Create space privilege**. The space privilege shows up under the Kibana privileges section of the role. + + +[role="screenshot"] +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** 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. + + +==== Privilege summary + +To view a summary of the privileges granted, click **View privilege summary**. + +[role="screenshot"] +image::security/images/view-privilege-summary.png[View privilege summary] + +==== Example 1: Grant all access to Dashboard at an individual space + +. Click **Add space privilege**. +. For **Spaces**, select an individual space. +. For **Privilege**, leave the default selection of **Custom**. +. For the Dashboard feature, select **All** +. Click **Create space privilege**. + +[role="screenshot"] +image::security/images/privilege-example-1.png[Privilege example 1] + +==== Example 2: Grant all access to one space and read access to another + +. Click **Add space privilege**. +. For **Spaces**, select the first space. +. For **Privilege**, select **All**. +. Click **Create space privilege**. +. Click **Add space privilege**. +. For **Spaces**, select the second space. +. For **Privilege**, select **Read**. +. Click **Create space privilege**. + +[role="screenshot"] +image::security/images/privilege-example-2.png[Privilege example 2] + +==== Example 3: Grant read access to all spaces and write access to an individual space + +. Click **Add space privilege**. +. For **Spaces**, select *** Global (all spaces)**. +. For **Privilege**, select **Read**. +. Click **Create space privilege**. +. Click **Add space privilege**. +. For **Spaces**, select the individual space. +. For **Privilege**, select **All**. +. Click **Create space privilege**. -When running multiple tenants of {kib}, and changing the `kibana.index` in your `kibana.yml`, you -must create custom roles that authorize the user for that specific tenant. You can use -either the *Management / Security / Roles* page in {kib} or the <> -to assign a specific <> at that tenant. After creating the -custom role, you should assign this role to the user(s) that you wish to have access. +[role="screenshot"] +image::security/images/privilege-example-3.png[Privilege example 3] -While multi-tenant installations are supported, the recommended approach to securing access to segments of {kib} is to grant users access to specific spaces. diff --git a/docs/security/authorization/kibana-privileges.asciidoc b/docs/security/authorization/kibana-privileges.asciidoc index d04a725f556a6..034b3254c96f2 100644 --- a/docs/security/authorization/kibana-privileges.asciidoc +++ b/docs/security/authorization/kibana-privileges.asciidoc @@ -2,14 +2,70 @@ [[kibana-privileges]] === Kibana privileges -This section lists the Kibana privileges that you can assign to a role. +{kib} privileges grant users access to features within {kib}. Roles have privileges to determine whether users have write or read access. -[horizontal] +==== Base privileges +Assigning a base privilege grants access to all available features in Kibana (Discover, Visualize, Dashboard, and so on). [[kibana-privileges-all]] -`all`:: -All Kibana privileges, can read, write and delete saved searches, dashboards, visualizations, -short URLs, Timelion sheets, graph workspaces, index patterns and advanced settings. +`all`:: Grants full read-write access. +`read`:: Grants read-only access. -`read`:: -Can read saved searches, dashboards, visualizations, short URLs, Timelion sheets, graph workspaces, -index patterns, and advanced settings. +===== Assigning base privileges +From the role management screen: + +[role="screenshot"] +image::security/images/assign_base_privilege.png[Assign base privilege] + +From the <>: +[source,js] +-------------------------------------------------- +PUT /api/security/role/my_kibana_role +{ + "elasticsearch": { + "cluster" : [ ], + "indices" : [ ] + }, + "kibana": [ + { + "base": ["all"], + "feature": {}, + "spaces": ["marketing"] + } + ] +} +-------------------------------------------------- + + + +==== Feature privileges +Assigning a feature privilege grants access to a specific feature. + +`all`:: Grants full read-write access. +`read`:: Grants read-only access. + +===== Assigning feature privileges +From the role management screen: + +[role="screenshot"] +image::security/images/assign_feature_privilege.png[Assign feature privilege] + +From the <>: +[source,js] +-------------------------------------------------- +PUT /api/security/role/my_kibana_role +{ + "elasticsearch": { + "cluster" : [ ], + "indices" : [ ] + }, + "kibana": [ + { + "base": [], + "feature": { + "dashboard": ["all"] + }, + "spaces": ["marketing"] + } + ] +} +-------------------------------------------------- diff --git a/docs/security/images/add-space-privileges.png b/docs/security/images/add-space-privileges.png new file mode 100755 index 0000000000000..7739332c33b60 Binary files /dev/null and b/docs/security/images/add-space-privileges.png differ diff --git a/docs/security/images/assign_base_privilege.png b/docs/security/images/assign_base_privilege.png new file mode 100644 index 0000000000000..2aef42132ef21 Binary files /dev/null and b/docs/security/images/assign_base_privilege.png differ diff --git a/docs/security/images/assign_feature_privilege.png b/docs/security/images/assign_feature_privilege.png new file mode 100644 index 0000000000000..e7d000d4554ad Binary files /dev/null and b/docs/security/images/assign_feature_privilege.png differ diff --git a/docs/security/images/create-space-privilege.png b/docs/security/images/create-space-privilege.png new file mode 100755 index 0000000000000..2e6cc299bfc54 Binary files /dev/null and b/docs/security/images/create-space-privilege.png differ diff --git a/docs/security/images/privilege-example-1.png b/docs/security/images/privilege-example-1.png new file mode 100755 index 0000000000000..68ba716437240 Binary files /dev/null and b/docs/security/images/privilege-example-1.png differ diff --git a/docs/security/images/privilege-example-2.png b/docs/security/images/privilege-example-2.png new file mode 100755 index 0000000000000..2530ca5da36e1 Binary files /dev/null and b/docs/security/images/privilege-example-2.png differ diff --git a/docs/security/images/privilege-example-3.png b/docs/security/images/privilege-example-3.png new file mode 100755 index 0000000000000..c2e5db3a18e20 Binary files /dev/null and b/docs/security/images/privilege-example-3.png differ diff --git a/docs/security/images/view-privilege-summary.png b/docs/security/images/view-privilege-summary.png new file mode 100755 index 0000000000000..d93d55c93fd12 Binary files /dev/null and b/docs/security/images/view-privilege-summary.png differ diff --git a/docs/security/index.asciidoc b/docs/security/index.asciidoc index 73c75135e9fe6..44ffb39a90618 100644 --- a/docs/security/index.asciidoc +++ b/docs/security/index.asciidoc @@ -13,7 +13,7 @@ auditing. For more information, see [float] === Users -You can create and manage users on the *Management* / *Security* / *Users* page. +You can create and manage users on the *Management -> Security -> Users* page. You can also change their passwords and roles. For more information about authentication and built-in users, see {stack-ov}/setting-up-authentication.html[Setting up user authentication]. @@ -21,9 +21,8 @@ authentication and built-in users, see [float] === Roles -You can manage roles on the *Management* / *Security* / *Roles* page, or use -{kib}'s <>. For more information on configuring roles for -{kib} see <>. +You can manage roles on the *Management -> Security -> Roles* page, or use +the <>. For more information on configuring roles for {kib}, see <>. For a more holistic overview of configuring roles for the entire stack, see {stack-ov}/authorization.html[Configuring role-based access control]. diff --git a/docs/settings/code-settings.asciidoc b/docs/settings/code-settings.asciidoc new file mode 100644 index 0000000000000..e2c56622eb509 --- /dev/null +++ b/docs/settings/code-settings.asciidoc @@ -0,0 +1,59 @@ +[role="xpack"] +[[code-settings-kibana]] +== Code Settings in Kibana +++++ +Code settings +++++ + +Unless you are running multiple Kibana instances as a cluster, you do not need to change any settings to use *Code* by default. If you’d like to change any of the default values, copy and paste the relevant settings below into your `kibana.yml` configuration file. + +`xpack.code.queueIndex`:: +Internal worker queue index name. Defaults to `.code_internal-worker-queue`. + +`xpack.code.queueTimeoutMs`:: +Internal worker queue task timeout. Default to `3600000`. + +`xpack.code.updateFrequencyMs`:: +Update scheduler execution frequency in milliseconds. Defaults to `300000`. + +`xpack.code.indexFrequencyMs`:: +Index scheduler execution frequency in milliseconds. Defaults to `86400000`. + +`xpack.code.updateRepoFrequencyMs`:: +Repo update frequency in milliseconds. Defaults to `300000`. + +`xpack.code.indexRepoFrequencyMs`:: +Repo index frequency in milliseconds. Defaults to `86400000`. + +`xpack.code.lsp.requestTimeoutMs`:: +Timeout time for a request to language servers in milliseconds. Defaults to `10000`. + +`xpack.code.lsp.detach`:: +Whether language servers will run in detach mode. Defaults to `false`. + +`xpack.code.lsp.verbose`:: +Whether to show more verbose log for language servers. Defaults to `false`. + +`xpack.code.security.enableMavenImport`:: +Whether to support maven. Defaults to `true`. + +`xpack.code.security.enableGradleImport`:: +Whether to support gradle. Defaults to `false`. + +`xpack.code.security.installNodeDependency`:: +Whether to enable node dependency download. Defaults to `true`. + +`xpack.code.security.gitHostWhitelist`:: +Whitelist of hostnames for git clone address. Defaults to `[ 'github.com', 'gitlab.com', 'bitbucket.org', 'gitbox.apache.org', 'eclipse.org',]`. + +`xpack.code.security.gitProtocolWhitelist`:: +Whitelist of protocols for git clone address. Defaults to `[ 'https', 'git', 'ssh' ]`. + +`xpack.code.security.enableGitCertCheck`:: +Whether to enable Code to load git key pairs. Defaults to `true`. + +`xpack.code.maxWorkspace`:: +Maximal number of workspaces each language server allows to span. Defaults to `5`. + +`xpack.code.codeNodeUrl`:: +URL of the Code node. This config is only needed when multiple Kibana instances are set up as a cluster. Defaults to `` diff --git a/docs/settings/i18n-settings.asciidoc b/docs/settings/i18n-settings.asciidoc index 618ee36e5714e..4fe466bcb4580 100644 --- a/docs/settings/i18n-settings.asciidoc +++ b/docs/settings/i18n-settings.asciidoc @@ -13,5 +13,6 @@ Kibana currently supports the following locales: + - English - `en` (default) - Chinese - `zh-CN` +- Japanese - `ja-JP` diff --git a/docs/setup/production.asciidoc b/docs/setup/production.asciidoc index 2297e53a0bebe..9158b1ae2197c 100644 --- a/docs/setup/production.asciidoc +++ b/docs/setup/production.asciidoc @@ -26,16 +26,14 @@ You can use {stack-ov}/elasticsearch-security.html[{stack} {security-features}] to control what {es} data users can access through Kibana. When {security-features} are enabled, Kibana users have to log in. They need to -have the `kibana_user` role as well as access to the indices they -will be working with in Kibana. +have a role granting <> as well as access +to the indices they will be working with in Kibana. If a user loads a Kibana dashboard that accesses data in an index that they are not authorized to view, they get an error that indicates the index does -not exist. The {security-features} do not currently provide a way to control -which users can load which dashboards. +not exist. -For information about setting up Kibana users, see -{kibana-ref}/using-kibana-with-security.html[Configuring security in Kibana]. +For more information on granting access to Kibana, see <>. [float] [[csp-strict-mode]] diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 365184f17f9b5..621503db1b37b 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -1,11 +1,11 @@ [[settings]] == Configuring Kibana -The Kibana server reads properties from the `kibana.yml` file on startup. The -location of this file differs depending on how you installed {kib}. For example, -if you installed {kib} from an archive distribution (`.tar.gz` or `.zip`), by -default it is in `$KIBANA_HOME/config`. By default, with package distributions -(Debian or RPM), it is in `/etc/kibana`. +The Kibana server reads properties from the `kibana.yml` file on startup. The +location of this file differs depending on how you installed {kib}. For example, +if you installed {kib} from an archive distribution (`.tar.gz` or `.zip`), by +default it is in `$KIBANA_HOME/config`. By default, with package distributions +(Debian or RPM), it is in `/etc/kibana`. The default settings configure Kibana to run on `localhost:5601`. To change the host or port number, or connect to Elasticsearch running on a different machine, @@ -118,22 +118,22 @@ 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. -`interpreter.enableInVisualize`:: *Default: true* Enables use of interpreter in +`interpreter.enableInVisualize`:: *Default: true* Enables use of interpreter in Visualize. `kibana.defaultAppId:`:: *Default: "discover"* The default application to load. `kibana.index:`:: *Default: ".kibana"* Kibana uses an index in Elasticsearch to store saved searches, visualizations and dashboards. Kibana creates a new index -if the index doesn’t already exist. If you configure a custom index, the name must +if the index doesn’t already exist. If you configure a custom index, the name must be lowercase, and conform to {es} {ref}/indices-create-index.html[index name limitations]. `logging.dest:`:: *Default: `stdout`* Enables you specify a file where Kibana stores log output. -`logging.json:`:: *Default: false* Logs output as JSON. When set to `true`, the -logs will be formatted as JSON strings that include timestamp, log level, context, message -text and any other metadata that may be associated with the log message itself. +`logging.json:`:: *Default: false* Logs output as JSON. When set to `true`, the +logs will be formatted as JSON strings that include timestamp, log level, context, message +text and any other metadata that may be associated with the log message itself. If `logging.dest.stdout` is set and there is no interactive terminal ("TTY"), this setting will default to `true`. @@ -151,10 +151,15 @@ be referenced at https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. setting to `true` to log all events, including system usage information and all requests. Supported on Elastic Cloud Enterprise. -`map.includeElasticMapsService:`:: *Default: true* Turns on or off whether -layers from the Elastic Maps Service should be included in the vector and tile -layer option list. By turning this off, only the layers that are configured here -will be included. +`map.includeElasticMapsService:`:: *Default: true* +Set to false to disable connections to Elastic Maps Service. +When `includeElasticMapsService` is turned off, only the vector layers configured by `map.regionmap` +and the tile layer configured by `map.tilemap.url` will be available in the <>, +<>, and <>. + +`map.proxyElasticMapsServiceInMaps:`:: *Default: false* +Set to true to proxy all <> Elastic Maps Service requests through the Kibana server. +This setting does not impact <> and <>. [[regionmap-settings]] `map.regionmap:`:: Specifies additional vector layers for use in <> visualizations. Supported on {ece}. Each layer @@ -299,10 +304,11 @@ disable the License Management user interface. `xpack.rollup.enabled:`:: *Default: true* Set this value to false to disable the Rollup user interface. -`i18n.locale`:: *Default: en* Set this value to change the Kibana interface language. Valid locales are: `en`, `zh-CN`. +`i18n.locale`:: *Default: en* Set this value to change the Kibana interface language. Valid locales are: `en`, `zh-CN`, `ja-JP`. include::{docdir}/settings/apm-settings.asciidoc[] +include::{docdir}/settings/code-settings.asciidoc[] include::{docdir}/settings/dev-settings.asciidoc[] include::{docdir}/settings/graph-settings.asciidoc[] include::{docdir}/settings/infrastructure-ui-settings.asciidoc[] diff --git a/docs/spaces/getting-started.asciidoc b/docs/spaces/getting-started.asciidoc deleted file mode 100644 index e6a96553873b7..0000000000000 --- a/docs/spaces/getting-started.asciidoc +++ /dev/null @@ -1,8 +0,0 @@ -[role="xpack"] -[[spaces-getting-started]] -=== Getting Started - -Spaces are automatically enabled in {kib}. If you don't wish to use this feature, you can disable it -by setting `xpack.spaces.enabled` to `false` in your `kibana.yml` configuration file. - -{kib} automatically creates a default space for you. If you are upgrading from another version of {kib}, then the default space will contain all of your existing saved objects. Although you can't delete the default space, you can customize it to your liking. \ No newline at end of file diff --git a/docs/spaces/images/change-space.png b/docs/spaces/images/change-space.png new file mode 100644 index 0000000000000..6907c80b5984f Binary files /dev/null and b/docs/spaces/images/change-space.png differ diff --git a/docs/spaces/images/delete-space.png b/docs/spaces/images/delete-space.png index 8237df1136a9e..ce83670ad2a3a 100644 Binary files a/docs/spaces/images/delete-space.png and b/docs/spaces/images/delete-space.png differ diff --git a/docs/spaces/images/edit-space-feature-visibility.png b/docs/spaces/images/edit-space-feature-visibility.png new file mode 100644 index 0000000000000..447944d2fe5ad Binary files /dev/null and b/docs/spaces/images/edit-space-feature-visibility.png differ diff --git a/docs/spaces/images/edit-space.png b/docs/spaces/images/edit-space.png index dae7d01f665c0..8381d546eaf07 100644 Binary files a/docs/spaces/images/edit-space.png and b/docs/spaces/images/edit-space.png differ diff --git a/docs/spaces/images/securing-spaces.png b/docs/spaces/images/securing-spaces.png index a94d2c36d4f5d..e109e66b4cec6 100644 Binary files a/docs/spaces/images/securing-spaces.png and b/docs/spaces/images/securing-spaces.png differ diff --git a/docs/spaces/images/space-management.png b/docs/spaces/images/space-management.png index bd58605362024..0cbbd7da1243b 100644 Binary files a/docs/spaces/images/space-management.png and b/docs/spaces/images/space-management.png differ diff --git a/docs/spaces/images/space-selector.png b/docs/spaces/images/space-selector.png index a1977b01d1fa0..908c5360acd39 100644 Binary files a/docs/spaces/images/space-selector.png and b/docs/spaces/images/space-selector.png differ diff --git a/docs/spaces/index.asciidoc b/docs/spaces/index.asciidoc index b40c4267c2b49..c6bf9a9184a66 100644 --- a/docs/spaces/index.asciidoc +++ b/docs/spaces/index.asciidoc @@ -3,15 +3,27 @@ == Spaces With spaces, you can organize your dashboards and other saved objects into meaningful categories. -After creating your own spaces, you will be asked to choose a space when you enter {kib}. Once inside a space, -you will only see the dashboards and other saved objects that belong to that space. You can change your active space at any time. +After creating your own spaces, you will be asked to choose a space when you enter Kibana. +Once inside a space, you will only see the dashboards and other saved objects that belong to that space. -With security enabled, you can control which users have access to individual spaces. +You can change your current space at any time, by clicking on the space avatar in the top left. [role="screenshot"] -image::spaces/images/space-selector.png["Space selector screen"] +image::spaces/images/change-space.png["Change current space"] + +With security enabled, you can <>. + + +[float] +[[spaces-getting-started]] +=== Getting Started + +Spaces are automatically enabled in {kib}. If you don't wish to use this feature, you can disable it +by setting `xpack.spaces.enabled` to `false` in your `kibana.yml` configuration file. + +{kib} automatically creates a default space for you. If you are upgrading from another +version of {kib}, then the default space will contain all of your existing saved objects. +Although you can't delete the default space, you can customize it to your liking. -include::getting-started.asciidoc[] include::managing-spaces.asciidoc[] -include::securing-spaces.asciidoc[] include::moving-saved-objects.asciidoc[] diff --git a/docs/spaces/managing-spaces.asciidoc b/docs/spaces/managing-spaces.asciidoc index 73a21ff049b36..fbfed15bc6ee9 100644 --- a/docs/spaces/managing-spaces.asciidoc +++ b/docs/spaces/managing-spaces.asciidoc @@ -18,8 +18,17 @@ You cannot change the space identifier once the space is created. [role="screenshot"] image::spaces/images/edit-space.png["Updating a space"] +==== Controlling feature visibility +You can control which {kib} features are visible in each space. For example, you can hide “Dev Tools” in your “Executive” space, if users of that space don’t need this feature. + +[role="screenshot"] +image::spaces/images/edit-space-feature-visibility.png["Controlling features visiblity"] + + +NOTE: This is not considered a security feature. If you wish to secure access to specific features on a per-user basis, then you need to configure <>. + ==== Deleting spaces Deleting a space is a destructive operation, which cannot be undone. When you delete a space, all of the saved objects that belong to that space are also deleted. [role="screenshot"] -image::spaces/images/delete-space.png["Deleting a space"] \ No newline at end of file +image::spaces/images/delete-space.png["Deleting a space"] diff --git a/docs/spaces/securing-spaces.asciidoc b/docs/spaces/securing-spaces.asciidoc deleted file mode 100644 index 596acc7872b9b..0000000000000 --- a/docs/spaces/securing-spaces.asciidoc +++ /dev/null @@ -1,74 +0,0 @@ -[role="xpack"] -[[spaces-securing]] -=== Securing spaces - - -With a Gold or Platinum license, you can control which roles have access to -each space. To get started, navigate to **Management > Roles**. - -[role="screenshot"] -image::images/spaces_secure_all_privileges.png[] - -==== Setting privileges - -Access for all spaces in {kib} is governed by a concept called "minimum privilege." -There are three options for minimum privilege: - - -[cols="2*^<"] -|=== - -|`all` -|Users have read/write access to all spaces in {kib}. Additionally, -users can create, edit, and delete individual spaces. This extends to spaces -that users create in the future. - -|`read` -|Users have read-only access to all spaces in {kib}. This extends to spaces -that users create in the future. - -|`none` -|Users do not have the all-spaces access. You must set access on -individual spaces. - - -|=== - -Once you set the minimum privilege for all spaces, you can then add read and write -access to individual spaces. - - -==== Examples -[cols="2*^<"] -|=== - -s|To provide -s|Do this - -|Full access to all spaces -|Set the minimum privilege to `all`. This grants -full access to all spaces. In this case, you can't -customize access to specific spaces. - -|Read-only access to all spaces, with full access to specific spaces -|Set the minimum privilege to `read`, -then grant the `all` privilege to individual spaces as needed. You can't revoke -access to an individual space. - -|Read-only access to a specific space -|Set the minimum privilege to `none` to prevent all-space access, -then set the `read` privilege for an individual space, -as shown below. - -|=== - -[role="screenshot"] -image::images/spaces_secure_specific_spaces.png[] - - - -[float] -=== Viewing all space privileges - -To see which roles have access to each space, click *View summary of spaces privileges*. - diff --git a/docs/timelion/getting-started/timelion-conditional.asciidoc b/docs/timelion/getting-started/timelion-conditional.asciidoc index 87cd18fb7ee60..754d97aa332e5 100644 --- a/docs/timelion/getting-started/timelion-conditional.asciidoc +++ b/docs/timelion/getting-started/timelion-conditional.asciidoc @@ -59,4 +59,4 @@ Now that you have thresholds and a moving average, let's format the visualizatio image::images/timelion-conditional04.png[] {nbsp} -Save your Timelion sheet and continue on to the next section to add these new visualizations to your dashboard. +Continue on to the next section to learn how to save your timelion sheet and add an expression to your dashboard. diff --git a/docs/timelion/getting-started/timelion-create.asciidoc b/docs/timelion/getting-started/timelion-create.asciidoc index 8d63ce0761a0b..5725a99927b35 100644 --- a/docs/timelion/getting-started/timelion-create.asciidoc +++ b/docs/timelion/getting-started/timelion-create.asciidoc @@ -33,5 +33,3 @@ It’s a bit hard to differentiate the two series. Customize the labels in order image::images/timelion-create03.png[] {nbsp} - -Save the entire Timelion sheet as _Metricbeat Example_. As a best practice, you should be saving any significant changes made to this sheet as you progress through this tutorial. diff --git a/docs/timelion/getting-started/timelion-customize.asciidoc b/docs/timelion/getting-started/timelion-customize.asciidoc index 617cc55cd0fd2..198a4e27513fb 100644 --- a/docs/timelion/getting-started/timelion-customize.asciidoc +++ b/docs/timelion/getting-started/timelion-customize.asciidoc @@ -50,4 +50,4 @@ Last but not least, adjust the legend so that it takes up as little space as pos image::images/timelion-customize04.png[] {nbsp} -Save your changes and continue on to the next section to learn about mathematical functions. +Continue on to the next section to learn about mathematical functions. diff --git a/docs/timelion/getting-started/timelion-math.asciidoc b/docs/timelion/getting-started/timelion-math.asciidoc index 561278db07e27..67c336b71696a 100644 --- a/docs/timelion/getting-started/timelion-math.asciidoc +++ b/docs/timelion/getting-started/timelion-math.asciidoc @@ -58,4 +58,4 @@ Utilizing the formatting functions `.title()`, `.label()`, `.color()`, `.lines() image::images/timelion-math06.png[] {nbsp} -Save your changes and continue on to the next section to learn about conditional logic and tracking trends. +Continue on to the next section to learn about conditional logic and tracking trends. diff --git a/docs/timelion/getting-started/timelion-save.asciidoc b/docs/timelion/getting-started/timelion-save.asciidoc index feaeb5468d8da..b8269ae008a55 100644 --- a/docs/timelion/getting-started/timelion-save.asciidoc +++ b/docs/timelion/getting-started/timelion-save.asciidoc @@ -1,9 +1,29 @@ [[timelion-save]] -=== Add to dashboard +=== Save and add to dashboard -You’ve officially harnessed the power of Timelion to create time series visualizations. The final step of this tutorial is to add your new visualizations to a dashboard. Below, this section will show you how to save a visualization from your Timelion sheet and add it to an existing dashboard. +You’ve officially harnessed the power of Timelion to create time series visualizations. The final steps of this tutorial are to save your entire Timelion sheet and save an expression as a dashboard panel. -To save a Timelion visualization as a dashboard panel, follow the steps below. +[role="xpack"] +[[timelion-read-only-access]] +==== Read only access +When you have insufficient privileges to save Timelion sheets, the following indicator in Kibana will be +displayed and the *Save* button won't be visible. For more information on granting access to +Kibana see <>. + +[role="screenshot"] +image::images/timelion-read-only-badge.png[Example of Timelion's read only access indicator in Kibana's header] + +==== Save entire Timelion sheet + +Saving an entire Timelion sheet allows you to reload it in the Timelion application and make changes to any of the expressions: + +. Click the `Save` option in the top menu +. Select `Save entire Timelion sheet` +. Name your sheet and click `Save` + +==== Save as dashboard panel + +To save a Timelion expression as a dashboard panel, follow the steps below. . Select the visualization you’d like to add to one (or multiple) dashboards . Click the `Save` option in the top menu diff --git a/docs/visualize.asciidoc b/docs/visualize.asciidoc index 893b6f3363808..61c409726b5fc 100644 --- a/docs/visualize.asciidoc +++ b/docs/visualize.asciidoc @@ -138,6 +138,8 @@ For more information about working with sub aggregations, see https://www.elastic.co/blog/kibana-aggregation-execution-order-and-you[Kibana, Aggregation Execution Order, and You]. +include::visualize/saving.asciidoc[] + include::visualize/xychart.asciidoc[] include::visualize/controls.asciidoc[] diff --git a/docs/visualize/images/read-only-badge.png b/docs/visualize/images/read-only-badge.png new file mode 100644 index 0000000000000..8970105f7443c Binary files /dev/null and b/docs/visualize/images/read-only-badge.png differ diff --git a/docs/visualize/saving.asciidoc b/docs/visualize/saving.asciidoc new file mode 100644 index 0000000000000..855555794a6f0 --- /dev/null +++ b/docs/visualize/saving.asciidoc @@ -0,0 +1,24 @@ +[[save-visualize]] +== Saving Visualizations +Saving visualizations enables you to reload them in Visualize and use them in +<>. + +[float] +[[visualize-read-only-access]] +=== [xpack]#Read only access# +When you have insufficient privileges to save visualizations, the following indicator in Kibana will be +displayed and the *Save* button won't be visible. For more information on granting access to +Kibana see <>. + +[role="screenshot"] +image::visualize/images/read-only-badge.png[Example of Visualize's read only access indicator in Kibana's header] + +[float] +[[saving-a-visualization]] +=== Saving a Visualization +To save the current visualization: + +. Click *Save* in the Kibana toolbar. +. Enter a name for the visualization and click *Save*. + +You can import, export and delete saved visualizations from *Management/Kibana/Saved Objects*. diff --git a/package.json b/package.json index f7f8ef6fc7252..2b8a2919da003 100644 --- a/package.json +++ b/package.json @@ -51,12 +51,11 @@ "test:server": "grunt test:server", "test:coverage": "grunt test:coverage", "typespec": "typings-tester --config x-pack/plugins/canvas/public/lib/aeroelastic/tsconfig.json x-pack/plugins/canvas/public/lib/aeroelastic/__fixtures__/typescript/typespec_tests.ts", - "checkLicenses": "grunt licenses --dev", + "checkLicenses": "node scripts/check_licenses --dev", "build": "node scripts/build --all-platforms", "start": "node --trace-warnings --trace-deprecation scripts/kibana --dev ", "debug": "node --nolazy --inspect scripts/kibana --dev", "debug-break": "node --nolazy --inspect-brk scripts/kibana --dev", - "precommit": "node scripts/precommit_hook", "karma": "karma start", "lint": "yarn run lint:es && yarn run lint:sass", "lint:es": "node scripts/eslint", @@ -81,7 +80,7 @@ "**/@types/node": "10.12.27", "**/@types/hapi": "^17.0.18", "**/typescript": "^3.3.3333", - "**/@elastic/eui/**/core-js": "2.5.3" + "**/@elastic/eui/**/core-js": "2.6.9" }, "workspaces": { "packages": [ @@ -100,12 +99,12 @@ ] }, "dependencies": { - "@babel/core": "^7.3.4", - "@babel/polyfill": "^7.2.5", - "@babel/register": "^7.0.0", - "@elastic/charts": "^3.11.2", + "@babel/core": "7.4.5", + "@babel/polyfill": "7.4.4", + "@babel/register": "7.4.4", + "@elastic/charts": "^4.2.6", "@elastic/datemath": "5.0.2", - "@elastic/eui": "10.4.0", + "@elastic/eui": "11.0.1", "@elastic/filesaver": "1.1.2", "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "2.3.3", @@ -124,93 +123,95 @@ "@types/lodash.clonedeep": "^4.5.4", "@types/react-grid-layout": "^0.16.7", "@types/recompose": "^0.30.5", - "JSONStream": "1.1.1", + "@types/yauzl": "^2.9.1", + "JSONStream": "1.3.5", "abortcontroller-polyfill": "^1.1.9", "angular": "1.6.9", "angular-aria": "1.6.6", - "angular-elastic": "2.5.0", + "angular-elastic": "2.5.1", "angular-recursion": "^1.0.5", "angular-route": "1.4.7", "angular-sanitize": "1.6.5", - "angular-sortable-view": "0.0.15", + "angular-sortable-view": "0.0.17", "autoprefixer": "^9.1.0", - "babel-loader": "8.0.5", - "bluebird": "3.5.3", + "babel-loader": "8.0.6", + "bluebird": "3.5.5", "boom": "^7.2.0", "brace": "0.11.1", - "cache-loader": "1.2.2", + "cache-loader": "4.0.0", "chalk": "^2.4.1", "color": "1.0.3", - "commander": "2.8.1", - "compare-versions": "3.1.0", - "core-js": "2.5.3", - "css-loader": "1.0.0", + "commander": "2.20.0", + "compare-versions": "3.4.0", + "core-js": "2.6.9", + "css-loader": "2.1.1", "custom-event-polyfill": "^0.3.0", - "d3": "3.5.6", - "d3-cloud": "1.2.1", - "del": "^3.0.0", - "dragula": "3.7.0", + "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", + "file-loader": "4.0.0", + "font-awesome": "4.7.0", "getos": "^3.1.0", "glob": "^7.1.2", "glob-all": "^3.1.0", "globby": "^8.0.1", "good-squeeze": "2.1.0", "h2o2": "^8.1.2", - "handlebars": "4.0.14", + "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": "1.5.1", + "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", + "less-loader": "5.0.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", + "pegjs": "0.10.0", "postcss-loader": "3.0.0", "prop-types": "15.5.8", "proxy-from-env": "1.0.0", "pug": "^2.0.3", "querystring-browser": "1.0.4", - "raw-loader": "0.5.1", + "raw-loader": "3.0.0", "react": "^16.8.0", "react-addons-shallow-compare": "15.6.2", "react-color": "^2.13.8", "react-dom": "^16.8.0", "react-grid-layout": "^0.16.2", + "react-hooks-testing-library": "^0.5.0", "react-input-range": "^1.3.0", "react-markdown": "^3.4.1", "react-redux": "^5.0.7", @@ -220,30 +221,31 @@ "redux": "4.0.0", "redux-actions": "2.2.1", "redux-thunk": "2.3.0", - "regression": "2.0.0", + "regression": "2.0.1", "request": "^2.88.0", "reselect": "^3.0.1", "resize-observer-polyfill": "^1.5.0", - "rimraf": "2.4.3", - "rison-node": "1.0.0", + "rimraf": "2.6.3", + "rison-node": "1.0.2", "rxjs": "^6.2.1", "script-loader": "0.7.2", "semver": "^5.5.0", "stream-stream": "^1.2.6", "style-it": "^2.1.3", "style-loader": "0.23.1", - "tar": "4.4.8", + "symbol-observable": "^1.2.0", + "tar": "4.4.10", "terser-webpack-plugin": "^1.1.0", "thread-loader": "^2.1.2", "tinygradient": "0.3.0", - "tinymath": "1.1.1", + "tinymath": "1.2.1", "topojson-client": "3.0.0", "trunc-html": "1.0.2", "trunc-text": "1.0.2", "tslib": "^1.9.3", "type-detect": "^4.0.8", "ui-select": "0.19.6", - "url-loader": "1.1.2", + "url-loader": "2.0.0", "uuid": "3.0.1", "val-loader": "^1.1.1", "validate-npm-package-name": "2.2.2", @@ -258,10 +260,11 @@ "yauzl": "2.7.0" }, "devDependencies": { - "@babel/parser": "^7.3.4", - "@babel/types": "^7.3.4", + "@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.13", + "@elastic/github-checks-reporter": "0.0.15", "@elastic/makelogs": "^4.4.0", "@kbn/es": "1.0.0", "@kbn/eslint-import-resolver-kibana": "2.0.0", @@ -269,8 +272,8 @@ "@kbn/expect": "1.0.0", "@kbn/plugin-generator": "1.0.0", "@kbn/test": "1.0.0", - "@microsoft/api-documenter": "7.0.49", - "@microsoft/api-extractor": "7.1.1", + "@microsoft/api-documenter": "7.2.1", + "@microsoft/api-extractor": "7.1.8", "@octokit/rest": "^15.10.0", "@types/angular": "1.6.50", "@types/angular-mocks": "^1.7.0", @@ -283,13 +286,12 @@ "@types/classnames": "^2.2.3", "@types/d3": "^3.5.41", "@types/dedent": "^0.7.0", - "@types/del": "^3.0.1", "@types/delete-empty": "^2.0.0", - "@types/elasticsearch": "^5.0.30", + "@types/elasticsearch": "^5.0.33", "@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", @@ -304,9 +306,11 @@ "@types/jquery": "^3.3.6", "@types/js-yaml": "^3.11.1", "@types/json5": "^0.0.30", - "@types/listr": "^0.13.0", + "@types/license-checker": "15.0.0", + "@types/listr": "^0.14.0", "@types/lodash": "^3.10.1", "@types/lru-cache": "^5.1.0", + "@types/markdown-it": "^0.0.7", "@types/minimatch": "^2.0.29", "@types/mocha": "^5.2.6", "@types/moment-timezone": "^0.5.8", @@ -338,49 +342,51 @@ "@typescript-eslint/parser": "^1.6.0", "angular-mocks": "1.4.7", "archiver": "^3.0.0", - "babel-eslint": "^10.0.1", + "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.10", + "chance": "1.0.18", "cheerio": "0.22.0", - "chokidar": "1.6.0", + "chokidar": "3.0.0", "chromedriver": "^74.0.0", - "classnames": "2.2.5", + "classnames": "2.2.6", "dedent": "^0.7.0", "delete-empty": "^2.0.0", - "enzyme": "^3.7.0", - "enzyme-adapter-react-16": "^1.9.0", + "enzyme": "^3.9.0", + "enzyme-adapter-react-16": "^1.13.1", "enzyme-adapter-utils": "^1.10.0", "enzyme-to-json": "^3.3.4", - "eslint": "^5.16.0", - "eslint-config-prettier": "^4.1.0", - "eslint-plugin-babel": "^5.3.0", - "eslint-plugin-ban": "^1.2.0", - "eslint-plugin-import": "^2.16.0", - "eslint-plugin-jest": "^22.4.1", - "eslint-plugin-jsx-a11y": "^6.2.1", - "eslint-plugin-mocha": "^5.3.0", - "eslint-plugin-no-unsanitized": "^3.0.2", - "eslint-plugin-prefer-object-spread": "^1.2.1", - "eslint-plugin-prettier": "^3.0.1", - "eslint-plugin-react": "^7.12.4", - "eslint-plugin-react-hooks": "^1.6.0", + "eslint": "5.16.0", + "eslint-config-prettier": "4.3.0", + "eslint-plugin-babel": "5.3.0", + "eslint-plugin-ban": "1.2.0", + "eslint-plugin-import": "2.17.3", + "eslint-plugin-jest": "22.6.4", + "eslint-plugin-jsx-a11y": "6.2.1", + "eslint-plugin-mocha": "5.3.0", + "eslint-plugin-node": "9.1.0", + "eslint-plugin-no-unsanitized": "3.0.2", + "eslint-plugin-prefer-object-spread": "1.2.1", + "eslint-plugin-prettier": "3.1.0", + "eslint-plugin-react": "7.13.0", + "eslint-plugin-react-hooks": "1.6.0", "exit-hook": "^2.1.0", "faker": "1.1.0", - "fetch-mock": "7.3.0", - "geckodriver": "^1.16.2", + "fetch-mock": "7.3.3", + "geckodriver": "1.16.2", "getopts": "^2.2.4", - "grunt": "1.0.3", + "grunt": "1.0.4", "grunt-cli": "^1.2.0", "grunt-contrib-watch": "^1.1.0", "grunt-karma": "2.0.0", "grunt-peg": "^2.0.1", - "grunt-run": "0.7.0", - "gulp-babel": "^8.0.0", - "gulp-sourcemaps": "2.6.4", + "grunt-run": "0.8.1", + "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", @@ -388,12 +394,12 @@ "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.1.1", - "karma-coverage": "1.1.1", - "karma-firefox-launcher": "1.0.1", + "karma-chrome-launcher": "2.2.0", + "karma-coverage": "1.1.2", + "karma-firefox-launcher": "1.1.0", "karma-ie-launcher": "1.0.0", "karma-junit-reporter": "1.2.0", "karma-mocha": "1.3.0", @@ -401,11 +407,11 @@ "license-checker": "^16.0.0", "listr": "^0.14.1", "load-grunt-config": "0.19.2", - "mocha": "3.3.0", + "mocha": "3.5.3", "multistream": "^2.1.1", "murmurhash3js": "3.0.1", "mutation-observer": "^1.0.3", - "nock": "10.0.4", + "nock": "10.0.6", "node-sass": "^4.9.4", "normalize-path": "^3.0.0", "pixelmatch": "4.0.2", @@ -414,11 +420,11 @@ "postcss": "^7.0.5", "postcss-url": "^8.0.0", "prettier": "^1.14.3", - "proxyquire": "1.7.11", + "proxyquire": "1.8.0", "regenerate": "^1.4.0", "sass-lint": "^1.12.1", "selenium-webdriver": "^4.0.0-alpha.1", - "simple-git": "1.37.0", + "simple-git": "1.113.0", "sinon": "^7.2.2", "strip-ansi": "^3.0.1", "supertest": "^3.1.0", diff --git a/packages/elastic-datemath/package.json b/packages/elastic-datemath/package.json index 77c545709a3cf..c0e16602a162e 100644 --- a/packages/elastic-datemath/package.json +++ b/packages/elastic-datemath/package.json @@ -11,9 +11,9 @@ "kbn:watch": "yarn build --watch" }, "devDependencies": { - "@babel/cli": "^7.2.3", - "@babel/preset-env": "^7.3.4", - "babel-plugin-add-module-exports": "^1.0.0", + "@babel/cli": "7.4.4", + "@babel/preset-env": "7.4.5", + "babel-plugin-add-module-exports": "1.0.2", "moment": "^2.13.0" }, "dependencies": { diff --git a/packages/eslint-config-kibana/.eslintrc.js b/packages/eslint-config-kibana/.eslintrc.js index 6d39eb98ddb4b..c3e8f5601e479 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 a5433fa6b5d97..f1ed905779fbc 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/package.json b/packages/eslint-config-kibana/package.json index 5902b3c9fcad4..82819dae5799f 100644 --- a/packages/eslint-config-kibana/package.json +++ b/packages/eslint-config-kibana/package.json @@ -17,17 +17,17 @@ "peerDependencies": { "@typescript-eslint/eslint-plugin": "^1.6.0", "@typescript-eslint/parser": "^1.6.0", - "babel-eslint": "^10.0.1", - "eslint": "^5.16.0", - "eslint-plugin-babel": "^5.3.0", - "eslint-plugin-ban": "^1.2.0", - "eslint-plugin-jsx-a11y": "^6.2.1", - "eslint-plugin-import": "^2.16.0", + "babel-eslint": "10.0.1", + "eslint": "5.16.0", + "eslint-plugin-babel": "5.3.0", + "eslint-plugin-ban": "1.2.0", + "eslint-plugin-jsx-a11y": "6.2.1", + "eslint-plugin-import": "2.17.3", "eslint-plugin-jest": "^22.4.1", "eslint-plugin-mocha": "^5.3.0", - "eslint-plugin-no-unsanitized": "^3.0.2", - "eslint-plugin-prefer-object-spread": "^1.2.1", - "eslint-plugin-react": "^7.12.4", - "eslint-plugin-react-hooks": "^1.6.0" + "eslint-plugin-no-unsanitized": "3.0.2", + "eslint-plugin-prefer-object-spread": "1.2.1", + "eslint-plugin-react": "7.13.0", + "eslint-plugin-react-hooks": "1.6.0" } } diff --git a/packages/eslint-config-kibana/typescript.js b/packages/eslint-config-kibana/typescript.js index 9bce8503d32fe..26a5db48d6c29 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-babel-code-parser/package.json b/packages/kbn-babel-code-parser/package.json index addc530285659..5a61e68107e23 100755 --- a/packages/kbn-babel-code-parser/package.json +++ b/packages/kbn-babel-code-parser/package.json @@ -15,12 +15,12 @@ "kbn:watch": "yarn build --watch" }, "devDependencies": { - "@babel/cli": "^7.2.3" + "@babel/cli": "7.4.4" }, "dependencies": { "@kbn/babel-preset": "1.0.0", - "@babel/parser": "^7.3.4", - "@babel/traverse": "^7.3.4", + "@babel/parser": "7.4.5", + "@babel/traverse": "7.4.5", "lodash": "^4.17.11" } } diff --git a/packages/kbn-babel-preset/node_preset.js b/packages/kbn-babel-preset/node_preset.js index ac4dc17e63803..c4d9193357d78 100644 --- a/packages/kbn-babel-preset/node_preset.js +++ b/packages/kbn-babel-preset/node_preset.js @@ -36,7 +36,8 @@ module.exports = () => { // for just the polyfills that the target versions don't already supply // on their own useBuiltIns: 'entry', - modules: 'cjs' + modules: 'cjs', + corejs: 2, }, ], require('./common_preset'), diff --git a/packages/kbn-babel-preset/package.json b/packages/kbn-babel-preset/package.json index 86328e70b132b..0c9f50e11883b 100644 --- a/packages/kbn-babel-preset/package.json +++ b/packages/kbn-babel-preset/package.json @@ -4,13 +4,13 @@ "version": "1.0.0", "license": "Apache-2.0", "dependencies": { - "@babel/plugin-proposal-class-properties": "^7.3.4", - "@babel/preset-react":"^7.0.0", - "@babel/preset-env": "^7.3.4", - "@babel/preset-typescript": "^7.3.3", + "@babel/plugin-proposal-class-properties": "7.4.4", + "@babel/preset-react":"7.0.0", + "@babel/preset-env": "7.4.5", + "@babel/preset-typescript": "7.3.3", "@kbn/elastic-idx": "1.0.0", - "babel-plugin-add-module-exports": "^1.0.0", - "babel-plugin-transform-define": "^1.3.1", - "babel-plugin-typescript-strip-namespaces": "^1.1.1" + "babel-plugin-add-module-exports": "1.0.2", + "babel-plugin-transform-define": "1.3.1", + "babel-plugin-typescript-strip-namespaces": "1.1.1" } } diff --git a/packages/kbn-babel-preset/webpack_preset.js b/packages/kbn-babel-preset/webpack_preset.js index df5745bc33f89..c5cef9572b9f5 100644 --- a/packages/kbn-babel-preset/webpack_preset.js +++ b/packages/kbn-babel-preset/webpack_preset.js @@ -31,7 +31,8 @@ module.exports = () => { ], }, useBuiltIns: 'entry', - modules: 'cjs' + modules: 'cjs', + corejs: 2, }, ], require('./common_preset'), diff --git a/packages/kbn-config-schema/src/types/object_type.test.ts b/packages/kbn-config-schema/src/types/object_type.test.ts index e0eaabadb8ef5..e47932a3a3ed2 100644 --- a/packages/kbn-config-schema/src/types/object_type.test.ts +++ b/packages/kbn-config-schema/src/types/object_type.test.ts @@ -199,3 +199,15 @@ test('includes namespace in failure when wrong value type', () => { expect(() => type.validate(value, {}, 'foo-namespace')).toThrowErrorMatchingSnapshot(); }); + +test('individual keys can validated', () => { + const type = schema.object({ + foo: schema.boolean(), + }); + + const value = false; + expect(() => type.validateKey('foo', value)).not.toThrowError(); + expect(() => type.validateKey('bar', '')).toThrowErrorMatchingInlineSnapshot( + `"bar is not a valid part of this schema"` + ); +}); diff --git a/packages/kbn-config-schema/src/types/object_type.ts b/packages/kbn-config-schema/src/types/object_type.ts index e61fcd90ef016..7ee4014b08d0e 100644 --- a/packages/kbn-config-schema/src/types/object_type.ts +++ b/packages/kbn-config-schema/src/types/object_type.ts @@ -20,6 +20,7 @@ import typeDetect from 'type-detect'; import { AnySchema, internals } from '../internals'; import { Type, TypeOptions } from './type'; +import { ValidationError } from '../errors'; export type Props = Record>; @@ -31,6 +32,8 @@ export type TypeOf> = RT['type']; export type ObjectResultType

= Readonly<{ [K in keyof P]: TypeOf }>; export class ObjectType

extends Type> { + private props: Record; + constructor(props: P, options: TypeOptions<{ [K in keyof P]: TypeOf }> = {}) { const schemaKeys = {} as Record; for (const [key, value] of Object.entries(props)) { @@ -44,6 +47,7 @@ export class ObjectType

extends Type> .default(); super(schema, options); + this.props = schemaKeys; } protected handleError(type: string, { reason, value }: Record) { @@ -57,4 +61,15 @@ export class ObjectType

extends Type> return reason[0]; } } + + validateKey(key: string, value: any) { + if (!this.props[key]) { + throw new Error(`${key} is not a valid part of this schema`); + } + const { value: validatedValue, error } = this.props[key].validate(value); + if (error) { + throw new ValidationError(error as any, key); + } + return validatedValue; + } } diff --git a/packages/kbn-dev-utils/package.json b/packages/kbn-dev-utils/package.json index 4fbc80d6e679a..5cd347f9808b3 100644 --- a/packages/kbn-dev-utils/package.json +++ b/packages/kbn-dev-utils/package.json @@ -19,9 +19,9 @@ "tslib": "^1.9.3" }, "devDependencies": { - "@babel/cli": "^7.2.3", + "@babel/cli": "7.4.4", "@kbn/babel-preset": "1.0.0", "@kbn/expect": "1.0.0", - "chance": "1.0.6" + "chance": "1.0.18" } } diff --git a/packages/kbn-elastic-idx/package.json b/packages/kbn-elastic-idx/package.json index 5d6935eda125b..ff80b2afe30d5 100644 --- a/packages/kbn-elastic-idx/package.json +++ b/packages/kbn-elastic-idx/package.json @@ -17,8 +17,8 @@ "test": "jest" }, "devDependencies": { - "@babel/core": "^7.3.4", - "@babel/plugin-transform-async-to-generator": "^7.4.0", + "@babel/core": "7.4.5", + "@babel/plugin-transform-async-to-generator": "7.4.4", "jest": "^24.1.0", "typescript": "^3.3.3333" }, diff --git a/packages/kbn-es-query/package.json b/packages/kbn-es-query/package.json index cb6076191f2ba..68b6944b29faf 100644 --- a/packages/kbn-es-query/package.json +++ b/packages/kbn-es-query/package.json @@ -16,16 +16,16 @@ "@kbn/i18n": "1.0.0" }, "devDependencies": { - "@babel/cli": "^7.2.3", - "@babel/core": "^7.3.4", - "@babel/plugin-proposal-class-properties": "^7.3.4", - "@babel/plugin-proposal-object-rest-spread": "^7.3.4", - "@babel/preset-env": "^7.3.4", - "@babel/preset-typescript": "^7.3.3", + "@babel/cli": "7.4.4", + "@babel/core": "7.4.5", + "@babel/plugin-proposal-class-properties": "7.4.4", + "@babel/plugin-proposal-object-rest-spread": "7.4.4", + "@babel/preset-env": "7.4.5", + "@babel/preset-typescript": "7.3.3", "@kbn/babel-preset": "1.0.0", "@kbn/dev-utils": "1.0.0", "@kbn/expect": "1.0.0", - "del": "^3.0.0", + "del": "^4.0.0", "getopts": "^2.2.4", "supports-color": "^6.1.0", "typescript": "^3.3.3333" diff --git a/packages/kbn-es-query/src/filters/__tests__/range.js b/packages/kbn-es-query/src/filters/__tests__/range.js index 595b386a32ab8..9b23fc23908d4 100644 --- a/packages/kbn-es-query/src/filters/__tests__/range.js +++ b/packages/kbn-es-query/src/filters/__tests__/range.js @@ -63,8 +63,8 @@ describe('Filter Manager', function () { it('should wrap painless scripts in comparator lambdas', function () { const field = getField(indexPattern, 'script date'); - const expected = `boolean gte(Supplier s, def v) {return s.get() >= v} ` + - `boolean lte(Supplier s, def v) {return s.get() <= v}` + + const expected = `boolean gte(Supplier s, def v) {return !s.get().toInstant().isBefore(Instant.parse(v))} ` + + `boolean lte(Supplier s, def v) {return !s.get().toInstant().isAfter(Instant.parse(v))}` + `gte(() -> { ${field.script} }, params.gte) && ` + `lte(() -> { ${field.script} }, params.lte)`; diff --git a/packages/kbn-es-query/src/filters/lib/meta_filter.ts b/packages/kbn-es-query/src/filters/lib/meta_filter.ts index db5b308a01da3..48ec04503fde0 100644 --- a/packages/kbn-es-query/src/filters/lib/meta_filter.ts +++ b/packages/kbn-es-query/src/filters/lib/meta_filter.ts @@ -40,7 +40,7 @@ export interface FilterMeta { export interface Filter { $state: FilterState; meta: FilterMeta; - query?: any; + query?: object; } export interface LatLon { diff --git a/packages/kbn-es-query/src/filters/query.js b/packages/kbn-es-query/src/filters/query.js index cfb1a6d36d9e2..ded877231bf67 100644 --- a/packages/kbn-es-query/src/filters/query.js +++ b/packages/kbn-es-query/src/filters/query.js @@ -18,11 +18,17 @@ */ // Creates a filter corresponding to a raw Elasticsearch query DSL object -export function buildQueryFilter(query, index) { - return { +export function buildQueryFilter(query, index, alias) { + const filter = { query: query, meta: { - index: index + index, } }; + + if (alias) { + filter.meta.alias = alias; + } + + return filter; } diff --git a/packages/kbn-es-query/src/filters/range.js b/packages/kbn-es-query/src/filters/range.js index 218a112a1610b..357f9209c50de 100644 --- a/packages/kbn-es-query/src/filters/range.js +++ b/packages/kbn-es-query/src/filters/range.js @@ -32,6 +32,13 @@ const comparators = { lt: 'boolean lt(Supplier s, def v) {return s.get() < v}', }; +const dateComparators = { + gt: 'boolean gt(Supplier s, def v) {return s.get().toInstant().isAfter(Instant.parse(v))}', + gte: 'boolean gte(Supplier s, def v) {return !s.get().toInstant().isBefore(Instant.parse(v))}', + lte: 'boolean lte(Supplier s, def v) {return !s.get().toInstant().isAfter(Instant.parse(v))}', + lt: 'boolean lt(Supplier s, def v) {return s.get().toInstant().isBefore(Instant.parse(v))}', +}; + function formatValue(field, params) { return _.map(params, (val, key) => operators[key] + format(field, val)).join(' '); } @@ -87,7 +94,8 @@ export function getRangeScript(field, params) { // We must wrap painless scripts in a lambda in case they're more than a simple expression if (field.lang === 'painless') { - const currentComparators = _.reduce(knownParams, (acc, val, key) => acc.concat(comparators[key]), []).join(' '); + const comp = field.type === 'date' ? dateComparators : comparators; + const currentComparators = _.reduce(knownParams, (acc, val, key) => acc.concat(comp[key]), []).join(' '); const comparisons = _.map(knownParams, function (val, key) { return `${key}(() -> { ${field.script} }, params.${key})`; diff --git a/packages/kbn-es/package.json b/packages/kbn-es/package.json index a645502bb6968..21a7ed47718b4 100644 --- a/packages/kbn-es/package.json +++ b/packages/kbn-es/package.json @@ -10,7 +10,7 @@ "abort-controller": "^2.0.3", "chalk": "^2.4.1", "dedent": "^0.7.0", - "del": "^3.0.0", + "del": "^4.0.0", "execa": "^1.0.0", "getopts": "^2.2.4", "glob": "^7.1.2", diff --git a/packages/kbn-es/src/cli_commands/archive.js b/packages/kbn-es/src/cli_commands/archive.js index 0d1096ab59f96..273cdea87c67c 100644 --- a/packages/kbn-es/src/cli_commands/archive.js +++ b/packages/kbn-es/src/cli_commands/archive.js @@ -64,5 +64,5 @@ exports.run = async (defaults = {}) => { } const { installPath } = await cluster.installArchive(path, options); - await cluster.run(installPath, { esArgs: options.esArgs }); + await cluster.run(installPath, options); }; diff --git a/packages/kbn-es/src/cli_commands/source.js b/packages/kbn-es/src/cli_commands/source.js index 7acad33ca80bf..3065ca887bfff 100644 --- a/packages/kbn-es/src/cli_commands/source.js +++ b/packages/kbn-es/src/cli_commands/source.js @@ -29,13 +29,14 @@ exports.help = (defaults = {}) => { return dedent` Options: - --license Run with a 'oss', 'basic', or 'trial' license [default: ${license}] - --source-path Path to ES source [default: ${defaults['source-path']}] - --base-path Path containing cache/installations [default: ${basePath}] - --install-path Installation path, defaults to 'source' within base-path - --data-archive Path to zip or tarball containing an ES data directory to seed the cluster with. - --password Sets password for elastic user [default: ${password}] - -E Additional key=value settings to pass to Elasticsearch + --license Run with a 'oss', 'basic', or 'trial' license [default: ${license}] + --source-path Path to ES source [default: ${defaults['source-path']}] + --base-path Path containing cache/installations [default: ${basePath}] + --install-path Installation path, defaults to 'source' within base-path + --data-archive Path to zip or tarball containing an ES data directory to seed the cluster with. + --password Sets password for elastic user [default: ${password}] + --password.[user] Sets password for native realm user [default: ${password}] + -E Additional key=value settings to pass to Elasticsearch Example: @@ -64,5 +65,5 @@ exports.run = async (defaults = {}) => { await cluster.extractDataDirectory(installPath, options.dataArchive); } - await cluster.run(installPath, { esArgs: options.esArgs }); + await cluster.run(installPath, options); }; diff --git a/packages/kbn-es/src/cluster.js b/packages/kbn-es/src/cluster.js index 9ea1817e0f49b..35a902e384599 100644 --- a/packages/kbn-es/src/cluster.js +++ b/packages/kbn-es/src/cluster.js @@ -33,6 +33,19 @@ const { createCliError } = require('./errors'); const { promisify } = require('util'); const treeKillAsync = promisify(require('tree-kill')); +// listen to data on stream until map returns anything but undefined +const first = (stream, map) => + new Promise(resolve => { + const onData = data => { + const result = map(data); + if (result !== undefined) { + resolve(result); + stream.removeListener('data', onData); + } + }; + stream.on('data', onData); + }); + exports.Cluster = class Cluster { constructor(log = defaultLog) { this._log = log; @@ -158,14 +171,15 @@ exports.Cluster = class Cluster { this._exec(installPath, options); await Promise.race([ - // await the "started" log message - new Promise(resolve => { - this._process.stdout.on('data', data => { + // wait for native realm to be setup and es to be started + Promise.all([ + first(this._process.stdout, data => { if (/started/.test(data)) { - resolve(); + return true; } - }); - }), + }), + this._nativeRealmSetup, + ]), // await the outcome of the process in case it exits before starting this._outcome.then(() => { @@ -185,6 +199,12 @@ exports.Cluster = class Cluster { async run(installPath, options = {}) { this._exec(installPath, options); + // log native realm setup errors so they aren't uncaught + this._nativeRealmSetup.catch(error => { + this._log.error(error); + this.stop(); + }); + // await the final outcome of the process await this._outcome; } @@ -238,45 +258,50 @@ exports.Cluster = class Cluster { this._process = execa(ES_BIN, args, { cwd: installPath, + env: { + ...process.env, + ...(options.esEnvVars || {}), + }, stdio: ['ignore', 'pipe', 'pipe'], }); + // parse log output to find http port + const httpPort = first(this._process.stdout, data => { + const match = data.toString('utf8').match(/HttpServer.+publish_address {[0-9.]+:([0-9]+)/); + + if (match) { + return match[1]; + } + }); + + // once the http port is available setup the native realm + this._nativeRealmSetup = httpPort.then(async port => { + const nativeRealm = new NativeRealm(options.password, port, this._log); + await nativeRealm.setPasswords(options); + }); + + // parse and forward es stdout to the log this._process.stdout.on('data', data => { const lines = parseEsLog(data.toString()); lines.forEach(line => { this._log.info(line.formattedMessage); - - // once we have the port we can stop checking for it - if (this.httpPort) { - return; - } - - const httpAddressMatch = line.message.match( - /HttpServer.+publish_address {[0-9.]+:([0-9]+)/ - ); - - if (httpAddressMatch) { - this.httpPort = httpAddressMatch[1]; - new NativeRealm(options.password, this.httpPort, this._log).setPasswords(options); - } }); }); + // forward es stderr to the log this._process.stderr.on('data', data => this._log.error(chalk.red(data.toString()))); - this._outcome = new Promise((resolve, reject) => { - this._process.once('exit', code => { - if (this._stopCalled) { - resolve(); - return; - } - // JVM exits with 143 on SIGTERM and 130 on SIGINT, dont' treat them as errors - if (code > 0 && !(code === 143 || code === 130)) { - reject(createCliError(`ES exited with code ${code}`)); - } else { - resolve(); - } - }); + // observe the exit code of the process and reflect in _outcome promies + const exitCode = new Promise(resolve => this._process.once('exit', resolve)); + this._outcome = exitCode.then(code => { + if (this._stopCalled) { + return; + } + + // JVM exits with 143 on SIGTERM and 130 on SIGINT, dont' treat them as errors + if (code > 0 && !(code === 143 || code === 130)) { + throw createCliError(`ES exited with code ${code}`); + } }); } }; diff --git a/packages/kbn-es/src/integration_tests/__fixtures__/es_bin.js b/packages/kbn-es/src/integration_tests/__fixtures__/es_bin.js index d07d5e24a641f..6c49371fd7d40 100644 --- a/packages/kbn-es/src/integration_tests/__fixtures__/es_bin.js +++ b/packages/kbn-es/src/integration_tests/__fixtures__/es_bin.js @@ -19,10 +19,61 @@ * under the License. */ +const { createServer } = require('http'); +const { format: formatUrl } = require('url'); const { exitCode, start } = JSON.parse(process.argv[2]); -if (start) { - console.log('started'); +process.exitCode = exitCode; + +if (!start) { + return; } -process.exitCode = exitCode; +let serverUrl; +const server = createServer((req, res) => { + const url = new URL(req.url, serverUrl); + const send = (code, body) => { + res.writeHead(code, { 'content-type': 'application/json' }); + res.end(JSON.stringify(body)); + }; + + if (url.pathname === '/_xpack') { + return send(400, { + error: { + reason: 'foo bar', + }, + }); + } + + return send(404, { + error: { + reason: 'not found', + }, + }); +}); + +// setup server auto close after 1 second of silence +let serverCloseTimer; +const delayServerClose = () => { + clearTimeout(serverCloseTimer); + serverCloseTimer = setTimeout(() => server.close(), 1000); +}; +server.on('request', delayServerClose); +server.on('listening', delayServerClose); + +server.listen(0, '127.0.0.1', function() { + const { port, address: hostname } = server.address(); + serverUrl = new URL( + formatUrl({ + protocol: 'http:', + port, + hostname, + }) + ); + + console.log( + `[o.e.h.AbstractHttpServerTransport] [computer] publish_address {127.0.0.1:${port}}, bound_addresses {[::1]:${port}}, {127.0.0.1:${port}}` + ); + + console.log('started'); +}); diff --git a/packages/kbn-es/src/utils/native_realm.js b/packages/kbn-es/src/utils/native_realm.js index fa3bda4752d59..f0010828afc37 100644 --- a/packages/kbn-es/src/utils/native_realm.js +++ b/packages/kbn-es/src/utils/native_realm.js @@ -1,82 +1,108 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -const { Client } = require('@elastic/elasticsearch'); -const chalk = require('chalk'); - -const { log: defaultLog } = require('./log'); - -exports.NativeRealm = class NativeRealm { - constructor(elasticPassword, port, log = defaultLog) { - this._client = new Client({ node: `http://elastic:${elasticPassword}@localhost:${port}` }); - this._elasticPassword = elasticPassword; - this._log = log; - } - - async setPassword(username, password = this._elasticPassword) { - this._log.info(`setting ${chalk.bold(username)} password to ${chalk.bold(password)}`); - - try { - await this._client.security.changePassword({ - username, - refresh: 'wait_for', - body: { - password, - }, - }); - } catch (e) { - this._log.error( - chalk.red(`unable to set password for ${chalk.bold(username)}: ${e.message}`) - ); - } - } - - async setPasswords(options) { - if (!(await this.isSecurityEnabled())) { - this._log.info('security is not enabled, unable to set native realm passwords'); - return; - } - - (await this.getReservedUsers()).forEach(user => { - this.setPassword(user, options[`password.${user}`]); - }); - } - - async getReservedUsers() { - const users = await this._client.security.getUser(); - - return Object.keys(users.body).reduce((acc, user) => { - if (users.body[user].metadata._reserved === true) { - acc.push(user); - } - return acc; - }, []); - } - - async isSecurityEnabled() { - try { - const { - body: { features }, - } = await this._client.xpack.info({ categories: 'features' }); - return features.security && features.security.enabled && features.security.available; - } catch (e) { - return false; - } - } -}; +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const { Client } = require('@elastic/elasticsearch'); +const chalk = require('chalk'); + +const { log: defaultLog } = require('./log'); + +exports.NativeRealm = class NativeRealm { + constructor(elasticPassword, port, log = defaultLog) { + this._client = new Client({ node: `http://elastic:${elasticPassword}@localhost:${port}` }); + this._elasticPassword = elasticPassword; + this._log = log; + } + + async setPassword(username, password = this._elasticPassword, { attempt = 1 } = {}) { + await this._autoRetry(async () => { + this._log.info( + (attempt > 1 ? `attempt ${attempt}: ` : '') + + `setting ${chalk.bold(username)} password to ${chalk.bold(password)}` + ); + + await this._client.security.changePassword({ + username, + refresh: 'wait_for', + body: { + password, + }, + }); + }); + } + + async setPasswords(options) { + if (!(await this.isSecurityEnabled())) { + this._log.info('security is not enabled, unable to set native realm passwords'); + return; + } + + const reservedUsers = await this.getReservedUsers(); + await Promise.all( + reservedUsers.map(async user => { + await this.setPassword(user, options[`password.${user}`]); + }) + ); + } + + async getReservedUsers() { + const users = await this._autoRetry(async () => { + return await this._client.security.getUser(); + }); + + return Object.keys(users.body).reduce((acc, user) => { + if (users.body[user].metadata._reserved === true) { + acc.push(user); + } + return acc; + }, []); + } + + async isSecurityEnabled() { + try { + return await this._autoRetry(async () => { + const { + body: { features }, + } = await this._client.xpack.info({ categories: 'features' }); + return features.security && features.security.enabled && features.security.available; + }); + } catch (error) { + if (error.meta && error.meta.statusCode === 400) { + return false; + } + + throw error; + } + } + + async _autoRetry(fn, attempt = 1) { + try { + return await fn(attempt); + } catch (error) { + if (attempt >= 3) { + throw error; + } + + this._log.warning( + 'assuming [elastic] user not available yet, waiting 1.5 seconds and trying again' + ); + await new Promise(resolve => setTimeout(resolve, 1500)); + return await this._autoRetry(fn, attempt + 1); + } + } +}; diff --git a/packages/kbn-es/src/utils/native_realm.test.js b/packages/kbn-es/src/utils/native_realm.test.js index 7dccb5eeef5b6..ae3c615f1ca25 100644 --- a/packages/kbn-es/src/utils/native_realm.test.js +++ b/packages/kbn-es/src/utils/native_realm.test.js @@ -22,6 +22,7 @@ const { NativeRealm } = require('./native_realm'); jest.genMockFromModule('@elastic/elasticsearch'); jest.mock('@elastic/elasticsearch'); +const { ToolingLog } = require('@kbn/dev-utils'); const { Client } = require('@elastic/elasticsearch'); const mockClient = { @@ -35,11 +36,7 @@ const mockClient = { }; Client.mockImplementation(() => mockClient); -const log = { - error: jest.fn(), - info: jest.fn(), -}; - +const log = new ToolingLog(); let nativeRealm; beforeEach(() => { @@ -79,12 +76,31 @@ describe('isSecurityEnabled', () => { expect(await nativeRealm.isSecurityEnabled()).toBe(false); }); - test('logs exception and returns false', async () => { + test('returns false if 400 error returned', async () => { mockClient.xpack.info.mockImplementation(() => { - throw new Error('ResponseError'); + const error = new Error('ResponseError'); + error.meta = { + statusCode: 400, + }; + throw error; }); + expect(await nativeRealm.isSecurityEnabled()).toBe(false); }); + + test('rejects if unexpected error is thrown', async () => { + mockClient.xpack.info.mockImplementation(() => { + const error = new Error('ResponseError'); + error.meta = { + statusCode: 500, + }; + throw error; + }); + + await expect(nativeRealm.isSecurityEnabled()).rejects.toThrowErrorMatchingInlineSnapshot( + `"ResponseError"` + ); + }); }); describe('setPasswords', () => { @@ -204,18 +220,13 @@ describe('setPassword', () => { }); }); - it('logs error', async () => { + it('rejects with errors', async () => { mockClient.security.changePassword.mockImplementation(() => { throw new Error('SomeError'); }); - await nativeRealm.setPassword('kibana', 'foo'); - expect(log.error.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "unable to set password for kibana: SomeError", - ], -] -`); + await expect( + nativeRealm.setPassword('kibana', 'foo') + ).rejects.toThrowErrorMatchingInlineSnapshot(`"SomeError"`); }); }); diff --git a/packages/kbn-eslint-import-resolver-kibana/package.json b/packages/kbn-eslint-import-resolver-kibana/package.json index a16cc1e128b74..144fdb5604231 100755 --- a/packages/kbn-eslint-import-resolver-kibana/package.json +++ b/packages/kbn-eslint-import-resolver-kibana/package.json @@ -11,8 +11,8 @@ }, "dependencies": { "debug": "^2.6.6", - "eslint-import-resolver-node": "^0.3.2", - "eslint-import-resolver-webpack": "^0.10.1", + "eslint-import-resolver-node": "0.3.2", + "eslint-import-resolver-webpack": "0.11.1", "glob-all": "^3.1.0", "lru-cache": "^4.1.3", "resolve": "^1.7.1", diff --git a/packages/kbn-eslint-plugin-eslint/package.json b/packages/kbn-eslint-plugin-eslint/package.json index 00ac1631a3950..70750b90acd02 100644 --- a/packages/kbn-eslint-plugin-eslint/package.json +++ b/packages/kbn-eslint-plugin-eslint/package.json @@ -4,12 +4,12 @@ "private": true, "license": "Apache-2.0", "peerDependencies": { - "eslint": "^5.14.1", - "babel-eslint": "^10.0.1" + "eslint": "5.16.0", + "babel-eslint": "10.0.1" }, "dependencies": { "micromatch": "3.1.10", "dedent": "^0.7.0", - "eslint-module-utils": "^2.3.0" + "eslint-module-utils": "2.4.0" } } 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 61fc1c112b8b2..0bf71d717bad6 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 33d178519d069..11f01a1777ab6 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 491ceb2290be1..984c090ceb06b 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-i18n/GUIDELINE.md b/packages/kbn-i18n/GUIDELINE.md index e33af4b7c35f1..45408dc6582b5 100644 --- a/packages/kbn-i18n/GUIDELINE.md +++ b/packages/kbn-i18n/GUIDELINE.md @@ -84,18 +84,24 @@ In case when `indicesLength` has value 1, the result string will be "`1 index`". #### In ReactJS +The long term plan is to rely on using `FormattedMessage` and `i18n.translate()` by statically importing `i18n` from the `@kbn/i18n` package. **Avoid using `injectI18n` and use `i18n.translate()` instead.** + - You should use `` most of the time. -- In case when the string is expected (`aria-label`, `placeholder`), use `props.intl.formatmessage()` (where `intl` is passed to `props` by `injectI18n` HOC). -- In case if none of the above can not be applied (e.g. it's needed to translate any code that doesn't have access to the component props), you can call JS function `i18n.translate()` from `@kbn/i18n` package. +- In the case where the string is expected (`aria-label`, `placeholder`), Call JS function `i18n.translate()` from the`@kbn/i18n` package. + +Currently, we support the following ReactJS `i18n` tools, but they will be removed in future releases: +- Usage of `props.intl.formatmessage()` (where `intl` is passed to `props` by `injectI18n` HOC). #### In AngularJS -- Use `i18n` service in controllers, directives, services by injected it. +The long term plan is to rely on using `i18n.translate()` by statically importing `i18n` from the `@kbn/i18n` package. **Avoid using the `i18n` filter and the `i18n` service injected in controllers, directives, services.** + +- Call JS function `i18n.translate()` from the `@kbn/i18n` package. - Use `i18nId` directive in template. -- Use `i18n` filter in template for attribute translation. -- In case if none of the above can not be applied, you can call JS function `i18n.translate()` from `@kbn/i18n` package. -Note: Use one-time binding ("{{:: ... }}") in filters wherever it's possible to prevent unnecessary expression re-evaluation. +Currently, we support the following AngluarJS `i18n` tools, but they will be removed in future releases: +- Usage of `i18n` service in controllers, directives, services by injecting it. +- Usage of `i18n` filter in template for attribute translation. Note: Use one-time binding ("{{:: ... }}") in filters wherever it's possible to prevent unnecessary expression re-evaluation. #### In JavaScript diff --git a/packages/kbn-i18n/README.md b/packages/kbn-i18n/README.md index c4f9f05837a57..04456bf630e9a 100644 --- a/packages/kbn-i18n/README.md +++ b/packages/kbn-i18n/README.md @@ -283,6 +283,8 @@ Initial result: `1 minute ago` ### Attributes translation in React +The long term plan is to rely on using `FormattedMessage` and `i18n.translate()` by statically importing `i18n` from the `@kbn/i18n` package. **Avoid using `injectI18n` and rely on `i18n.translate()` instead.** + React wrapper provides an ability to inject the imperative formatting API into a React component via its props using `injectI18n` Higher-Order Component. This should be used when your React component needs to format data to a string value where a React element is not suitable; e.g., a `title` or `aria` attribute. In order to use it you should wrap your component with `injectI18n` Higher-Order Component. The formatting API will be provided to the wrapped component via `props.intl`. React component as a pure function: @@ -342,6 +344,8 @@ export const MyComponent = injectI18n( ## AngularJS +The long term plan is to rely on using `i18n.translate()` by statically importing `i18n` from the `@kbn/i18n` package. **Avoid using the `i18n` filter and the `i18n` service injected in controllers, directives, services.** + AngularJS wrapper has 4 entities: translation `provider`, `service`, `directive` and `filter`. Both the directive and the filter use the translation `service` with i18n engine under the hood. diff --git a/packages/kbn-i18n/package.json b/packages/kbn-i18n/package.json index 86d8a3a3647e8..23c8e71f3dd02 100644 --- a/packages/kbn-i18n/package.json +++ b/packages/kbn-i18n/package.json @@ -12,17 +12,17 @@ "kbn:watch": "node scripts/build --watch --source-maps" }, "devDependencies": { - "@babel/cli": "^7.2.3", - "@babel/core": "^7.3.4", - "@babel/plugin-proposal-class-properties": "^7.3.4", - "@babel/plugin-proposal-object-rest-spread": "^7.3.4", - "@babel/preset-env": "^7.3.4", - "@babel/preset-react": "^7.0.0", - "@babel/preset-typescript": "^7.3.3", + "@babel/cli": "7.4.4", + "@babel/core": "7.4.5", + "@babel/plugin-proposal-class-properties": "7.4.4", + "@babel/plugin-proposal-object-rest-spread": "7.4.4", + "@babel/preset-env": "7.4.5", + "@babel/preset-react": "7.0.0", + "@babel/preset-typescript": "7.3.3", "@kbn/dev-utils": "1.0.0", "@types/intl-relativeformat": "^2.1.0", "@types/react-intl": "^2.3.15", - "del": "^3.0.0", + "del": "^4.0.0", "getopts": "^2.2.4", "supports-color": "^6.1.0", "typescript": "^3.3.3333" diff --git a/packages/kbn-interpreter/package.json b/packages/kbn-interpreter/package.json index 216c3fa1ffdfa..be4d39d9b178b 100644 --- a/packages/kbn-interpreter/package.json +++ b/packages/kbn-interpreter/package.json @@ -9,29 +9,29 @@ "kbn:watch": "node scripts/build --dev --watch" }, "dependencies": { - "@babel/runtime": "^7.3.4", + "@babel/runtime": "7.4.5", "@kbn/i18n": "1.0.0", "lodash": "npm:@elastic/lodash@3.10.1-kibana1", "lodash.clone": "^4.5.0", "uuid": "3.0.1" }, "devDependencies": { - "@babel/cli": "^7.2.3", - "@babel/core": "7.3.4", - "@babel/plugin-transform-runtime": "^7.3.4", - "@babel/polyfill": "7.2.5", + "@babel/cli": "7.4.4", + "@babel/core": "7.4.5", + "@babel/plugin-transform-runtime": "7.4.4", + "@babel/polyfill": "7.4.4", "@kbn/babel-preset": "1.0.0", "@kbn/dev-utils": "1.0.0", - "babel-loader": "8.0.5", - "copy-webpack-plugin": "^4.6.0", - "css-loader": "1.0.0", - "del": "^3.0.0", + "babel-loader": "8.0.6", + "copy-webpack-plugin": "^5.0.0", + "css-loader": "2.1.1", + "del": "^4.0.0", "getopts": "^2.2.4", - "pegjs": "0.9.0", + "pegjs": "0.10.0", "sass-loader": "^7.1.0", "style-loader": "0.23.1", "supports-color": "^5.5.0", - "url-loader": "1.1.2", + "url-loader": "2.0.0", "webpack": "4.23.1", "webpack-cli": "^3.1.2" } diff --git a/packages/kbn-interpreter/src/common/index.d.ts b/packages/kbn-interpreter/src/common/index.d.ts index b41f9f318a13b..a8917b7a65df1 100644 --- a/packages/kbn-interpreter/src/common/index.d.ts +++ b/packages/kbn-interpreter/src/common/index.d.ts @@ -18,3 +18,5 @@ */ export { Registry } from './lib/registry'; + +export { fromExpression, Ast } from './lib/ast'; diff --git a/packages/kbn-interpreter/src/common/lib/ast.d.ts b/packages/kbn-interpreter/src/common/lib/ast.d.ts new file mode 100644 index 0000000000000..2b0328bda9392 --- /dev/null +++ b/packages/kbn-interpreter/src/common/lib/ast.d.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export type Ast = unknown; + +export declare function fromExpression(expression: string): Ast; diff --git a/packages/kbn-plugin-helpers/package.json b/packages/kbn-plugin-helpers/package.json index 883b584d65f35..82747ccd0e073 100644 --- a/packages/kbn-plugin-helpers/package.json +++ b/packages/kbn-plugin-helpers/package.json @@ -9,14 +9,19 @@ }, "author": "Spencer Alger ", "license": "Apache-2.0", + "peerDependencies": { + "@kbn/babel-preset": "1.0.0" + }, "dependencies": { + "@babel/core": "^7.4.4", "argv-split": "^2.0.1", "commander": "^2.9.0", - "del": "^2.2.2", + "del": "^4.0.0", "execa": "^1.0.0", - "gulp-rename": "1.2.2", "globby": "^8.0.1", - "gulp-zip": "^4.1.0", + "gulp-babel": "^8.0.0", + "gulp-rename": "1.4.0", + "gulp-zip": "4.2.0", "inquirer": "^1.2.2", "minimatch": "^3.0.4", "node-sass": "^4.9.4", diff --git a/packages/kbn-plugin-helpers/tasks/build/create_build.js b/packages/kbn-plugin-helpers/tasks/build/create_build.js index 250d44b5a9a63..7d42dc13ef278 100644 --- a/packages/kbn-plugin-helpers/tasks/build/create_build.js +++ b/packages/kbn-plugin-helpers/tasks/build/create_build.js @@ -27,10 +27,15 @@ const vfs = require('vinyl-fs'); const rename = require('gulp-rename'); const through = require('through2'); const minimatch = require('minimatch'); +const gulpBabel = require('gulp-babel'); +const { promisify } = require('util'); +const { pipeline } = require('stream'); const rewritePackageJson = require('./rewrite_package_json'); const winCmd = require('../../lib/win_cmd'); +const asyncPipeline = promisify(pipeline); + // `link:` dependencies create symlinks, but we don't want to include symlinks // in the built zip file. Therefore we remove all symlinked dependencies, so we // can re-create them when installing the plugin. @@ -65,6 +70,31 @@ function parseTsconfig(pluginSourcePath, configPath) { return config; } +// transpile with babel +async function transpileWithBabel(srcGlobs, buildRoot, presets) { + await asyncPipeline( + vfs.src( + srcGlobs.concat([ + '!**/*.d.ts', + '!**/*.{test,test.mocks,mock,mocks}.{ts,tsx}', + '!**/node_modules/**', + '!**/bower_components/**', + '!**/__tests__/**', + ]), + { + cwd: buildRoot, + } + ), + + gulpBabel({ + babelrc: false, + presets, + }), + + vfs.dest(buildRoot) + ); +} + module.exports = function createBuild(plugin, buildTarget, buildVersion, kibanaVersion, files) { const buildSource = plugin.root; const buildRoot = path.join(buildTarget, 'kibana', plugin.id); @@ -122,19 +152,13 @@ module.exports = function createBuild(plugin, buildTarget, buildVersion, kibanaV del.sync([path.join(buildRoot, '**', '*.s{a,c}ss')]); }) - .then(function() { + .then(async function() { const buildConfigPath = path.join(buildRoot, 'tsconfig.json'); if (!existsSync(buildConfigPath)) { return; } - if (!plugin.pkg.devDependencies.typescript) { - throw new Error( - 'Found tsconfig.json file in plugin but typescript is not a devDependency.' - ); - } - // attempt to patch the extends path in the tsconfig file const buildConfig = parseTsconfig(buildSource, buildConfigPath); @@ -144,11 +168,19 @@ module.exports = function createBuild(plugin, buildTarget, buildVersion, kibanaV writeFileSync(buildConfigPath, JSON.stringify(buildConfig)); } - execa.sync( - path.join(buildSource, 'node_modules', '.bin', winCmd('tsc')), - ['--pretty', 'true'], - { cwd: buildRoot } - ); + // Transpile ts server code + // + // Include everything except content from public folders + await transpileWithBabel(['**/*.{ts,tsx}', '!**/public/**'], buildRoot, [ + require.resolve('@kbn/babel-preset/node_preset'), + ]); + + // Transpile ts client code + // + // Include everything inside a public directory + await transpileWithBabel(['**/public/**/*.{ts,tsx}'], buildRoot, [ + require.resolve('@kbn/babel-preset/webpack_preset'), + ]); del.sync([ path.join(buildRoot, '**', '*.{ts,tsx,d.ts}'), diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 4ec9854637780..309a39b4bcc79 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -94,7 +94,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _cli__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "run", function() { return _cli__WEBPACK_IMPORTED_MODULE_0__["run"]; }); -/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(367); +/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(368); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["buildProductionProjects"]; }); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["prepareExternalProjectDependencies"]; }); @@ -152,7 +152,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(16); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_3__); /* harmony import */ var _commands__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(17); -/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(357); +/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(358); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(33); /* * Licensed to Elasticsearch B.V. under one or more contributor @@ -2503,8 +2503,8 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "commands", function() { return commands; }); /* harmony import */ var _bootstrap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(18); /* harmony import */ var _clean__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(133); -/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(155); -/* harmony import */ var _watch__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(156); +/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(156); +/* harmony import */ var _watch__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(157); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -17647,7 +17647,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(chalk__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(134); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(del__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(148); +/* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(149); /* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(ora__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(16); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_3__); @@ -17756,37 +17756,35 @@ const path = __webpack_require__(16); const globby = __webpack_require__(135); const isPathCwd = __webpack_require__(142); const isPathInCwd = __webpack_require__(143); -const pify = __webpack_require__(62); -const rimraf = __webpack_require__(146); -const pMap = __webpack_require__(147); +const pify = __webpack_require__(146); +const rimraf = __webpack_require__(147); +const pMap = __webpack_require__(148); const rimrafP = pify(rimraf); function safeCheck(file) { if (isPathCwd(file)) { - throw new Error('Cannot delete the current working directory. Can be overriden with the `force` option.'); + throw new Error('Cannot delete the current working directory. Can be overridden with the `force` option.'); } if (!isPathInCwd(file)) { - throw new Error('Cannot delete files/folders outside the current working directory. Can be overriden with the `force` option.'); + throw new Error('Cannot delete files/folders outside the current working directory. Can be overridden with the `force` option.'); } } -module.exports = (patterns, opts) => { - opts = Object.assign({}, opts); - - const force = opts.force; - delete opts.force; +const del = (patterns, options) => { + options = Object.assign({}, options); - const dryRun = opts.dryRun; - delete opts.dryRun; + const {force, dryRun} = options; + delete options.force; + delete options.dryRun; const mapper = file => { if (!force) { safeCheck(file); } - file = path.resolve(opts.cwd || '', file); + file = path.resolve(options.cwd || '', file); if (dryRun) { return file; @@ -17795,24 +17793,26 @@ module.exports = (patterns, opts) => { return rimrafP(file, {glob: false}).then(() => file); }; - return globby(patterns, opts).then(files => pMap(files, mapper, opts)); + return globby(patterns, options).then(files => pMap(files, mapper, options)); }; -module.exports.sync = (patterns, opts) => { - opts = Object.assign({}, opts); +module.exports = del; +// TODO: Remove this for the next major release +module.exports.default = del; - const force = opts.force; - delete opts.force; +module.exports.sync = (patterns, options) => { + options = Object.assign({}, options); - const dryRun = opts.dryRun; - delete opts.dryRun; + const {force, dryRun} = options; + delete options.force; + delete options.dryRun; - return globby.sync(patterns, opts).map(file => { + return globby.sync(patterns, options).map(file => { if (!force) { safeCheck(file); } - file = path.resolve(opts.cwd || '', file); + file = path.resolve(options.cwd || '', file); if (!dryRun) { rimraf.sync(file, {glob: false}); @@ -18487,11 +18487,9 @@ pify.all = pify; "use strict"; -var path = __webpack_require__(16); +const path = __webpack_require__(16); -module.exports = function (str) { - return path.resolve(str) === path.resolve(process.cwd()); -}; +module.exports = path_ => path.resolve(path_) === process.cwd(); /***/ }), @@ -18500,11 +18498,9 @@ module.exports = function (str) { "use strict"; -var isPathInside = __webpack_require__(144); +const isPathInside = __webpack_require__(144); -module.exports = function (str) { - return isPathInside(str, process.cwd()); -}; +module.exports = path => isPathInside(path, process.cwd()); /***/ }), @@ -18513,18 +18509,18 @@ module.exports = function (str) { "use strict"; -var path = __webpack_require__(16); -var pathIsInside = __webpack_require__(145); +const path = __webpack_require__(16); +const pathIsInside = __webpack_require__(145); -module.exports = function (a, b) { - a = path.resolve(a); - b = path.resolve(b); +module.exports = (childPath, parentPath) => { + childPath = path.resolve(childPath); + parentPath = path.resolve(parentPath); - if (a === b) { + if (childPath === parentPath) { return false; } - return pathIsInside(a, b); + return pathIsInside(childPath, parentPath); }; @@ -18567,6 +18563,81 @@ function stripTrailingSep(thePath) { /* 146 */ /***/ (function(module, exports, __webpack_require__) { +"use strict"; + + +const processFn = (fn, options) => function (...args) { + const P = options.promiseModule; + + return new P((resolve, reject) => { + if (options.multiArgs) { + args.push((...result) => { + if (options.errorFirst) { + if (result[0]) { + reject(result); + } else { + result.shift(); + resolve(result); + } + } else { + resolve(result); + } + }); + } else if (options.errorFirst) { + args.push((error, result) => { + if (error) { + reject(error); + } else { + resolve(result); + } + }); + } else { + args.push(resolve); + } + + fn.apply(this, args); + }); +}; + +module.exports = (input, options) => { + options = Object.assign({ + exclude: [/.+(Sync|Stream)$/], + errorFirst: true, + promiseModule: Promise + }, options); + + const objType = typeof input; + if (!(input !== null && (objType === 'object' || objType === 'function'))) { + throw new TypeError(`Expected \`input\` to be a \`Function\` or \`Object\`, got \`${input === null ? 'null' : objType}\``); + } + + const filter = key => { + const match = pattern => typeof pattern === 'string' ? key === pattern : pattern.test(key); + return options.include ? options.include.some(match) : !options.exclude.some(match); + }; + + let ret; + if (objType === 'function') { + ret = function (...args) { + return options.excludeMain ? input(...args) : processFn(input, options).apply(this, args); + }; + } else { + ret = Object.create(Object.getPrototypeOf(input)); + } + + for (const key in input) { // eslint-disable-line guard-for-in + const property = input[key]; + ret[key] = typeof property === 'function' && filter(key) ? processFn(property, options) : property; + } + + return ret; +}; + + +/***/ }), +/* 147 */ +/***/ (function(module, exports, __webpack_require__) { + module.exports = rimraf rimraf.sync = rimrafSync @@ -18934,21 +19005,22 @@ function rmkidsSync (p, options) { /***/ }), -/* 147 */ +/* 148 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -module.exports = (iterable, mapper, opts) => new Promise((resolve, reject) => { - opts = Object.assign({ + +const pMap = (iterable, mapper, options) => new Promise((resolve, reject) => { + options = Object.assign({ concurrency: Infinity - }, opts); + }, options); if (typeof mapper !== 'function') { throw new TypeError('Mapper function is required'); } - const concurrency = opts.concurrency; + const {concurrency} = options; if (!(typeof concurrency === 'number' && concurrency >= 1)) { throw new TypeError(`Expected \`concurrency\` to be a number from 1 and up, got \`${concurrency}\` (${typeof concurrency})`); @@ -18957,9 +19029,9 @@ module.exports = (iterable, mapper, opts) => new Promise((resolve, reject) => { const ret = []; const iterator = iterable[Symbol.iterator](); let isRejected = false; - let iterableDone = false; + let isIterableDone = false; let resolvingCount = 0; - let currentIdx = 0; + let currentIndex = 0; const next = () => { if (isRejected) { @@ -18967,11 +19039,11 @@ module.exports = (iterable, mapper, opts) => new Promise((resolve, reject) => { } const nextItem = iterator.next(); - const i = currentIdx; - currentIdx++; + const i = currentIndex; + currentIndex++; if (nextItem.done) { - iterableDone = true; + isIterableDone = true; if (resolvingCount === 0) { resolve(ret); @@ -18983,16 +19055,16 @@ module.exports = (iterable, mapper, opts) => new Promise((resolve, reject) => { resolvingCount++; Promise.resolve(nextItem.value) - .then(el => mapper(el, i)) + .then(element => mapper(element, i)) .then( - val => { - ret[i] = val; + value => { + ret[i] = value; resolvingCount--; next(); }, - err => { + error => { isRejected = true; - reject(err); + reject(error); } ); }; @@ -19000,22 +19072,26 @@ module.exports = (iterable, mapper, opts) => new Promise((resolve, reject) => { for (let i = 0; i < concurrency; i++) { next(); - if (iterableDone) { + if (isIterableDone) { break; } } }); +module.exports = pMap; +// TODO: Remove this for the next major release +module.exports.default = pMap; + /***/ }), -/* 148 */ +/* 149 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const chalk = __webpack_require__(2); -const cliCursor = __webpack_require__(149); -const cliSpinners = __webpack_require__(153); +const cliCursor = __webpack_require__(150); +const cliSpinners = __webpack_require__(154); const logSymbols = __webpack_require__(122); class Ora { @@ -19163,12 +19239,12 @@ module.exports.promise = (action, options) => { /***/ }), -/* 149 */ +/* 150 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const restoreCursor = __webpack_require__(150); +const restoreCursor = __webpack_require__(151); let hidden = false; @@ -19209,12 +19285,12 @@ exports.toggle = (force, stream) => { /***/ }), -/* 150 */ +/* 151 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const onetime = __webpack_require__(151); +const onetime = __webpack_require__(152); const signalExit = __webpack_require__(86); module.exports = onetime(() => { @@ -19225,12 +19301,12 @@ module.exports = onetime(() => { /***/ }), -/* 151 */ +/* 152 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const mimicFn = __webpack_require__(152); +const mimicFn = __webpack_require__(153); module.exports = (fn, opts) => { // TODO: Remove this in v3 @@ -19271,7 +19347,7 @@ module.exports = (fn, opts) => { /***/ }), -/* 152 */ +/* 153 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -19287,22 +19363,22 @@ module.exports = (to, from) => { /***/ }), -/* 153 */ +/* 154 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -module.exports = __webpack_require__(154); +module.exports = __webpack_require__(155); /***/ }), -/* 154 */ +/* 155 */ /***/ (function(module) { module.exports = {"dots":{"interval":80,"frames":["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"]},"dots2":{"interval":80,"frames":["⣾","⣽","⣻","⢿","⡿","⣟","⣯","⣷"]},"dots3":{"interval":80,"frames":["⠋","⠙","⠚","⠞","⠖","⠦","⠴","⠲","⠳","⠓"]},"dots4":{"interval":80,"frames":["⠄","⠆","⠇","⠋","⠙","⠸","⠰","⠠","⠰","⠸","⠙","⠋","⠇","⠆"]},"dots5":{"interval":80,"frames":["⠋","⠙","⠚","⠒","⠂","⠂","⠒","⠲","⠴","⠦","⠖","⠒","⠐","⠐","⠒","⠓","⠋"]},"dots6":{"interval":80,"frames":["⠁","⠉","⠙","⠚","⠒","⠂","⠂","⠒","⠲","⠴","⠤","⠄","⠄","⠤","⠴","⠲","⠒","⠂","⠂","⠒","⠚","⠙","⠉","⠁"]},"dots7":{"interval":80,"frames":["⠈","⠉","⠋","⠓","⠒","⠐","⠐","⠒","⠖","⠦","⠤","⠠","⠠","⠤","⠦","⠖","⠒","⠐","⠐","⠒","⠓","⠋","⠉","⠈"]},"dots8":{"interval":80,"frames":["⠁","⠁","⠉","⠙","⠚","⠒","⠂","⠂","⠒","⠲","⠴","⠤","⠄","⠄","⠤","⠠","⠠","⠤","⠦","⠖","⠒","⠐","⠐","⠒","⠓","⠋","⠉","⠈","⠈"]},"dots9":{"interval":80,"frames":["⢹","⢺","⢼","⣸","⣇","⡧","⡗","⡏"]},"dots10":{"interval":80,"frames":["⢄","⢂","⢁","⡁","⡈","⡐","⡠"]},"dots11":{"interval":100,"frames":["⠁","⠂","⠄","⡀","⢀","⠠","⠐","⠈"]},"dots12":{"interval":80,"frames":["⢀⠀","⡀⠀","⠄⠀","⢂⠀","⡂⠀","⠅⠀","⢃⠀","⡃⠀","⠍⠀","⢋⠀","⡋⠀","⠍⠁","⢋⠁","⡋⠁","⠍⠉","⠋⠉","⠋⠉","⠉⠙","⠉⠙","⠉⠩","⠈⢙","⠈⡙","⢈⠩","⡀⢙","⠄⡙","⢂⠩","⡂⢘","⠅⡘","⢃⠨","⡃⢐","⠍⡐","⢋⠠","⡋⢀","⠍⡁","⢋⠁","⡋⠁","⠍⠉","⠋⠉","⠋⠉","⠉⠙","⠉⠙","⠉⠩","⠈⢙","⠈⡙","⠈⠩","⠀⢙","⠀⡙","⠀⠩","⠀⢘","⠀⡘","⠀⠨","⠀⢐","⠀⡐","⠀⠠","⠀⢀","⠀⡀"]},"line":{"interval":130,"frames":["-","\\","|","/"]},"line2":{"interval":100,"frames":["⠂","-","–","—","–","-"]},"pipe":{"interval":100,"frames":["┤","┘","┴","└","├","┌","┬","┐"]},"simpleDots":{"interval":400,"frames":[". ",".. ","..."," "]},"simpleDotsScrolling":{"interval":200,"frames":[". ",".. ","..."," .."," ."," "]},"star":{"interval":70,"frames":["✶","✸","✹","✺","✹","✷"]},"star2":{"interval":80,"frames":["+","x","*"]},"flip":{"interval":70,"frames":["_","_","_","-","`","`","'","´","-","_","_","_"]},"hamburger":{"interval":100,"frames":["☱","☲","☴"]},"growVertical":{"interval":120,"frames":["▁","▃","▄","▅","▆","▇","▆","▅","▄","▃"]},"growHorizontal":{"interval":120,"frames":["▏","▎","▍","▌","▋","▊","▉","▊","▋","▌","▍","▎"]},"balloon":{"interval":140,"frames":[" ",".","o","O","@","*"," "]},"balloon2":{"interval":120,"frames":[".","o","O","°","O","o","."]},"noise":{"interval":100,"frames":["▓","▒","░"]},"bounce":{"interval":120,"frames":["⠁","⠂","⠄","⠂"]},"boxBounce":{"interval":120,"frames":["▖","▘","▝","▗"]},"boxBounce2":{"interval":100,"frames":["▌","▀","▐","▄"]},"triangle":{"interval":50,"frames":["◢","◣","◤","◥"]},"arc":{"interval":100,"frames":["◜","◠","◝","◞","◡","◟"]},"circle":{"interval":120,"frames":["◡","⊙","◠"]},"squareCorners":{"interval":180,"frames":["◰","◳","◲","◱"]},"circleQuarters":{"interval":120,"frames":["◴","◷","◶","◵"]},"circleHalves":{"interval":50,"frames":["◐","◓","◑","◒"]},"squish":{"interval":100,"frames":["╫","╪"]},"toggle":{"interval":250,"frames":["⊶","⊷"]},"toggle2":{"interval":80,"frames":["▫","▪"]},"toggle3":{"interval":120,"frames":["□","■"]},"toggle4":{"interval":100,"frames":["■","□","▪","▫"]},"toggle5":{"interval":100,"frames":["▮","▯"]},"toggle6":{"interval":300,"frames":["ဝ","၀"]},"toggle7":{"interval":80,"frames":["⦾","⦿"]},"toggle8":{"interval":100,"frames":["◍","◌"]},"toggle9":{"interval":100,"frames":["◉","◎"]},"toggle10":{"interval":100,"frames":["㊂","㊀","㊁"]},"toggle11":{"interval":50,"frames":["⧇","⧆"]},"toggle12":{"interval":120,"frames":["☗","☖"]},"toggle13":{"interval":80,"frames":["=","*","-"]},"arrow":{"interval":100,"frames":["←","↖","↑","↗","→","↘","↓","↙"]},"arrow2":{"interval":80,"frames":["⬆️ ","↗️ ","➡️ ","↘️ ","⬇️ ","↙️ ","⬅️ ","↖️ "]},"arrow3":{"interval":120,"frames":["▹▹▹▹▹","▸▹▹▹▹","▹▸▹▹▹","▹▹▸▹▹","▹▹▹▸▹","▹▹▹▹▸"]},"bouncingBar":{"interval":80,"frames":["[ ]","[= ]","[== ]","[=== ]","[ ===]","[ ==]","[ =]","[ ]","[ =]","[ ==]","[ ===]","[====]","[=== ]","[== ]","[= ]"]},"bouncingBall":{"interval":80,"frames":["( ● )","( ● )","( ● )","( ● )","( ●)","( ● )","( ● )","( ● )","( ● )","(● )"]},"smiley":{"interval":200,"frames":["😄 ","😝 "]},"monkey":{"interval":300,"frames":["🙈 ","🙈 ","🙉 ","🙊 "]},"hearts":{"interval":100,"frames":["💛 ","💙 ","💜 ","💚 ","❤️ "]},"clock":{"interval":100,"frames":["🕐 ","🕑 ","🕒 ","🕓 ","🕔 ","🕕 ","🕖 ","🕗 ","🕘 ","🕙 ","🕚 "]},"earth":{"interval":180,"frames":["🌍 ","🌎 ","🌏 "]},"moon":{"interval":80,"frames":["🌑 ","🌒 ","🌓 ","🌔 ","🌕 ","🌖 ","🌗 ","🌘 "]},"runner":{"interval":140,"frames":["🚶 ","🏃 "]},"pong":{"interval":80,"frames":["▐⠂ ▌","▐⠈ ▌","▐ ⠂ ▌","▐ ⠠ ▌","▐ ⡀ ▌","▐ ⠠ ▌","▐ ⠂ ▌","▐ ⠈ ▌","▐ ⠂ ▌","▐ ⠠ ▌","▐ ⡀ ▌","▐ ⠠ ▌","▐ ⠂ ▌","▐ ⠈ ▌","▐ ⠂▌","▐ ⠠▌","▐ ⡀▌","▐ ⠠ ▌","▐ ⠂ ▌","▐ ⠈ ▌","▐ ⠂ ▌","▐ ⠠ ▌","▐ ⡀ ▌","▐ ⠠ ▌","▐ ⠂ ▌","▐ ⠈ ▌","▐ ⠂ ▌","▐ ⠠ ▌","▐ ⡀ ▌","▐⠠ ▌"]},"shark":{"interval":120,"frames":["▐|\\____________▌","▐_|\\___________▌","▐__|\\__________▌","▐___|\\_________▌","▐____|\\________▌","▐_____|\\_______▌","▐______|\\______▌","▐_______|\\_____▌","▐________|\\____▌","▐_________|\\___▌","▐__________|\\__▌","▐___________|\\_▌","▐____________|\\▌","▐____________/|▌","▐___________/|_▌","▐__________/|__▌","▐_________/|___▌","▐________/|____▌","▐_______/|_____▌","▐______/|______▌","▐_____/|_______▌","▐____/|________▌","▐___/|_________▌","▐__/|__________▌","▐_/|___________▌","▐/|____________▌"]},"dqpb":{"interval":100,"frames":["d","q","p","b"]},"weather":{"interval":100,"frames":["☀️ ","☀️ ","☀️ ","🌤 ","⛅️ ","🌥 ","☁️ ","🌧 ","🌨 ","🌧 ","🌨 ","🌧 ","🌨 ","⛈ ","🌨 ","🌧 ","🌨 ","☁️ ","🌥 ","⛅️ ","🌤 ","☀️ ","☀️ "]},"christmas":{"interval":400,"frames":["🌲","🎄"]}}; /***/ }), -/* 155 */ +/* 156 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -19362,7 +19438,7 @@ const RunCommand = { }; /***/ }), -/* 156 */ +/* 157 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -19373,7 +19449,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(33); /* harmony import */ var _utils_parallelize__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(34); /* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(35); -/* harmony import */ var _utils_watch__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(157); +/* harmony import */ var _utils_watch__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(158); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -19457,14 +19533,14 @@ const WatchCommand = { }; /***/ }), -/* 157 */ +/* 158 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "waitUntilWatchIsReady", function() { return waitUntilWatchIsReady; }); -/* harmony import */ var rxjs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(158); -/* harmony import */ var rxjs_operators__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(257); +/* harmony import */ var rxjs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(159); +/* harmony import */ var rxjs_operators__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(258); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -19531,168 +19607,168 @@ function waitUntilWatchIsReady(stream, opts = {}) { } /***/ }), -/* 158 */ +/* 159 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony import */ var _internal_Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(159); +/* harmony import */ var _internal_Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(160); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Observable", function() { return _internal_Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"]; }); -/* harmony import */ var _internal_observable_ConnectableObservable__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(177); +/* harmony import */ var _internal_observable_ConnectableObservable__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(178); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "ConnectableObservable", function() { return _internal_observable_ConnectableObservable__WEBPACK_IMPORTED_MODULE_1__["ConnectableObservable"]; }); -/* harmony import */ var _internal_operators_groupBy__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(182); +/* harmony import */ var _internal_operators_groupBy__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(183); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "GroupedObservable", function() { return _internal_operators_groupBy__WEBPACK_IMPORTED_MODULE_2__["GroupedObservable"]; }); -/* harmony import */ var _internal_symbol_observable__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(174); +/* harmony import */ var _internal_symbol_observable__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(175); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "observable", function() { return _internal_symbol_observable__WEBPACK_IMPORTED_MODULE_3__["observable"]; }); -/* harmony import */ var _internal_Subject__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(178); +/* harmony import */ var _internal_Subject__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(179); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Subject", function() { return _internal_Subject__WEBPACK_IMPORTED_MODULE_4__["Subject"]; }); -/* harmony import */ var _internal_BehaviorSubject__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(183); +/* harmony import */ var _internal_BehaviorSubject__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(184); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "BehaviorSubject", function() { return _internal_BehaviorSubject__WEBPACK_IMPORTED_MODULE_5__["BehaviorSubject"]; }); -/* harmony import */ var _internal_ReplaySubject__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(184); +/* harmony import */ var _internal_ReplaySubject__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(185); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "ReplaySubject", function() { return _internal_ReplaySubject__WEBPACK_IMPORTED_MODULE_6__["ReplaySubject"]; }); -/* harmony import */ var _internal_AsyncSubject__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(201); +/* harmony import */ var _internal_AsyncSubject__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(202); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "AsyncSubject", function() { return _internal_AsyncSubject__WEBPACK_IMPORTED_MODULE_7__["AsyncSubject"]; }); -/* harmony import */ var _internal_scheduler_asap__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(202); +/* harmony import */ var _internal_scheduler_asap__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(203); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "asapScheduler", function() { return _internal_scheduler_asap__WEBPACK_IMPORTED_MODULE_8__["asap"]; }); -/* harmony import */ var _internal_scheduler_async__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(206); +/* harmony import */ var _internal_scheduler_async__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(207); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "asyncScheduler", function() { return _internal_scheduler_async__WEBPACK_IMPORTED_MODULE_9__["async"]; }); -/* harmony import */ var _internal_scheduler_queue__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(185); +/* harmony import */ var _internal_scheduler_queue__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(186); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "queueScheduler", function() { return _internal_scheduler_queue__WEBPACK_IMPORTED_MODULE_10__["queue"]; }); -/* harmony import */ var _internal_scheduler_animationFrame__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(207); +/* harmony import */ var _internal_scheduler_animationFrame__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(208); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "animationFrameScheduler", function() { return _internal_scheduler_animationFrame__WEBPACK_IMPORTED_MODULE_11__["animationFrame"]; }); -/* harmony import */ var _internal_scheduler_VirtualTimeScheduler__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(210); +/* harmony import */ var _internal_scheduler_VirtualTimeScheduler__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(211); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "VirtualTimeScheduler", function() { return _internal_scheduler_VirtualTimeScheduler__WEBPACK_IMPORTED_MODULE_12__["VirtualTimeScheduler"]; }); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "VirtualAction", function() { return _internal_scheduler_VirtualTimeScheduler__WEBPACK_IMPORTED_MODULE_12__["VirtualAction"]; }); -/* harmony import */ var _internal_Scheduler__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(191); +/* harmony import */ var _internal_Scheduler__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(192); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Scheduler", function() { return _internal_Scheduler__WEBPACK_IMPORTED_MODULE_13__["Scheduler"]; }); -/* harmony import */ var _internal_Subscription__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(167); +/* harmony import */ var _internal_Subscription__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(168); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Subscription", function() { return _internal_Subscription__WEBPACK_IMPORTED_MODULE_14__["Subscription"]; }); -/* harmony import */ var _internal_Subscriber__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(161); +/* harmony import */ var _internal_Subscriber__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(162); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Subscriber", function() { return _internal_Subscriber__WEBPACK_IMPORTED_MODULE_15__["Subscriber"]; }); -/* harmony import */ var _internal_Notification__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(193); +/* harmony import */ var _internal_Notification__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(194); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Notification", function() { return _internal_Notification__WEBPACK_IMPORTED_MODULE_16__["Notification"]; }); -/* harmony import */ var _internal_util_pipe__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(175); +/* harmony import */ var _internal_util_pipe__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(176); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "pipe", function() { return _internal_util_pipe__WEBPACK_IMPORTED_MODULE_17__["pipe"]; }); -/* harmony import */ var _internal_util_noop__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(176); +/* harmony import */ var _internal_util_noop__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(177); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "noop", function() { return _internal_util_noop__WEBPACK_IMPORTED_MODULE_18__["noop"]; }); -/* harmony import */ var _internal_util_identity__WEBPACK_IMPORTED_MODULE_19__ = __webpack_require__(211); +/* harmony import */ var _internal_util_identity__WEBPACK_IMPORTED_MODULE_19__ = __webpack_require__(212); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "identity", function() { return _internal_util_identity__WEBPACK_IMPORTED_MODULE_19__["identity"]; }); -/* harmony import */ var _internal_util_isObservable__WEBPACK_IMPORTED_MODULE_20__ = __webpack_require__(212); +/* harmony import */ var _internal_util_isObservable__WEBPACK_IMPORTED_MODULE_20__ = __webpack_require__(213); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "isObservable", function() { return _internal_util_isObservable__WEBPACK_IMPORTED_MODULE_20__["isObservable"]; }); -/* harmony import */ var _internal_util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_21__ = __webpack_require__(213); +/* harmony import */ var _internal_util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_21__ = __webpack_require__(214); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "ArgumentOutOfRangeError", function() { return _internal_util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_21__["ArgumentOutOfRangeError"]; }); -/* harmony import */ var _internal_util_EmptyError__WEBPACK_IMPORTED_MODULE_22__ = __webpack_require__(214); +/* harmony import */ var _internal_util_EmptyError__WEBPACK_IMPORTED_MODULE_22__ = __webpack_require__(215); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "EmptyError", function() { return _internal_util_EmptyError__WEBPACK_IMPORTED_MODULE_22__["EmptyError"]; }); -/* harmony import */ var _internal_util_ObjectUnsubscribedError__WEBPACK_IMPORTED_MODULE_23__ = __webpack_require__(179); +/* harmony import */ var _internal_util_ObjectUnsubscribedError__WEBPACK_IMPORTED_MODULE_23__ = __webpack_require__(180); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "ObjectUnsubscribedError", function() { return _internal_util_ObjectUnsubscribedError__WEBPACK_IMPORTED_MODULE_23__["ObjectUnsubscribedError"]; }); -/* harmony import */ var _internal_util_UnsubscriptionError__WEBPACK_IMPORTED_MODULE_24__ = __webpack_require__(172); +/* harmony import */ var _internal_util_UnsubscriptionError__WEBPACK_IMPORTED_MODULE_24__ = __webpack_require__(173); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "UnsubscriptionError", function() { return _internal_util_UnsubscriptionError__WEBPACK_IMPORTED_MODULE_24__["UnsubscriptionError"]; }); -/* harmony import */ var _internal_util_TimeoutError__WEBPACK_IMPORTED_MODULE_25__ = __webpack_require__(215); +/* harmony import */ var _internal_util_TimeoutError__WEBPACK_IMPORTED_MODULE_25__ = __webpack_require__(216); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "TimeoutError", function() { return _internal_util_TimeoutError__WEBPACK_IMPORTED_MODULE_25__["TimeoutError"]; }); -/* harmony import */ var _internal_observable_bindCallback__WEBPACK_IMPORTED_MODULE_26__ = __webpack_require__(216); +/* harmony import */ var _internal_observable_bindCallback__WEBPACK_IMPORTED_MODULE_26__ = __webpack_require__(217); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bindCallback", function() { return _internal_observable_bindCallback__WEBPACK_IMPORTED_MODULE_26__["bindCallback"]; }); -/* harmony import */ var _internal_observable_bindNodeCallback__WEBPACK_IMPORTED_MODULE_27__ = __webpack_require__(218); +/* harmony import */ var _internal_observable_bindNodeCallback__WEBPACK_IMPORTED_MODULE_27__ = __webpack_require__(219); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bindNodeCallback", function() { return _internal_observable_bindNodeCallback__WEBPACK_IMPORTED_MODULE_27__["bindNodeCallback"]; }); -/* harmony import */ var _internal_observable_combineLatest__WEBPACK_IMPORTED_MODULE_28__ = __webpack_require__(219); +/* harmony import */ var _internal_observable_combineLatest__WEBPACK_IMPORTED_MODULE_28__ = __webpack_require__(220); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "combineLatest", function() { return _internal_observable_combineLatest__WEBPACK_IMPORTED_MODULE_28__["combineLatest"]; }); -/* harmony import */ var _internal_observable_concat__WEBPACK_IMPORTED_MODULE_29__ = __webpack_require__(230); +/* harmony import */ var _internal_observable_concat__WEBPACK_IMPORTED_MODULE_29__ = __webpack_require__(231); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concat", function() { return _internal_observable_concat__WEBPACK_IMPORTED_MODULE_29__["concat"]; }); -/* harmony import */ var _internal_observable_defer__WEBPACK_IMPORTED_MODULE_30__ = __webpack_require__(240); +/* harmony import */ var _internal_observable_defer__WEBPACK_IMPORTED_MODULE_30__ = __webpack_require__(241); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "defer", function() { return _internal_observable_defer__WEBPACK_IMPORTED_MODULE_30__["defer"]; }); -/* harmony import */ var _internal_observable_empty__WEBPACK_IMPORTED_MODULE_31__ = __webpack_require__(194); +/* harmony import */ var _internal_observable_empty__WEBPACK_IMPORTED_MODULE_31__ = __webpack_require__(195); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "empty", function() { return _internal_observable_empty__WEBPACK_IMPORTED_MODULE_31__["empty"]; }); -/* harmony import */ var _internal_observable_forkJoin__WEBPACK_IMPORTED_MODULE_32__ = __webpack_require__(241); +/* harmony import */ var _internal_observable_forkJoin__WEBPACK_IMPORTED_MODULE_32__ = __webpack_require__(242); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "forkJoin", function() { return _internal_observable_forkJoin__WEBPACK_IMPORTED_MODULE_32__["forkJoin"]; }); -/* harmony import */ var _internal_observable_from__WEBPACK_IMPORTED_MODULE_33__ = __webpack_require__(231); +/* harmony import */ var _internal_observable_from__WEBPACK_IMPORTED_MODULE_33__ = __webpack_require__(232); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "from", function() { return _internal_observable_from__WEBPACK_IMPORTED_MODULE_33__["from"]; }); -/* harmony import */ var _internal_observable_fromEvent__WEBPACK_IMPORTED_MODULE_34__ = __webpack_require__(242); +/* harmony import */ var _internal_observable_fromEvent__WEBPACK_IMPORTED_MODULE_34__ = __webpack_require__(243); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "fromEvent", function() { return _internal_observable_fromEvent__WEBPACK_IMPORTED_MODULE_34__["fromEvent"]; }); -/* harmony import */ var _internal_observable_fromEventPattern__WEBPACK_IMPORTED_MODULE_35__ = __webpack_require__(243); +/* harmony import */ var _internal_observable_fromEventPattern__WEBPACK_IMPORTED_MODULE_35__ = __webpack_require__(244); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "fromEventPattern", function() { return _internal_observable_fromEventPattern__WEBPACK_IMPORTED_MODULE_35__["fromEventPattern"]; }); -/* harmony import */ var _internal_observable_generate__WEBPACK_IMPORTED_MODULE_36__ = __webpack_require__(244); +/* harmony import */ var _internal_observable_generate__WEBPACK_IMPORTED_MODULE_36__ = __webpack_require__(245); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "generate", function() { return _internal_observable_generate__WEBPACK_IMPORTED_MODULE_36__["generate"]; }); -/* harmony import */ var _internal_observable_iif__WEBPACK_IMPORTED_MODULE_37__ = __webpack_require__(245); +/* harmony import */ var _internal_observable_iif__WEBPACK_IMPORTED_MODULE_37__ = __webpack_require__(246); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "iif", function() { return _internal_observable_iif__WEBPACK_IMPORTED_MODULE_37__["iif"]; }); -/* harmony import */ var _internal_observable_interval__WEBPACK_IMPORTED_MODULE_38__ = __webpack_require__(246); +/* harmony import */ var _internal_observable_interval__WEBPACK_IMPORTED_MODULE_38__ = __webpack_require__(247); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "interval", function() { return _internal_observable_interval__WEBPACK_IMPORTED_MODULE_38__["interval"]; }); -/* harmony import */ var _internal_observable_merge__WEBPACK_IMPORTED_MODULE_39__ = __webpack_require__(248); +/* harmony import */ var _internal_observable_merge__WEBPACK_IMPORTED_MODULE_39__ = __webpack_require__(249); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "merge", function() { return _internal_observable_merge__WEBPACK_IMPORTED_MODULE_39__["merge"]; }); -/* harmony import */ var _internal_observable_never__WEBPACK_IMPORTED_MODULE_40__ = __webpack_require__(249); +/* harmony import */ var _internal_observable_never__WEBPACK_IMPORTED_MODULE_40__ = __webpack_require__(250); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "never", function() { return _internal_observable_never__WEBPACK_IMPORTED_MODULE_40__["never"]; }); -/* harmony import */ var _internal_observable_of__WEBPACK_IMPORTED_MODULE_41__ = __webpack_require__(195); +/* harmony import */ var _internal_observable_of__WEBPACK_IMPORTED_MODULE_41__ = __webpack_require__(196); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "of", function() { return _internal_observable_of__WEBPACK_IMPORTED_MODULE_41__["of"]; }); -/* harmony import */ var _internal_observable_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_42__ = __webpack_require__(250); +/* harmony import */ var _internal_observable_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_42__ = __webpack_require__(251); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "onErrorResumeNext", function() { return _internal_observable_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_42__["onErrorResumeNext"]; }); -/* harmony import */ var _internal_observable_pairs__WEBPACK_IMPORTED_MODULE_43__ = __webpack_require__(251); +/* harmony import */ var _internal_observable_pairs__WEBPACK_IMPORTED_MODULE_43__ = __webpack_require__(252); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "pairs", function() { return _internal_observable_pairs__WEBPACK_IMPORTED_MODULE_43__["pairs"]; }); -/* harmony import */ var _internal_observable_race__WEBPACK_IMPORTED_MODULE_44__ = __webpack_require__(252); +/* harmony import */ var _internal_observable_race__WEBPACK_IMPORTED_MODULE_44__ = __webpack_require__(253); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "race", function() { return _internal_observable_race__WEBPACK_IMPORTED_MODULE_44__["race"]; }); -/* harmony import */ var _internal_observable_range__WEBPACK_IMPORTED_MODULE_45__ = __webpack_require__(253); +/* harmony import */ var _internal_observable_range__WEBPACK_IMPORTED_MODULE_45__ = __webpack_require__(254); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "range", function() { return _internal_observable_range__WEBPACK_IMPORTED_MODULE_45__["range"]; }); -/* harmony import */ var _internal_observable_throwError__WEBPACK_IMPORTED_MODULE_46__ = __webpack_require__(200); +/* harmony import */ var _internal_observable_throwError__WEBPACK_IMPORTED_MODULE_46__ = __webpack_require__(201); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "throwError", function() { return _internal_observable_throwError__WEBPACK_IMPORTED_MODULE_46__["throwError"]; }); -/* harmony import */ var _internal_observable_timer__WEBPACK_IMPORTED_MODULE_47__ = __webpack_require__(254); +/* harmony import */ var _internal_observable_timer__WEBPACK_IMPORTED_MODULE_47__ = __webpack_require__(255); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timer", function() { return _internal_observable_timer__WEBPACK_IMPORTED_MODULE_47__["timer"]; }); -/* harmony import */ var _internal_observable_using__WEBPACK_IMPORTED_MODULE_48__ = __webpack_require__(255); +/* harmony import */ var _internal_observable_using__WEBPACK_IMPORTED_MODULE_48__ = __webpack_require__(256); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "using", function() { return _internal_observable_using__WEBPACK_IMPORTED_MODULE_48__["using"]; }); -/* harmony import */ var _internal_observable_zip__WEBPACK_IMPORTED_MODULE_49__ = __webpack_require__(256); +/* harmony import */ var _internal_observable_zip__WEBPACK_IMPORTED_MODULE_49__ = __webpack_require__(257); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "zip", function() { return _internal_observable_zip__WEBPACK_IMPORTED_MODULE_49__["zip"]; }); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "EMPTY", function() { return _internal_observable_empty__WEBPACK_IMPORTED_MODULE_31__["EMPTY"]; }); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "NEVER", function() { return _internal_observable_never__WEBPACK_IMPORTED_MODULE_40__["NEVER"]; }); -/* harmony import */ var _internal_config__WEBPACK_IMPORTED_MODULE_50__ = __webpack_require__(165); +/* harmony import */ var _internal_config__WEBPACK_IMPORTED_MODULE_50__ = __webpack_require__(166); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "config", function() { return _internal_config__WEBPACK_IMPORTED_MODULE_50__["config"]; }); /** PURE_IMPORTS_START PURE_IMPORTS_END */ @@ -19753,16 +19829,16 @@ __webpack_require__.r(__webpack_exports__); /***/ }), -/* 159 */ +/* 160 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Observable", function() { return Observable; }); -/* harmony import */ var _util_toSubscriber__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(160); -/* harmony import */ var _internal_symbol_observable__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(174); -/* harmony import */ var _util_pipe__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(175); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(165); +/* harmony import */ var _util_toSubscriber__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(161); +/* harmony import */ var _internal_symbol_observable__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(175); +/* harmony import */ var _util_pipe__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(176); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(166); /** PURE_IMPORTS_START _util_toSubscriber,_internal_symbol_observable,_util_pipe,_config PURE_IMPORTS_END */ @@ -19876,15 +19952,15 @@ function getPromiseCtor(promiseCtor) { /***/ }), -/* 160 */ +/* 161 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "toSubscriber", function() { return toSubscriber; }); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(161); -/* harmony import */ var _symbol_rxSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(173); -/* harmony import */ var _Observer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(164); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); +/* harmony import */ var _symbol_rxSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(174); +/* harmony import */ var _Observer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(165); /** PURE_IMPORTS_START _Subscriber,_symbol_rxSubscriber,_Observer PURE_IMPORTS_END */ @@ -19907,19 +19983,19 @@ function toSubscriber(nextOrObserver, error, complete) { /***/ }), -/* 161 */ +/* 162 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Subscriber", function() { return Subscriber; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _util_isFunction__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(163); -/* harmony import */ var _Observer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(164); -/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(167); -/* harmony import */ var _internal_symbol_rxSubscriber__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(173); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(165); -/* harmony import */ var _util_hostReportError__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(166); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _util_isFunction__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(164); +/* harmony import */ var _Observer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(165); +/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(168); +/* harmony import */ var _internal_symbol_rxSubscriber__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(174); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(166); +/* harmony import */ var _util_hostReportError__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(167); /** PURE_IMPORTS_START tslib,_util_isFunction,_Observer,_Subscription,_internal_symbol_rxSubscriber,_config,_util_hostReportError PURE_IMPORTS_END */ @@ -20161,7 +20237,7 @@ function isTrustedSubscriber(obj) { /***/ }), -/* 162 */ +/* 163 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -20374,7 +20450,7 @@ function __importDefault(mod) { /***/ }), -/* 163 */ +/* 164 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -20388,14 +20464,14 @@ function isFunction(x) { /***/ }), -/* 164 */ +/* 165 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "empty", function() { return empty; }); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(165); -/* harmony import */ var _util_hostReportError__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(166); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(166); +/* harmony import */ var _util_hostReportError__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(167); /** PURE_IMPORTS_START _config,_util_hostReportError PURE_IMPORTS_END */ @@ -20416,7 +20492,7 @@ var empty = { /***/ }), -/* 165 */ +/* 166 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -20444,7 +20520,7 @@ var config = { /***/ }), -/* 166 */ +/* 167 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -20458,18 +20534,18 @@ function hostReportError(err) { /***/ }), -/* 167 */ +/* 168 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Subscription", function() { return Subscription; }); -/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(168); -/* harmony import */ var _util_isObject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(169); -/* harmony import */ var _util_isFunction__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(163); -/* harmony import */ var _util_tryCatch__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(170); -/* harmony import */ var _util_errorObject__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(171); -/* harmony import */ var _util_UnsubscriptionError__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(172); +/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(169); +/* harmony import */ var _util_isObject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(170); +/* harmony import */ var _util_isFunction__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(164); +/* harmony import */ var _util_tryCatch__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(171); +/* harmony import */ var _util_errorObject__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(172); +/* harmony import */ var _util_UnsubscriptionError__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(173); /** PURE_IMPORTS_START _util_isArray,_util_isObject,_util_isFunction,_util_tryCatch,_util_errorObject,_util_UnsubscriptionError PURE_IMPORTS_END */ @@ -20605,7 +20681,7 @@ function flattenUnsubscriptionErrors(errors) { /***/ }), -/* 168 */ +/* 169 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -20617,7 +20693,7 @@ var isArray = Array.isArray || (function (x) { return x && typeof x.length === ' /***/ }), -/* 169 */ +/* 170 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -20631,13 +20707,13 @@ function isObject(x) { /***/ }), -/* 170 */ +/* 171 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "tryCatch", function() { return tryCatch; }); -/* harmony import */ var _errorObject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(171); +/* harmony import */ var _errorObject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(172); /** PURE_IMPORTS_START _errorObject PURE_IMPORTS_END */ var tryCatchTarget; @@ -20658,7 +20734,7 @@ function tryCatch(fn) { /***/ }), -/* 171 */ +/* 172 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -20670,13 +20746,13 @@ var errorObject = { e: {} }; /***/ }), -/* 172 */ +/* 173 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "UnsubscriptionError", function() { return UnsubscriptionError; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); /** PURE_IMPORTS_START tslib PURE_IMPORTS_END */ var UnsubscriptionError = /*@__PURE__*/ (function (_super) { @@ -20696,7 +20772,7 @@ var UnsubscriptionError = /*@__PURE__*/ (function (_super) { /***/ }), -/* 173 */ +/* 174 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -20712,7 +20788,7 @@ var $$rxSubscriber = rxSubscriber; /***/ }), -/* 174 */ +/* 175 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -20724,14 +20800,14 @@ var observable = typeof Symbol === 'function' && Symbol.observable || '@@observa /***/ }), -/* 175 */ +/* 176 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "pipe", function() { return pipe; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "pipeFromArray", function() { return pipeFromArray; }); -/* harmony import */ var _noop__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(176); +/* harmony import */ var _noop__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(177); /** PURE_IMPORTS_START _noop PURE_IMPORTS_END */ function pipe() { @@ -20756,7 +20832,7 @@ function pipeFromArray(fns) { /***/ }), -/* 176 */ +/* 177 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -20768,19 +20844,19 @@ function noop() { } /***/ }), -/* 177 */ +/* 178 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ConnectableObservable", function() { return ConnectableObservable; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "connectableObservableDescriptor", function() { return connectableObservableDescriptor; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(178); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(159); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(161); -/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(167); -/* harmony import */ var _operators_refCount__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(181); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(179); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(160); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(162); +/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(168); +/* harmony import */ var _operators_refCount__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(182); /** PURE_IMPORTS_START tslib,_Subject,_Observable,_Subscriber,_Subscription,_operators_refCount PURE_IMPORTS_END */ @@ -20927,7 +21003,7 @@ var RefCountSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 178 */ +/* 179 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -20935,13 +21011,13 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SubjectSubscriber", function() { return SubjectSubscriber; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Subject", function() { return Subject; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "AnonymousSubject", function() { return AnonymousSubject; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(159); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(161); -/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(167); -/* harmony import */ var _util_ObjectUnsubscribedError__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(179); -/* harmony import */ var _SubjectSubscription__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(180); -/* harmony import */ var _internal_symbol_rxSubscriber__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(173); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(160); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(162); +/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(168); +/* harmony import */ var _util_ObjectUnsubscribedError__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(180); +/* harmony import */ var _SubjectSubscription__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(181); +/* harmony import */ var _internal_symbol_rxSubscriber__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(174); /** PURE_IMPORTS_START tslib,_Observable,_Subscriber,_Subscription,_util_ObjectUnsubscribedError,_SubjectSubscription,_internal_symbol_rxSubscriber PURE_IMPORTS_END */ @@ -21103,13 +21179,13 @@ var AnonymousSubject = /*@__PURE__*/ (function (_super) { /***/ }), -/* 179 */ +/* 180 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ObjectUnsubscribedError", function() { return ObjectUnsubscribedError; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); /** PURE_IMPORTS_START tslib PURE_IMPORTS_END */ var ObjectUnsubscribedError = /*@__PURE__*/ (function (_super) { @@ -21127,14 +21203,14 @@ var ObjectUnsubscribedError = /*@__PURE__*/ (function (_super) { /***/ }), -/* 180 */ +/* 181 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SubjectSubscription", function() { return SubjectSubscription; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(167); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(168); /** PURE_IMPORTS_START tslib,_Subscription PURE_IMPORTS_END */ @@ -21170,14 +21246,14 @@ var SubjectSubscription = /*@__PURE__*/ (function (_super) { /***/ }), -/* 181 */ +/* 182 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "refCount", function() { return refCount; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); /** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ @@ -21239,18 +21315,18 @@ var RefCountSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 182 */ +/* 183 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "groupBy", function() { return groupBy; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "GroupedObservable", function() { return GroupedObservable; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); -/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(167); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(159); -/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(178); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); +/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(168); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(160); +/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(179); /** PURE_IMPORTS_START tslib,_Subscriber,_Subscription,_Observable,_Subject PURE_IMPORTS_END */ @@ -21436,15 +21512,15 @@ var InnerRefCountSubscription = /*@__PURE__*/ (function (_super) { /***/ }), -/* 183 */ +/* 184 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "BehaviorSubject", function() { return BehaviorSubject; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(178); -/* harmony import */ var _util_ObjectUnsubscribedError__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(179); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(179); +/* harmony import */ var _util_ObjectUnsubscribedError__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(180); /** PURE_IMPORTS_START tslib,_Subject,_util_ObjectUnsubscribedError PURE_IMPORTS_END */ @@ -21491,19 +21567,19 @@ var BehaviorSubject = /*@__PURE__*/ (function (_super) { /***/ }), -/* 184 */ +/* 185 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ReplaySubject", function() { return ReplaySubject; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(178); -/* harmony import */ var _scheduler_queue__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(185); -/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(167); -/* harmony import */ var _operators_observeOn__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(192); -/* harmony import */ var _util_ObjectUnsubscribedError__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(179); -/* harmony import */ var _SubjectSubscription__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(180); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(179); +/* harmony import */ var _scheduler_queue__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(186); +/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(168); +/* harmony import */ var _operators_observeOn__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(193); +/* harmony import */ var _util_ObjectUnsubscribedError__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(180); +/* harmony import */ var _SubjectSubscription__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(181); /** PURE_IMPORTS_START tslib,_Subject,_scheduler_queue,_Subscription,_operators_observeOn,_util_ObjectUnsubscribedError,_SubjectSubscription PURE_IMPORTS_END */ @@ -21624,14 +21700,14 @@ var ReplayEvent = /*@__PURE__*/ (function () { /***/ }), -/* 185 */ +/* 186 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "queue", function() { return queue; }); -/* harmony import */ var _QueueAction__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(186); -/* harmony import */ var _QueueScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(189); +/* harmony import */ var _QueueAction__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(187); +/* harmony import */ var _QueueScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(190); /** PURE_IMPORTS_START _QueueAction,_QueueScheduler PURE_IMPORTS_END */ @@ -21640,14 +21716,14 @@ var queue = /*@__PURE__*/ new _QueueScheduler__WEBPACK_IMPORTED_MODULE_1__["Queu /***/ }), -/* 186 */ +/* 187 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "QueueAction", function() { return QueueAction; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _AsyncAction__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(187); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _AsyncAction__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(188); /** PURE_IMPORTS_START tslib,_AsyncAction PURE_IMPORTS_END */ @@ -21692,14 +21768,14 @@ var QueueAction = /*@__PURE__*/ (function (_super) { /***/ }), -/* 187 */ +/* 188 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "AsyncAction", function() { return AsyncAction; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Action__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(188); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Action__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(189); /** PURE_IMPORTS_START tslib,_Action PURE_IMPORTS_END */ @@ -21797,14 +21873,14 @@ var AsyncAction = /*@__PURE__*/ (function (_super) { /***/ }), -/* 188 */ +/* 189 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Action", function() { return Action; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(167); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(168); /** PURE_IMPORTS_START tslib,_Subscription PURE_IMPORTS_END */ @@ -21826,14 +21902,14 @@ var Action = /*@__PURE__*/ (function (_super) { /***/ }), -/* 189 */ +/* 190 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "QueueScheduler", function() { return QueueScheduler; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _AsyncScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(190); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _AsyncScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(191); /** PURE_IMPORTS_START tslib,_AsyncScheduler PURE_IMPORTS_END */ @@ -21849,14 +21925,14 @@ var QueueScheduler = /*@__PURE__*/ (function (_super) { /***/ }), -/* 190 */ +/* 191 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "AsyncScheduler", function() { return AsyncScheduler; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Scheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(191); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Scheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(192); /** PURE_IMPORTS_START tslib,_Scheduler PURE_IMPORTS_END */ @@ -21918,7 +21994,7 @@ var AsyncScheduler = /*@__PURE__*/ (function (_super) { /***/ }), -/* 191 */ +/* 192 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -21946,7 +22022,7 @@ var Scheduler = /*@__PURE__*/ (function () { /***/ }), -/* 192 */ +/* 193 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -21955,9 +22031,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ObserveOnOperator", function() { return ObserveOnOperator; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ObserveOnSubscriber", function() { return ObserveOnSubscriber; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ObserveOnMessage", function() { return ObserveOnMessage; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); -/* harmony import */ var _Notification__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(193); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); +/* harmony import */ var _Notification__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(194); /** PURE_IMPORTS_START tslib,_Subscriber,_Notification PURE_IMPORTS_END */ @@ -22027,15 +22103,15 @@ var ObserveOnMessage = /*@__PURE__*/ (function () { /***/ }), -/* 193 */ +/* 194 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Notification", function() { return Notification; }); -/* harmony import */ var _observable_empty__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(194); -/* harmony import */ var _observable_of__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(195); -/* harmony import */ var _observable_throwError__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(200); +/* harmony import */ var _observable_empty__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(195); +/* harmony import */ var _observable_of__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(196); +/* harmony import */ var _observable_throwError__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(201); /** PURE_IMPORTS_START _observable_empty,_observable_of,_observable_throwError PURE_IMPORTS_END */ @@ -22109,7 +22185,7 @@ var Notification = /*@__PURE__*/ (function () { /***/ }), -/* 194 */ +/* 195 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -22117,7 +22193,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "EMPTY", function() { return EMPTY; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "empty", function() { return empty; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "emptyScheduled", function() { return emptyScheduled; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(159); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(160); /** PURE_IMPORTS_START _Observable PURE_IMPORTS_END */ var EMPTY = /*@__PURE__*/ new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { return subscriber.complete(); }); @@ -22131,16 +22207,16 @@ function emptyScheduled(scheduler) { /***/ }), -/* 195 */ +/* 196 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "of", function() { return of; }); -/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(196); -/* harmony import */ var _fromArray__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(197); -/* harmony import */ var _empty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(194); -/* harmony import */ var _scalar__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(199); +/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(197); +/* harmony import */ var _fromArray__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(198); +/* harmony import */ var _empty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(195); +/* harmony import */ var _scalar__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(200); /** PURE_IMPORTS_START _util_isScheduler,_fromArray,_empty,_scalar PURE_IMPORTS_END */ @@ -22171,7 +22247,7 @@ function of() { /***/ }), -/* 196 */ +/* 197 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -22185,15 +22261,15 @@ function isScheduler(value) { /***/ }), -/* 197 */ +/* 198 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "fromArray", function() { return fromArray; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(159); -/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(167); -/* harmony import */ var _util_subscribeToArray__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(198); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(160); +/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(168); +/* harmony import */ var _util_subscribeToArray__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(199); /** PURE_IMPORTS_START _Observable,_Subscription,_util_subscribeToArray PURE_IMPORTS_END */ @@ -22224,7 +22300,7 @@ function fromArray(input, scheduler) { /***/ }), -/* 198 */ +/* 199 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -22245,13 +22321,13 @@ var subscribeToArray = function (array) { /***/ }), -/* 199 */ +/* 200 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "scalar", function() { return scalar; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(159); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(160); /** PURE_IMPORTS_START _Observable PURE_IMPORTS_END */ function scalar(value) { @@ -22267,13 +22343,13 @@ function scalar(value) { /***/ }), -/* 200 */ +/* 201 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "throwError", function() { return throwError; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(159); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(160); /** PURE_IMPORTS_START _Observable PURE_IMPORTS_END */ function throwError(error, scheduler) { @@ -22292,15 +22368,15 @@ function dispatch(_a) { /***/ }), -/* 201 */ +/* 202 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "AsyncSubject", function() { return AsyncSubject; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(178); -/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(167); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(179); +/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(168); /** PURE_IMPORTS_START tslib,_Subject,_Subscription PURE_IMPORTS_END */ @@ -22351,14 +22427,14 @@ var AsyncSubject = /*@__PURE__*/ (function (_super) { /***/ }), -/* 202 */ +/* 203 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "asap", function() { return asap; }); -/* harmony import */ var _AsapAction__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(203); -/* harmony import */ var _AsapScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(205); +/* harmony import */ var _AsapAction__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(204); +/* harmony import */ var _AsapScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(206); /** PURE_IMPORTS_START _AsapAction,_AsapScheduler PURE_IMPORTS_END */ @@ -22367,15 +22443,15 @@ var asap = /*@__PURE__*/ new _AsapScheduler__WEBPACK_IMPORTED_MODULE_1__["AsapSc /***/ }), -/* 203 */ +/* 204 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "AsapAction", function() { return AsapAction; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _util_Immediate__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(204); -/* harmony import */ var _AsyncAction__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(187); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _util_Immediate__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(205); +/* harmony import */ var _AsyncAction__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(188); /** PURE_IMPORTS_START tslib,_util_Immediate,_AsyncAction PURE_IMPORTS_END */ @@ -22418,7 +22494,7 @@ var AsapAction = /*@__PURE__*/ (function (_super) { /***/ }), -/* 204 */ +/* 205 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -22448,14 +22524,14 @@ var Immediate = { /***/ }), -/* 205 */ +/* 206 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "AsapScheduler", function() { return AsapScheduler; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _AsyncScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(190); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _AsyncScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(191); /** PURE_IMPORTS_START tslib,_AsyncScheduler PURE_IMPORTS_END */ @@ -22492,14 +22568,14 @@ var AsapScheduler = /*@__PURE__*/ (function (_super) { /***/ }), -/* 206 */ +/* 207 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "async", function() { return async; }); -/* harmony import */ var _AsyncAction__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(187); -/* harmony import */ var _AsyncScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(190); +/* harmony import */ var _AsyncAction__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(188); +/* harmony import */ var _AsyncScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(191); /** PURE_IMPORTS_START _AsyncAction,_AsyncScheduler PURE_IMPORTS_END */ @@ -22508,14 +22584,14 @@ var async = /*@__PURE__*/ new _AsyncScheduler__WEBPACK_IMPORTED_MODULE_1__["Asyn /***/ }), -/* 207 */ +/* 208 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "animationFrame", function() { return animationFrame; }); -/* harmony import */ var _AnimationFrameAction__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(208); -/* harmony import */ var _AnimationFrameScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(209); +/* harmony import */ var _AnimationFrameAction__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(209); +/* harmony import */ var _AnimationFrameScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(210); /** PURE_IMPORTS_START _AnimationFrameAction,_AnimationFrameScheduler PURE_IMPORTS_END */ @@ -22524,14 +22600,14 @@ var animationFrame = /*@__PURE__*/ new _AnimationFrameScheduler__WEBPACK_IMPORTE /***/ }), -/* 208 */ +/* 209 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "AnimationFrameAction", function() { return AnimationFrameAction; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _AsyncAction__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(187); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _AsyncAction__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(188); /** PURE_IMPORTS_START tslib,_AsyncAction PURE_IMPORTS_END */ @@ -22573,14 +22649,14 @@ var AnimationFrameAction = /*@__PURE__*/ (function (_super) { /***/ }), -/* 209 */ +/* 210 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "AnimationFrameScheduler", function() { return AnimationFrameScheduler; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _AsyncScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(190); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _AsyncScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(191); /** PURE_IMPORTS_START tslib,_AsyncScheduler PURE_IMPORTS_END */ @@ -22617,16 +22693,16 @@ var AnimationFrameScheduler = /*@__PURE__*/ (function (_super) { /***/ }), -/* 210 */ +/* 211 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "VirtualTimeScheduler", function() { return VirtualTimeScheduler; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "VirtualAction", function() { return VirtualAction; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _AsyncAction__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(187); -/* harmony import */ var _AsyncScheduler__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(190); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _AsyncAction__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(188); +/* harmony import */ var _AsyncScheduler__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(191); /** PURE_IMPORTS_START tslib,_AsyncAction,_AsyncScheduler PURE_IMPORTS_END */ @@ -22738,7 +22814,7 @@ var VirtualAction = /*@__PURE__*/ (function (_super) { /***/ }), -/* 211 */ +/* 212 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -22752,13 +22828,13 @@ function identity(x) { /***/ }), -/* 212 */ +/* 213 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isObservable", function() { return isObservable; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(159); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(160); /** PURE_IMPORTS_START _Observable PURE_IMPORTS_END */ function isObservable(obj) { @@ -22768,13 +22844,13 @@ function isObservable(obj) { /***/ }), -/* 213 */ +/* 214 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ArgumentOutOfRangeError", function() { return ArgumentOutOfRangeError; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); /** PURE_IMPORTS_START tslib PURE_IMPORTS_END */ var ArgumentOutOfRangeError = /*@__PURE__*/ (function (_super) { @@ -22792,13 +22868,13 @@ var ArgumentOutOfRangeError = /*@__PURE__*/ (function (_super) { /***/ }), -/* 214 */ +/* 215 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "EmptyError", function() { return EmptyError; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); /** PURE_IMPORTS_START tslib PURE_IMPORTS_END */ var EmptyError = /*@__PURE__*/ (function (_super) { @@ -22816,13 +22892,13 @@ var EmptyError = /*@__PURE__*/ (function (_super) { /***/ }), -/* 215 */ +/* 216 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TimeoutError", function() { return TimeoutError; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); /** PURE_IMPORTS_START tslib PURE_IMPORTS_END */ var TimeoutError = /*@__PURE__*/ (function (_super) { @@ -22840,17 +22916,17 @@ var TimeoutError = /*@__PURE__*/ (function (_super) { /***/ }), -/* 216 */ +/* 217 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "bindCallback", function() { return bindCallback; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(159); -/* harmony import */ var _AsyncSubject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(201); -/* harmony import */ var _operators_map__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(217); -/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(168); -/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(196); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(160); +/* harmony import */ var _AsyncSubject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(202); +/* harmony import */ var _operators_map__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(218); +/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(169); +/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(197); /** PURE_IMPORTS_START _Observable,_AsyncSubject,_operators_map,_util_isArray,_util_isScheduler PURE_IMPORTS_END */ @@ -22953,15 +23029,15 @@ function dispatchError(state) { /***/ }), -/* 217 */ +/* 218 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "map", function() { return map; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MapOperator", function() { return MapOperator; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); /** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ @@ -23010,17 +23086,17 @@ var MapSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 218 */ +/* 219 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "bindNodeCallback", function() { return bindNodeCallback; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(159); -/* harmony import */ var _AsyncSubject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(201); -/* harmony import */ var _operators_map__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(217); -/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(196); -/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(168); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(160); +/* harmony import */ var _AsyncSubject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(202); +/* harmony import */ var _operators_map__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(218); +/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(197); +/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(169); /** PURE_IMPORTS_START _Observable,_AsyncSubject,_operators_map,_util_isScheduler,_util_isArray PURE_IMPORTS_END */ @@ -23131,7 +23207,7 @@ function dispatchError(arg) { /***/ }), -/* 219 */ +/* 220 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -23139,12 +23215,12 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "combineLatest", function() { return combineLatest; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "CombineLatestOperator", function() { return CombineLatestOperator; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "CombineLatestSubscriber", function() { return CombineLatestSubscriber; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(196); -/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(168); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(220); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(221); -/* harmony import */ var _fromArray__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(197); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(197); +/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(169); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(221); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(222); +/* harmony import */ var _fromArray__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(198); /** PURE_IMPORTS_START tslib,_util_isScheduler,_util_isArray,_OuterSubscriber,_util_subscribeToResult,_fromArray PURE_IMPORTS_END */ @@ -23249,14 +23325,14 @@ var CombineLatestSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 220 */ +/* 221 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "OuterSubscriber", function() { return OuterSubscriber; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); /** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ @@ -23281,14 +23357,14 @@ var OuterSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 221 */ +/* 222 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "subscribeToResult", function() { return subscribeToResult; }); -/* harmony import */ var _InnerSubscriber__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(222); -/* harmony import */ var _subscribeTo__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(223); +/* harmony import */ var _InnerSubscriber__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(223); +/* harmony import */ var _subscribeTo__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(224); /** PURE_IMPORTS_START _InnerSubscriber,_subscribeTo PURE_IMPORTS_END */ @@ -23300,14 +23376,14 @@ function subscribeToResult(outerSubscriber, result, outerValue, outerIndex) { /***/ }), -/* 222 */ +/* 223 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "InnerSubscriber", function() { return InnerSubscriber; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); /** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ @@ -23339,22 +23415,22 @@ var InnerSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 223 */ +/* 224 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "subscribeTo", function() { return subscribeTo; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(159); -/* harmony import */ var _subscribeToArray__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(198); -/* harmony import */ var _subscribeToPromise__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(224); -/* harmony import */ var _subscribeToIterable__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(225); -/* harmony import */ var _subscribeToObservable__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(227); -/* harmony import */ var _isArrayLike__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(228); -/* harmony import */ var _isPromise__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(229); -/* harmony import */ var _isObject__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(169); -/* harmony import */ var _symbol_iterator__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(226); -/* harmony import */ var _symbol_observable__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(174); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(160); +/* harmony import */ var _subscribeToArray__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(199); +/* harmony import */ var _subscribeToPromise__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(225); +/* harmony import */ var _subscribeToIterable__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(226); +/* harmony import */ var _subscribeToObservable__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(228); +/* harmony import */ var _isArrayLike__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(229); +/* harmony import */ var _isPromise__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(230); +/* harmony import */ var _isObject__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(170); +/* harmony import */ var _symbol_iterator__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(227); +/* harmony import */ var _symbol_observable__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(175); /** PURE_IMPORTS_START _Observable,_subscribeToArray,_subscribeToPromise,_subscribeToIterable,_subscribeToObservable,_isArrayLike,_isPromise,_isObject,_symbol_iterator,_symbol_observable PURE_IMPORTS_END */ @@ -23402,13 +23478,13 @@ var subscribeTo = function (result) { /***/ }), -/* 224 */ +/* 225 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "subscribeToPromise", function() { return subscribeToPromise; }); -/* harmony import */ var _hostReportError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(166); +/* harmony import */ var _hostReportError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(167); /** PURE_IMPORTS_START _hostReportError PURE_IMPORTS_END */ var subscribeToPromise = function (promise) { @@ -23427,13 +23503,13 @@ var subscribeToPromise = function (promise) { /***/ }), -/* 225 */ +/* 226 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "subscribeToIterable", function() { return subscribeToIterable; }); -/* harmony import */ var _symbol_iterator__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(226); +/* harmony import */ var _symbol_iterator__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(227); /** PURE_IMPORTS_START _symbol_iterator PURE_IMPORTS_END */ var subscribeToIterable = function (iterable) { @@ -23464,7 +23540,7 @@ var subscribeToIterable = function (iterable) { /***/ }), -/* 226 */ +/* 227 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -23485,13 +23561,13 @@ var $$iterator = iterator; /***/ }), -/* 227 */ +/* 228 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "subscribeToObservable", function() { return subscribeToObservable; }); -/* harmony import */ var _symbol_observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(174); +/* harmony import */ var _symbol_observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(175); /** PURE_IMPORTS_START _symbol_observable PURE_IMPORTS_END */ var subscribeToObservable = function (obj) { @@ -23509,7 +23585,7 @@ var subscribeToObservable = function (obj) { /***/ }), -/* 228 */ +/* 229 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -23521,7 +23597,7 @@ var isArrayLike = (function (x) { return x && typeof x.length === 'number' && ty /***/ }), -/* 229 */ +/* 230 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -23535,16 +23611,16 @@ function isPromise(value) { /***/ }), -/* 230 */ +/* 231 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "concat", function() { return concat; }); -/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(196); -/* harmony import */ var _of__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(195); -/* harmony import */ var _from__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(231); -/* harmony import */ var _operators_concatAll__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(237); +/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(197); +/* harmony import */ var _of__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(196); +/* harmony import */ var _from__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(232); +/* harmony import */ var _operators_concatAll__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(238); /** PURE_IMPORTS_START _util_isScheduler,_of,_from,_operators_concatAll PURE_IMPORTS_END */ @@ -23564,22 +23640,22 @@ function concat() { /***/ }), -/* 231 */ +/* 232 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "from", function() { return from; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(159); -/* harmony import */ var _util_isPromise__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(229); -/* harmony import */ var _util_isArrayLike__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(228); -/* harmony import */ var _util_isInteropObservable__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(232); -/* harmony import */ var _util_isIterable__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(233); -/* harmony import */ var _fromArray__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(197); -/* harmony import */ var _fromPromise__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(234); -/* harmony import */ var _fromIterable__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(235); -/* harmony import */ var _fromObservable__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(236); -/* harmony import */ var _util_subscribeTo__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(223); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(160); +/* harmony import */ var _util_isPromise__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(230); +/* harmony import */ var _util_isArrayLike__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(229); +/* harmony import */ var _util_isInteropObservable__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(233); +/* harmony import */ var _util_isIterable__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(234); +/* harmony import */ var _fromArray__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(198); +/* harmony import */ var _fromPromise__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(235); +/* harmony import */ var _fromIterable__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(236); +/* harmony import */ var _fromObservable__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(237); +/* harmony import */ var _util_subscribeTo__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(224); /** PURE_IMPORTS_START _Observable,_util_isPromise,_util_isArrayLike,_util_isInteropObservable,_util_isIterable,_fromArray,_fromPromise,_fromIterable,_fromObservable,_util_subscribeTo PURE_IMPORTS_END */ @@ -23618,13 +23694,13 @@ function from(input, scheduler) { /***/ }), -/* 232 */ +/* 233 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isInteropObservable", function() { return isInteropObservable; }); -/* harmony import */ var _symbol_observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(174); +/* harmony import */ var _symbol_observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(175); /** PURE_IMPORTS_START _symbol_observable PURE_IMPORTS_END */ function isInteropObservable(input) { @@ -23634,13 +23710,13 @@ function isInteropObservable(input) { /***/ }), -/* 233 */ +/* 234 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isIterable", function() { return isIterable; }); -/* harmony import */ var _symbol_iterator__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(226); +/* harmony import */ var _symbol_iterator__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(227); /** PURE_IMPORTS_START _symbol_iterator PURE_IMPORTS_END */ function isIterable(input) { @@ -23650,15 +23726,15 @@ function isIterable(input) { /***/ }), -/* 234 */ +/* 235 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "fromPromise", function() { return fromPromise; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(159); -/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(167); -/* harmony import */ var _util_subscribeToPromise__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(224); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(160); +/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(168); +/* harmony import */ var _util_subscribeToPromise__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(225); /** PURE_IMPORTS_START _Observable,_Subscription,_util_subscribeToPromise PURE_IMPORTS_END */ @@ -23688,16 +23764,16 @@ function fromPromise(input, scheduler) { /***/ }), -/* 235 */ +/* 236 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "fromIterable", function() { return fromIterable; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(159); -/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(167); -/* harmony import */ var _symbol_iterator__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(226); -/* harmony import */ var _util_subscribeToIterable__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(225); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(160); +/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(168); +/* harmony import */ var _symbol_iterator__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(227); +/* harmony import */ var _util_subscribeToIterable__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(226); /** PURE_IMPORTS_START _Observable,_Subscription,_symbol_iterator,_util_subscribeToIterable PURE_IMPORTS_END */ @@ -23753,16 +23829,16 @@ function fromIterable(input, scheduler) { /***/ }), -/* 236 */ +/* 237 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "fromObservable", function() { return fromObservable; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(159); -/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(167); -/* harmony import */ var _symbol_observable__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(174); -/* harmony import */ var _util_subscribeToObservable__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(227); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(160); +/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(168); +/* harmony import */ var _symbol_observable__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(175); +/* harmony import */ var _util_subscribeToObservable__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(228); /** PURE_IMPORTS_START _Observable,_Subscription,_symbol_observable,_util_subscribeToObservable PURE_IMPORTS_END */ @@ -23791,13 +23867,13 @@ function fromObservable(input, scheduler) { /***/ }), -/* 237 */ +/* 238 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "concatAll", function() { return concatAll; }); -/* harmony import */ var _mergeAll__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(238); +/* harmony import */ var _mergeAll__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(239); /** PURE_IMPORTS_START _mergeAll PURE_IMPORTS_END */ function concatAll() { @@ -23807,14 +23883,14 @@ function concatAll() { /***/ }), -/* 238 */ +/* 239 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "mergeAll", function() { return mergeAll; }); -/* harmony import */ var _mergeMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(239); -/* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(211); +/* harmony import */ var _mergeMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(240); +/* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(212); /** PURE_IMPORTS_START _mergeMap,_util_identity PURE_IMPORTS_END */ @@ -23828,7 +23904,7 @@ function mergeAll(concurrent) { /***/ }), -/* 239 */ +/* 240 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -23836,11 +23912,11 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "mergeMap", function() { return mergeMap; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MergeMapOperator", function() { return MergeMapOperator; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MergeMapSubscriber", function() { return MergeMapSubscriber; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(221); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(220); -/* harmony import */ var _map__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(217); -/* harmony import */ var _observable_from__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(231); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(222); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(221); +/* harmony import */ var _map__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(218); +/* harmony import */ var _observable_from__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(232); /** PURE_IMPORTS_START tslib,_util_subscribeToResult,_OuterSubscriber,_map,_observable_from PURE_IMPORTS_END */ @@ -23939,15 +24015,15 @@ var MergeMapSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 240 */ +/* 241 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "defer", function() { return defer; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(159); -/* harmony import */ var _from__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(231); -/* harmony import */ var _empty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(194); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(160); +/* harmony import */ var _from__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(232); +/* harmony import */ var _empty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(195); /** PURE_IMPORTS_START _Observable,_from,_empty PURE_IMPORTS_END */ @@ -23970,19 +24046,19 @@ function defer(observableFactory) { /***/ }), -/* 241 */ +/* 242 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "forkJoin", function() { return forkJoin; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(159); -/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(168); -/* harmony import */ var _empty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(194); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(221); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(220); -/* harmony import */ var _operators_map__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(217); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(160); +/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(169); +/* harmony import */ var _empty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(195); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(222); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(221); +/* harmony import */ var _operators_map__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(218); /** PURE_IMPORTS_START tslib,_Observable,_util_isArray,_empty,_util_subscribeToResult,_OuterSubscriber,_operators_map PURE_IMPORTS_END */ @@ -24060,16 +24136,16 @@ var ForkJoinSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 242 */ +/* 243 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "fromEvent", function() { return fromEvent; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(159); -/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(168); -/* harmony import */ var _util_isFunction__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(163); -/* harmony import */ var _operators_map__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(217); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(160); +/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(169); +/* harmony import */ var _util_isFunction__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(164); +/* harmony import */ var _operators_map__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(218); /** PURE_IMPORTS_START _Observable,_util_isArray,_util_isFunction,_operators_map PURE_IMPORTS_END */ @@ -24136,16 +24212,16 @@ function isEventTarget(sourceObj) { /***/ }), -/* 243 */ +/* 244 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "fromEventPattern", function() { return fromEventPattern; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(159); -/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(168); -/* harmony import */ var _util_isFunction__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(163); -/* harmony import */ var _operators_map__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(217); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(160); +/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(169); +/* harmony import */ var _util_isFunction__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(164); +/* harmony import */ var _operators_map__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(218); /** PURE_IMPORTS_START _Observable,_util_isArray,_util_isFunction,_operators_map PURE_IMPORTS_END */ @@ -24181,15 +24257,15 @@ function fromEventPattern(addHandler, removeHandler, resultSelector) { /***/ }), -/* 244 */ +/* 245 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "generate", function() { return generate; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(159); -/* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(211); -/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(196); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(160); +/* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(212); +/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(197); /** PURE_IMPORTS_START _Observable,_util_identity,_util_isScheduler PURE_IMPORTS_END */ @@ -24318,14 +24394,14 @@ function dispatch(state) { /***/ }), -/* 245 */ +/* 246 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "iif", function() { return iif; }); -/* harmony import */ var _defer__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(240); -/* harmony import */ var _empty__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(194); +/* harmony import */ var _defer__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(241); +/* harmony import */ var _empty__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(195); /** PURE_IMPORTS_START _defer,_empty PURE_IMPORTS_END */ @@ -24342,15 +24418,15 @@ function iif(condition, trueResult, falseResult) { /***/ }), -/* 246 */ +/* 247 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "interval", function() { return interval; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(159); -/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(206); -/* harmony import */ var _util_isNumeric__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(247); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(160); +/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(207); +/* harmony import */ var _util_isNumeric__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(248); /** PURE_IMPORTS_START _Observable,_scheduler_async,_util_isNumeric PURE_IMPORTS_END */ @@ -24382,13 +24458,13 @@ function dispatch(state) { /***/ }), -/* 247 */ +/* 248 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isNumeric", function() { return isNumeric; }); -/* harmony import */ var _isArray__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(168); +/* harmony import */ var _isArray__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(169); /** PURE_IMPORTS_START _isArray PURE_IMPORTS_END */ function isNumeric(val) { @@ -24398,16 +24474,16 @@ function isNumeric(val) { /***/ }), -/* 248 */ +/* 249 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "merge", function() { return merge; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(159); -/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(196); -/* harmony import */ var _operators_mergeAll__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(238); -/* harmony import */ var _fromArray__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(197); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(160); +/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(197); +/* harmony import */ var _operators_mergeAll__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(239); +/* harmony import */ var _fromArray__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(198); /** PURE_IMPORTS_START _Observable,_util_isScheduler,_operators_mergeAll,_fromArray PURE_IMPORTS_END */ @@ -24439,15 +24515,15 @@ function merge() { /***/ }), -/* 249 */ +/* 250 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "NEVER", function() { return NEVER; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "never", function() { return never; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(159); -/* harmony import */ var _util_noop__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(176); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(160); +/* harmony import */ var _util_noop__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(177); /** PURE_IMPORTS_START _Observable,_util_noop PURE_IMPORTS_END */ @@ -24459,16 +24535,16 @@ function never() { /***/ }), -/* 250 */ +/* 251 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "onErrorResumeNext", function() { return onErrorResumeNext; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(159); -/* harmony import */ var _from__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(231); -/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(168); -/* harmony import */ var _empty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(194); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(160); +/* harmony import */ var _from__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(232); +/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(169); +/* harmony import */ var _empty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(195); /** PURE_IMPORTS_START _Observable,_from,_util_isArray,_empty PURE_IMPORTS_END */ @@ -24499,15 +24575,15 @@ function onErrorResumeNext() { /***/ }), -/* 251 */ +/* 252 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "pairs", function() { return pairs; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "dispatch", function() { return dispatch; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(159); -/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(167); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(160); +/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(168); /** PURE_IMPORTS_START _Observable,_Subscription PURE_IMPORTS_END */ @@ -24550,7 +24626,7 @@ function dispatch(state) { /***/ }), -/* 252 */ +/* 253 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -24558,11 +24634,11 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "race", function() { return race; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "RaceOperator", function() { return RaceOperator; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "RaceSubscriber", function() { return RaceSubscriber; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(168); -/* harmony import */ var _fromArray__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(197); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(220); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(221); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(169); +/* harmony import */ var _fromArray__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(198); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(221); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(222); /** PURE_IMPORTS_START tslib,_util_isArray,_fromArray,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ @@ -24644,14 +24720,14 @@ var RaceSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 253 */ +/* 254 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "range", function() { return range; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "dispatch", function() { return dispatch; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(159); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(160); /** PURE_IMPORTS_START _Observable PURE_IMPORTS_END */ function range(start, count, scheduler) { @@ -24702,16 +24778,16 @@ function dispatch(state) { /***/ }), -/* 254 */ +/* 255 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timer", function() { return timer; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(159); -/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(206); -/* harmony import */ var _util_isNumeric__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(247); -/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(196); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(160); +/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(207); +/* harmony import */ var _util_isNumeric__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(248); +/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(197); /** PURE_IMPORTS_START _Observable,_scheduler_async,_util_isNumeric,_util_isScheduler PURE_IMPORTS_END */ @@ -24756,15 +24832,15 @@ function dispatch(state) { /***/ }), -/* 255 */ +/* 256 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "using", function() { return using; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(159); -/* harmony import */ var _from__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(231); -/* harmony import */ var _empty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(194); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(160); +/* harmony import */ var _from__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(232); +/* harmony import */ var _empty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(195); /** PURE_IMPORTS_START _Observable,_from,_empty PURE_IMPORTS_END */ @@ -24801,7 +24877,7 @@ function using(resourceFactory, observableFactory) { /***/ }), -/* 256 */ +/* 257 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -24809,13 +24885,13 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "zip", function() { return zip; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ZipOperator", function() { return ZipOperator; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ZipSubscriber", function() { return ZipSubscriber; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _fromArray__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(197); -/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(168); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(161); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(220); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(221); -/* harmony import */ var _internal_symbol_iterator__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(226); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _fromArray__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(198); +/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(169); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(162); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(221); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(222); +/* harmony import */ var _internal_symbol_iterator__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(227); /** PURE_IMPORTS_START tslib,_fromArray,_util_isArray,_Subscriber,_OuterSubscriber,_util_subscribeToResult,_.._internal_symbol_iterator PURE_IMPORTS_END */ @@ -25035,320 +25111,320 @@ var ZipBufferIterator = /*@__PURE__*/ (function (_super) { /***/ }), -/* 257 */ +/* 258 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony import */ var _internal_operators_audit__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(258); +/* harmony import */ var _internal_operators_audit__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(259); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "audit", function() { return _internal_operators_audit__WEBPACK_IMPORTED_MODULE_0__["audit"]; }); -/* harmony import */ var _internal_operators_auditTime__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(259); +/* harmony import */ var _internal_operators_auditTime__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(260); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "auditTime", function() { return _internal_operators_auditTime__WEBPACK_IMPORTED_MODULE_1__["auditTime"]; }); -/* harmony import */ var _internal_operators_buffer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(260); +/* harmony import */ var _internal_operators_buffer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(261); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buffer", function() { return _internal_operators_buffer__WEBPACK_IMPORTED_MODULE_2__["buffer"]; }); -/* harmony import */ var _internal_operators_bufferCount__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(261); +/* harmony import */ var _internal_operators_bufferCount__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(262); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferCount", function() { return _internal_operators_bufferCount__WEBPACK_IMPORTED_MODULE_3__["bufferCount"]; }); -/* harmony import */ var _internal_operators_bufferTime__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(262); +/* harmony import */ var _internal_operators_bufferTime__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(263); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferTime", function() { return _internal_operators_bufferTime__WEBPACK_IMPORTED_MODULE_4__["bufferTime"]; }); -/* harmony import */ var _internal_operators_bufferToggle__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(263); +/* harmony import */ var _internal_operators_bufferToggle__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(264); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferToggle", function() { return _internal_operators_bufferToggle__WEBPACK_IMPORTED_MODULE_5__["bufferToggle"]; }); -/* harmony import */ var _internal_operators_bufferWhen__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(264); +/* harmony import */ var _internal_operators_bufferWhen__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(265); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferWhen", function() { return _internal_operators_bufferWhen__WEBPACK_IMPORTED_MODULE_6__["bufferWhen"]; }); -/* harmony import */ var _internal_operators_catchError__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(265); +/* harmony import */ var _internal_operators_catchError__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(266); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "catchError", function() { return _internal_operators_catchError__WEBPACK_IMPORTED_MODULE_7__["catchError"]; }); -/* harmony import */ var _internal_operators_combineAll__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(266); +/* harmony import */ var _internal_operators_combineAll__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(267); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "combineAll", function() { return _internal_operators_combineAll__WEBPACK_IMPORTED_MODULE_8__["combineAll"]; }); -/* harmony import */ var _internal_operators_combineLatest__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(267); +/* harmony import */ var _internal_operators_combineLatest__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(268); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "combineLatest", function() { return _internal_operators_combineLatest__WEBPACK_IMPORTED_MODULE_9__["combineLatest"]; }); -/* harmony import */ var _internal_operators_concat__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(268); +/* harmony import */ var _internal_operators_concat__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(269); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concat", function() { return _internal_operators_concat__WEBPACK_IMPORTED_MODULE_10__["concat"]; }); -/* harmony import */ var _internal_operators_concatAll__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(237); +/* harmony import */ var _internal_operators_concatAll__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(238); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concatAll", function() { return _internal_operators_concatAll__WEBPACK_IMPORTED_MODULE_11__["concatAll"]; }); -/* harmony import */ var _internal_operators_concatMap__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(269); +/* harmony import */ var _internal_operators_concatMap__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(270); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concatMap", function() { return _internal_operators_concatMap__WEBPACK_IMPORTED_MODULE_12__["concatMap"]; }); -/* harmony import */ var _internal_operators_concatMapTo__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(270); +/* harmony import */ var _internal_operators_concatMapTo__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(271); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concatMapTo", function() { return _internal_operators_concatMapTo__WEBPACK_IMPORTED_MODULE_13__["concatMapTo"]; }); -/* harmony import */ var _internal_operators_count__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(271); +/* harmony import */ var _internal_operators_count__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(272); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "count", function() { return _internal_operators_count__WEBPACK_IMPORTED_MODULE_14__["count"]; }); -/* harmony import */ var _internal_operators_debounce__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(272); +/* harmony import */ var _internal_operators_debounce__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(273); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "debounce", function() { return _internal_operators_debounce__WEBPACK_IMPORTED_MODULE_15__["debounce"]; }); -/* harmony import */ var _internal_operators_debounceTime__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(273); +/* harmony import */ var _internal_operators_debounceTime__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(274); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "debounceTime", function() { return _internal_operators_debounceTime__WEBPACK_IMPORTED_MODULE_16__["debounceTime"]; }); -/* harmony import */ var _internal_operators_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(274); +/* harmony import */ var _internal_operators_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(275); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "defaultIfEmpty", function() { return _internal_operators_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_17__["defaultIfEmpty"]; }); -/* harmony import */ var _internal_operators_delay__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(275); +/* harmony import */ var _internal_operators_delay__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(276); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "delay", function() { return _internal_operators_delay__WEBPACK_IMPORTED_MODULE_18__["delay"]; }); -/* harmony import */ var _internal_operators_delayWhen__WEBPACK_IMPORTED_MODULE_19__ = __webpack_require__(277); +/* harmony import */ var _internal_operators_delayWhen__WEBPACK_IMPORTED_MODULE_19__ = __webpack_require__(278); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "delayWhen", function() { return _internal_operators_delayWhen__WEBPACK_IMPORTED_MODULE_19__["delayWhen"]; }); -/* harmony import */ var _internal_operators_dematerialize__WEBPACK_IMPORTED_MODULE_20__ = __webpack_require__(278); +/* harmony import */ var _internal_operators_dematerialize__WEBPACK_IMPORTED_MODULE_20__ = __webpack_require__(279); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "dematerialize", function() { return _internal_operators_dematerialize__WEBPACK_IMPORTED_MODULE_20__["dematerialize"]; }); -/* harmony import */ var _internal_operators_distinct__WEBPACK_IMPORTED_MODULE_21__ = __webpack_require__(279); +/* harmony import */ var _internal_operators_distinct__WEBPACK_IMPORTED_MODULE_21__ = __webpack_require__(280); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "distinct", function() { return _internal_operators_distinct__WEBPACK_IMPORTED_MODULE_21__["distinct"]; }); -/* harmony import */ var _internal_operators_distinctUntilChanged__WEBPACK_IMPORTED_MODULE_22__ = __webpack_require__(280); +/* harmony import */ var _internal_operators_distinctUntilChanged__WEBPACK_IMPORTED_MODULE_22__ = __webpack_require__(281); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "distinctUntilChanged", function() { return _internal_operators_distinctUntilChanged__WEBPACK_IMPORTED_MODULE_22__["distinctUntilChanged"]; }); -/* harmony import */ var _internal_operators_distinctUntilKeyChanged__WEBPACK_IMPORTED_MODULE_23__ = __webpack_require__(281); +/* harmony import */ var _internal_operators_distinctUntilKeyChanged__WEBPACK_IMPORTED_MODULE_23__ = __webpack_require__(282); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "distinctUntilKeyChanged", function() { return _internal_operators_distinctUntilKeyChanged__WEBPACK_IMPORTED_MODULE_23__["distinctUntilKeyChanged"]; }); -/* harmony import */ var _internal_operators_elementAt__WEBPACK_IMPORTED_MODULE_24__ = __webpack_require__(282); +/* harmony import */ var _internal_operators_elementAt__WEBPACK_IMPORTED_MODULE_24__ = __webpack_require__(283); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "elementAt", function() { return _internal_operators_elementAt__WEBPACK_IMPORTED_MODULE_24__["elementAt"]; }); -/* harmony import */ var _internal_operators_endWith__WEBPACK_IMPORTED_MODULE_25__ = __webpack_require__(287); +/* harmony import */ var _internal_operators_endWith__WEBPACK_IMPORTED_MODULE_25__ = __webpack_require__(288); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "endWith", function() { return _internal_operators_endWith__WEBPACK_IMPORTED_MODULE_25__["endWith"]; }); -/* harmony import */ var _internal_operators_every__WEBPACK_IMPORTED_MODULE_26__ = __webpack_require__(288); +/* harmony import */ var _internal_operators_every__WEBPACK_IMPORTED_MODULE_26__ = __webpack_require__(289); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "every", function() { return _internal_operators_every__WEBPACK_IMPORTED_MODULE_26__["every"]; }); -/* harmony import */ var _internal_operators_exhaust__WEBPACK_IMPORTED_MODULE_27__ = __webpack_require__(289); +/* harmony import */ var _internal_operators_exhaust__WEBPACK_IMPORTED_MODULE_27__ = __webpack_require__(290); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "exhaust", function() { return _internal_operators_exhaust__WEBPACK_IMPORTED_MODULE_27__["exhaust"]; }); -/* harmony import */ var _internal_operators_exhaustMap__WEBPACK_IMPORTED_MODULE_28__ = __webpack_require__(290); +/* harmony import */ var _internal_operators_exhaustMap__WEBPACK_IMPORTED_MODULE_28__ = __webpack_require__(291); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "exhaustMap", function() { return _internal_operators_exhaustMap__WEBPACK_IMPORTED_MODULE_28__["exhaustMap"]; }); -/* harmony import */ var _internal_operators_expand__WEBPACK_IMPORTED_MODULE_29__ = __webpack_require__(291); +/* harmony import */ var _internal_operators_expand__WEBPACK_IMPORTED_MODULE_29__ = __webpack_require__(292); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "expand", function() { return _internal_operators_expand__WEBPACK_IMPORTED_MODULE_29__["expand"]; }); -/* harmony import */ var _internal_operators_filter__WEBPACK_IMPORTED_MODULE_30__ = __webpack_require__(283); +/* harmony import */ var _internal_operators_filter__WEBPACK_IMPORTED_MODULE_30__ = __webpack_require__(284); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "filter", function() { return _internal_operators_filter__WEBPACK_IMPORTED_MODULE_30__["filter"]; }); -/* harmony import */ var _internal_operators_finalize__WEBPACK_IMPORTED_MODULE_31__ = __webpack_require__(292); +/* harmony import */ var _internal_operators_finalize__WEBPACK_IMPORTED_MODULE_31__ = __webpack_require__(293); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "finalize", function() { return _internal_operators_finalize__WEBPACK_IMPORTED_MODULE_31__["finalize"]; }); -/* harmony import */ var _internal_operators_find__WEBPACK_IMPORTED_MODULE_32__ = __webpack_require__(293); +/* harmony import */ var _internal_operators_find__WEBPACK_IMPORTED_MODULE_32__ = __webpack_require__(294); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "find", function() { return _internal_operators_find__WEBPACK_IMPORTED_MODULE_32__["find"]; }); -/* harmony import */ var _internal_operators_findIndex__WEBPACK_IMPORTED_MODULE_33__ = __webpack_require__(294); +/* harmony import */ var _internal_operators_findIndex__WEBPACK_IMPORTED_MODULE_33__ = __webpack_require__(295); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "findIndex", function() { return _internal_operators_findIndex__WEBPACK_IMPORTED_MODULE_33__["findIndex"]; }); -/* harmony import */ var _internal_operators_first__WEBPACK_IMPORTED_MODULE_34__ = __webpack_require__(295); +/* harmony import */ var _internal_operators_first__WEBPACK_IMPORTED_MODULE_34__ = __webpack_require__(296); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "first", function() { return _internal_operators_first__WEBPACK_IMPORTED_MODULE_34__["first"]; }); -/* harmony import */ var _internal_operators_groupBy__WEBPACK_IMPORTED_MODULE_35__ = __webpack_require__(182); +/* harmony import */ var _internal_operators_groupBy__WEBPACK_IMPORTED_MODULE_35__ = __webpack_require__(183); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "groupBy", function() { return _internal_operators_groupBy__WEBPACK_IMPORTED_MODULE_35__["groupBy"]; }); -/* harmony import */ var _internal_operators_ignoreElements__WEBPACK_IMPORTED_MODULE_36__ = __webpack_require__(296); +/* harmony import */ var _internal_operators_ignoreElements__WEBPACK_IMPORTED_MODULE_36__ = __webpack_require__(297); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "ignoreElements", function() { return _internal_operators_ignoreElements__WEBPACK_IMPORTED_MODULE_36__["ignoreElements"]; }); -/* harmony import */ var _internal_operators_isEmpty__WEBPACK_IMPORTED_MODULE_37__ = __webpack_require__(297); +/* harmony import */ var _internal_operators_isEmpty__WEBPACK_IMPORTED_MODULE_37__ = __webpack_require__(298); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "isEmpty", function() { return _internal_operators_isEmpty__WEBPACK_IMPORTED_MODULE_37__["isEmpty"]; }); -/* harmony import */ var _internal_operators_last__WEBPACK_IMPORTED_MODULE_38__ = __webpack_require__(298); +/* harmony import */ var _internal_operators_last__WEBPACK_IMPORTED_MODULE_38__ = __webpack_require__(299); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "last", function() { return _internal_operators_last__WEBPACK_IMPORTED_MODULE_38__["last"]; }); -/* harmony import */ var _internal_operators_map__WEBPACK_IMPORTED_MODULE_39__ = __webpack_require__(217); +/* harmony import */ var _internal_operators_map__WEBPACK_IMPORTED_MODULE_39__ = __webpack_require__(218); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "map", function() { return _internal_operators_map__WEBPACK_IMPORTED_MODULE_39__["map"]; }); -/* harmony import */ var _internal_operators_mapTo__WEBPACK_IMPORTED_MODULE_40__ = __webpack_require__(300); +/* harmony import */ var _internal_operators_mapTo__WEBPACK_IMPORTED_MODULE_40__ = __webpack_require__(301); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mapTo", function() { return _internal_operators_mapTo__WEBPACK_IMPORTED_MODULE_40__["mapTo"]; }); -/* harmony import */ var _internal_operators_materialize__WEBPACK_IMPORTED_MODULE_41__ = __webpack_require__(301); +/* harmony import */ var _internal_operators_materialize__WEBPACK_IMPORTED_MODULE_41__ = __webpack_require__(302); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "materialize", function() { return _internal_operators_materialize__WEBPACK_IMPORTED_MODULE_41__["materialize"]; }); -/* harmony import */ var _internal_operators_max__WEBPACK_IMPORTED_MODULE_42__ = __webpack_require__(302); +/* harmony import */ var _internal_operators_max__WEBPACK_IMPORTED_MODULE_42__ = __webpack_require__(303); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "max", function() { return _internal_operators_max__WEBPACK_IMPORTED_MODULE_42__["max"]; }); -/* harmony import */ var _internal_operators_merge__WEBPACK_IMPORTED_MODULE_43__ = __webpack_require__(305); +/* harmony import */ var _internal_operators_merge__WEBPACK_IMPORTED_MODULE_43__ = __webpack_require__(306); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "merge", function() { return _internal_operators_merge__WEBPACK_IMPORTED_MODULE_43__["merge"]; }); -/* harmony import */ var _internal_operators_mergeAll__WEBPACK_IMPORTED_MODULE_44__ = __webpack_require__(238); +/* harmony import */ var _internal_operators_mergeAll__WEBPACK_IMPORTED_MODULE_44__ = __webpack_require__(239); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mergeAll", function() { return _internal_operators_mergeAll__WEBPACK_IMPORTED_MODULE_44__["mergeAll"]; }); -/* harmony import */ var _internal_operators_mergeMap__WEBPACK_IMPORTED_MODULE_45__ = __webpack_require__(239); +/* harmony import */ var _internal_operators_mergeMap__WEBPACK_IMPORTED_MODULE_45__ = __webpack_require__(240); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mergeMap", function() { return _internal_operators_mergeMap__WEBPACK_IMPORTED_MODULE_45__["mergeMap"]; }); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "flatMap", function() { return _internal_operators_mergeMap__WEBPACK_IMPORTED_MODULE_45__["mergeMap"]; }); -/* harmony import */ var _internal_operators_mergeMapTo__WEBPACK_IMPORTED_MODULE_46__ = __webpack_require__(306); +/* harmony import */ var _internal_operators_mergeMapTo__WEBPACK_IMPORTED_MODULE_46__ = __webpack_require__(307); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mergeMapTo", function() { return _internal_operators_mergeMapTo__WEBPACK_IMPORTED_MODULE_46__["mergeMapTo"]; }); -/* harmony import */ var _internal_operators_mergeScan__WEBPACK_IMPORTED_MODULE_47__ = __webpack_require__(307); +/* harmony import */ var _internal_operators_mergeScan__WEBPACK_IMPORTED_MODULE_47__ = __webpack_require__(308); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mergeScan", function() { return _internal_operators_mergeScan__WEBPACK_IMPORTED_MODULE_47__["mergeScan"]; }); -/* harmony import */ var _internal_operators_min__WEBPACK_IMPORTED_MODULE_48__ = __webpack_require__(308); +/* harmony import */ var _internal_operators_min__WEBPACK_IMPORTED_MODULE_48__ = __webpack_require__(309); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "min", function() { return _internal_operators_min__WEBPACK_IMPORTED_MODULE_48__["min"]; }); -/* harmony import */ var _internal_operators_multicast__WEBPACK_IMPORTED_MODULE_49__ = __webpack_require__(309); +/* harmony import */ var _internal_operators_multicast__WEBPACK_IMPORTED_MODULE_49__ = __webpack_require__(310); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "multicast", function() { return _internal_operators_multicast__WEBPACK_IMPORTED_MODULE_49__["multicast"]; }); -/* harmony import */ var _internal_operators_observeOn__WEBPACK_IMPORTED_MODULE_50__ = __webpack_require__(192); +/* harmony import */ var _internal_operators_observeOn__WEBPACK_IMPORTED_MODULE_50__ = __webpack_require__(193); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "observeOn", function() { return _internal_operators_observeOn__WEBPACK_IMPORTED_MODULE_50__["observeOn"]; }); -/* harmony import */ var _internal_operators_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_51__ = __webpack_require__(310); +/* harmony import */ var _internal_operators_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_51__ = __webpack_require__(311); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "onErrorResumeNext", function() { return _internal_operators_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_51__["onErrorResumeNext"]; }); -/* harmony import */ var _internal_operators_pairwise__WEBPACK_IMPORTED_MODULE_52__ = __webpack_require__(311); +/* harmony import */ var _internal_operators_pairwise__WEBPACK_IMPORTED_MODULE_52__ = __webpack_require__(312); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "pairwise", function() { return _internal_operators_pairwise__WEBPACK_IMPORTED_MODULE_52__["pairwise"]; }); -/* harmony import */ var _internal_operators_partition__WEBPACK_IMPORTED_MODULE_53__ = __webpack_require__(312); +/* harmony import */ var _internal_operators_partition__WEBPACK_IMPORTED_MODULE_53__ = __webpack_require__(313); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "partition", function() { return _internal_operators_partition__WEBPACK_IMPORTED_MODULE_53__["partition"]; }); -/* harmony import */ var _internal_operators_pluck__WEBPACK_IMPORTED_MODULE_54__ = __webpack_require__(314); +/* harmony import */ var _internal_operators_pluck__WEBPACK_IMPORTED_MODULE_54__ = __webpack_require__(315); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "pluck", function() { return _internal_operators_pluck__WEBPACK_IMPORTED_MODULE_54__["pluck"]; }); -/* harmony import */ var _internal_operators_publish__WEBPACK_IMPORTED_MODULE_55__ = __webpack_require__(315); +/* harmony import */ var _internal_operators_publish__WEBPACK_IMPORTED_MODULE_55__ = __webpack_require__(316); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publish", function() { return _internal_operators_publish__WEBPACK_IMPORTED_MODULE_55__["publish"]; }); -/* harmony import */ var _internal_operators_publishBehavior__WEBPACK_IMPORTED_MODULE_56__ = __webpack_require__(316); +/* harmony import */ var _internal_operators_publishBehavior__WEBPACK_IMPORTED_MODULE_56__ = __webpack_require__(317); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publishBehavior", function() { return _internal_operators_publishBehavior__WEBPACK_IMPORTED_MODULE_56__["publishBehavior"]; }); -/* harmony import */ var _internal_operators_publishLast__WEBPACK_IMPORTED_MODULE_57__ = __webpack_require__(317); +/* harmony import */ var _internal_operators_publishLast__WEBPACK_IMPORTED_MODULE_57__ = __webpack_require__(318); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publishLast", function() { return _internal_operators_publishLast__WEBPACK_IMPORTED_MODULE_57__["publishLast"]; }); -/* harmony import */ var _internal_operators_publishReplay__WEBPACK_IMPORTED_MODULE_58__ = __webpack_require__(318); +/* harmony import */ var _internal_operators_publishReplay__WEBPACK_IMPORTED_MODULE_58__ = __webpack_require__(319); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publishReplay", function() { return _internal_operators_publishReplay__WEBPACK_IMPORTED_MODULE_58__["publishReplay"]; }); -/* harmony import */ var _internal_operators_race__WEBPACK_IMPORTED_MODULE_59__ = __webpack_require__(319); +/* harmony import */ var _internal_operators_race__WEBPACK_IMPORTED_MODULE_59__ = __webpack_require__(320); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "race", function() { return _internal_operators_race__WEBPACK_IMPORTED_MODULE_59__["race"]; }); -/* harmony import */ var _internal_operators_reduce__WEBPACK_IMPORTED_MODULE_60__ = __webpack_require__(303); +/* harmony import */ var _internal_operators_reduce__WEBPACK_IMPORTED_MODULE_60__ = __webpack_require__(304); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "reduce", function() { return _internal_operators_reduce__WEBPACK_IMPORTED_MODULE_60__["reduce"]; }); -/* harmony import */ var _internal_operators_repeat__WEBPACK_IMPORTED_MODULE_61__ = __webpack_require__(320); +/* harmony import */ var _internal_operators_repeat__WEBPACK_IMPORTED_MODULE_61__ = __webpack_require__(321); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "repeat", function() { return _internal_operators_repeat__WEBPACK_IMPORTED_MODULE_61__["repeat"]; }); -/* harmony import */ var _internal_operators_repeatWhen__WEBPACK_IMPORTED_MODULE_62__ = __webpack_require__(321); +/* harmony import */ var _internal_operators_repeatWhen__WEBPACK_IMPORTED_MODULE_62__ = __webpack_require__(322); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "repeatWhen", function() { return _internal_operators_repeatWhen__WEBPACK_IMPORTED_MODULE_62__["repeatWhen"]; }); -/* harmony import */ var _internal_operators_retry__WEBPACK_IMPORTED_MODULE_63__ = __webpack_require__(322); +/* harmony import */ var _internal_operators_retry__WEBPACK_IMPORTED_MODULE_63__ = __webpack_require__(323); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "retry", function() { return _internal_operators_retry__WEBPACK_IMPORTED_MODULE_63__["retry"]; }); -/* harmony import */ var _internal_operators_retryWhen__WEBPACK_IMPORTED_MODULE_64__ = __webpack_require__(323); +/* harmony import */ var _internal_operators_retryWhen__WEBPACK_IMPORTED_MODULE_64__ = __webpack_require__(324); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "retryWhen", function() { return _internal_operators_retryWhen__WEBPACK_IMPORTED_MODULE_64__["retryWhen"]; }); -/* harmony import */ var _internal_operators_refCount__WEBPACK_IMPORTED_MODULE_65__ = __webpack_require__(181); +/* harmony import */ var _internal_operators_refCount__WEBPACK_IMPORTED_MODULE_65__ = __webpack_require__(182); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "refCount", function() { return _internal_operators_refCount__WEBPACK_IMPORTED_MODULE_65__["refCount"]; }); -/* harmony import */ var _internal_operators_sample__WEBPACK_IMPORTED_MODULE_66__ = __webpack_require__(324); +/* harmony import */ var _internal_operators_sample__WEBPACK_IMPORTED_MODULE_66__ = __webpack_require__(325); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "sample", function() { return _internal_operators_sample__WEBPACK_IMPORTED_MODULE_66__["sample"]; }); -/* harmony import */ var _internal_operators_sampleTime__WEBPACK_IMPORTED_MODULE_67__ = __webpack_require__(325); +/* harmony import */ var _internal_operators_sampleTime__WEBPACK_IMPORTED_MODULE_67__ = __webpack_require__(326); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "sampleTime", function() { return _internal_operators_sampleTime__WEBPACK_IMPORTED_MODULE_67__["sampleTime"]; }); -/* harmony import */ var _internal_operators_scan__WEBPACK_IMPORTED_MODULE_68__ = __webpack_require__(304); +/* harmony import */ var _internal_operators_scan__WEBPACK_IMPORTED_MODULE_68__ = __webpack_require__(305); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "scan", function() { return _internal_operators_scan__WEBPACK_IMPORTED_MODULE_68__["scan"]; }); -/* harmony import */ var _internal_operators_sequenceEqual__WEBPACK_IMPORTED_MODULE_69__ = __webpack_require__(326); +/* harmony import */ var _internal_operators_sequenceEqual__WEBPACK_IMPORTED_MODULE_69__ = __webpack_require__(327); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "sequenceEqual", function() { return _internal_operators_sequenceEqual__WEBPACK_IMPORTED_MODULE_69__["sequenceEqual"]; }); -/* harmony import */ var _internal_operators_share__WEBPACK_IMPORTED_MODULE_70__ = __webpack_require__(327); +/* harmony import */ var _internal_operators_share__WEBPACK_IMPORTED_MODULE_70__ = __webpack_require__(328); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "share", function() { return _internal_operators_share__WEBPACK_IMPORTED_MODULE_70__["share"]; }); -/* harmony import */ var _internal_operators_shareReplay__WEBPACK_IMPORTED_MODULE_71__ = __webpack_require__(328); +/* harmony import */ var _internal_operators_shareReplay__WEBPACK_IMPORTED_MODULE_71__ = __webpack_require__(329); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "shareReplay", function() { return _internal_operators_shareReplay__WEBPACK_IMPORTED_MODULE_71__["shareReplay"]; }); -/* harmony import */ var _internal_operators_single__WEBPACK_IMPORTED_MODULE_72__ = __webpack_require__(329); +/* harmony import */ var _internal_operators_single__WEBPACK_IMPORTED_MODULE_72__ = __webpack_require__(330); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "single", function() { return _internal_operators_single__WEBPACK_IMPORTED_MODULE_72__["single"]; }); -/* harmony import */ var _internal_operators_skip__WEBPACK_IMPORTED_MODULE_73__ = __webpack_require__(330); +/* harmony import */ var _internal_operators_skip__WEBPACK_IMPORTED_MODULE_73__ = __webpack_require__(331); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skip", function() { return _internal_operators_skip__WEBPACK_IMPORTED_MODULE_73__["skip"]; }); -/* harmony import */ var _internal_operators_skipLast__WEBPACK_IMPORTED_MODULE_74__ = __webpack_require__(331); +/* harmony import */ var _internal_operators_skipLast__WEBPACK_IMPORTED_MODULE_74__ = __webpack_require__(332); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skipLast", function() { return _internal_operators_skipLast__WEBPACK_IMPORTED_MODULE_74__["skipLast"]; }); -/* harmony import */ var _internal_operators_skipUntil__WEBPACK_IMPORTED_MODULE_75__ = __webpack_require__(332); +/* harmony import */ var _internal_operators_skipUntil__WEBPACK_IMPORTED_MODULE_75__ = __webpack_require__(333); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skipUntil", function() { return _internal_operators_skipUntil__WEBPACK_IMPORTED_MODULE_75__["skipUntil"]; }); -/* harmony import */ var _internal_operators_skipWhile__WEBPACK_IMPORTED_MODULE_76__ = __webpack_require__(333); +/* harmony import */ var _internal_operators_skipWhile__WEBPACK_IMPORTED_MODULE_76__ = __webpack_require__(334); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skipWhile", function() { return _internal_operators_skipWhile__WEBPACK_IMPORTED_MODULE_76__["skipWhile"]; }); -/* harmony import */ var _internal_operators_startWith__WEBPACK_IMPORTED_MODULE_77__ = __webpack_require__(334); +/* harmony import */ var _internal_operators_startWith__WEBPACK_IMPORTED_MODULE_77__ = __webpack_require__(335); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "startWith", function() { return _internal_operators_startWith__WEBPACK_IMPORTED_MODULE_77__["startWith"]; }); -/* harmony import */ var _internal_operators_subscribeOn__WEBPACK_IMPORTED_MODULE_78__ = __webpack_require__(335); +/* harmony import */ var _internal_operators_subscribeOn__WEBPACK_IMPORTED_MODULE_78__ = __webpack_require__(336); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "subscribeOn", function() { return _internal_operators_subscribeOn__WEBPACK_IMPORTED_MODULE_78__["subscribeOn"]; }); -/* harmony import */ var _internal_operators_switchAll__WEBPACK_IMPORTED_MODULE_79__ = __webpack_require__(337); +/* harmony import */ var _internal_operators_switchAll__WEBPACK_IMPORTED_MODULE_79__ = __webpack_require__(338); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "switchAll", function() { return _internal_operators_switchAll__WEBPACK_IMPORTED_MODULE_79__["switchAll"]; }); -/* harmony import */ var _internal_operators_switchMap__WEBPACK_IMPORTED_MODULE_80__ = __webpack_require__(338); +/* harmony import */ var _internal_operators_switchMap__WEBPACK_IMPORTED_MODULE_80__ = __webpack_require__(339); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "switchMap", function() { return _internal_operators_switchMap__WEBPACK_IMPORTED_MODULE_80__["switchMap"]; }); -/* harmony import */ var _internal_operators_switchMapTo__WEBPACK_IMPORTED_MODULE_81__ = __webpack_require__(339); +/* harmony import */ var _internal_operators_switchMapTo__WEBPACK_IMPORTED_MODULE_81__ = __webpack_require__(340); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "switchMapTo", function() { return _internal_operators_switchMapTo__WEBPACK_IMPORTED_MODULE_81__["switchMapTo"]; }); -/* harmony import */ var _internal_operators_take__WEBPACK_IMPORTED_MODULE_82__ = __webpack_require__(286); +/* harmony import */ var _internal_operators_take__WEBPACK_IMPORTED_MODULE_82__ = __webpack_require__(287); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "take", function() { return _internal_operators_take__WEBPACK_IMPORTED_MODULE_82__["take"]; }); -/* harmony import */ var _internal_operators_takeLast__WEBPACK_IMPORTED_MODULE_83__ = __webpack_require__(299); +/* harmony import */ var _internal_operators_takeLast__WEBPACK_IMPORTED_MODULE_83__ = __webpack_require__(300); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "takeLast", function() { return _internal_operators_takeLast__WEBPACK_IMPORTED_MODULE_83__["takeLast"]; }); -/* harmony import */ var _internal_operators_takeUntil__WEBPACK_IMPORTED_MODULE_84__ = __webpack_require__(340); +/* harmony import */ var _internal_operators_takeUntil__WEBPACK_IMPORTED_MODULE_84__ = __webpack_require__(341); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "takeUntil", function() { return _internal_operators_takeUntil__WEBPACK_IMPORTED_MODULE_84__["takeUntil"]; }); -/* harmony import */ var _internal_operators_takeWhile__WEBPACK_IMPORTED_MODULE_85__ = __webpack_require__(341); +/* harmony import */ var _internal_operators_takeWhile__WEBPACK_IMPORTED_MODULE_85__ = __webpack_require__(342); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "takeWhile", function() { return _internal_operators_takeWhile__WEBPACK_IMPORTED_MODULE_85__["takeWhile"]; }); -/* harmony import */ var _internal_operators_tap__WEBPACK_IMPORTED_MODULE_86__ = __webpack_require__(285); +/* harmony import */ var _internal_operators_tap__WEBPACK_IMPORTED_MODULE_86__ = __webpack_require__(286); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "tap", function() { return _internal_operators_tap__WEBPACK_IMPORTED_MODULE_86__["tap"]; }); -/* harmony import */ var _internal_operators_throttle__WEBPACK_IMPORTED_MODULE_87__ = __webpack_require__(342); +/* harmony import */ var _internal_operators_throttle__WEBPACK_IMPORTED_MODULE_87__ = __webpack_require__(343); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "throttle", function() { return _internal_operators_throttle__WEBPACK_IMPORTED_MODULE_87__["throttle"]; }); -/* harmony import */ var _internal_operators_throttleTime__WEBPACK_IMPORTED_MODULE_88__ = __webpack_require__(343); +/* harmony import */ var _internal_operators_throttleTime__WEBPACK_IMPORTED_MODULE_88__ = __webpack_require__(344); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "throttleTime", function() { return _internal_operators_throttleTime__WEBPACK_IMPORTED_MODULE_88__["throttleTime"]; }); -/* harmony import */ var _internal_operators_throwIfEmpty__WEBPACK_IMPORTED_MODULE_89__ = __webpack_require__(284); +/* harmony import */ var _internal_operators_throwIfEmpty__WEBPACK_IMPORTED_MODULE_89__ = __webpack_require__(285); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "throwIfEmpty", function() { return _internal_operators_throwIfEmpty__WEBPACK_IMPORTED_MODULE_89__["throwIfEmpty"]; }); -/* harmony import */ var _internal_operators_timeInterval__WEBPACK_IMPORTED_MODULE_90__ = __webpack_require__(344); +/* harmony import */ var _internal_operators_timeInterval__WEBPACK_IMPORTED_MODULE_90__ = __webpack_require__(345); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timeInterval", function() { return _internal_operators_timeInterval__WEBPACK_IMPORTED_MODULE_90__["timeInterval"]; }); -/* harmony import */ var _internal_operators_timeout__WEBPACK_IMPORTED_MODULE_91__ = __webpack_require__(345); +/* harmony import */ var _internal_operators_timeout__WEBPACK_IMPORTED_MODULE_91__ = __webpack_require__(346); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timeout", function() { return _internal_operators_timeout__WEBPACK_IMPORTED_MODULE_91__["timeout"]; }); -/* harmony import */ var _internal_operators_timeoutWith__WEBPACK_IMPORTED_MODULE_92__ = __webpack_require__(346); +/* harmony import */ var _internal_operators_timeoutWith__WEBPACK_IMPORTED_MODULE_92__ = __webpack_require__(347); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timeoutWith", function() { return _internal_operators_timeoutWith__WEBPACK_IMPORTED_MODULE_92__["timeoutWith"]; }); -/* harmony import */ var _internal_operators_timestamp__WEBPACK_IMPORTED_MODULE_93__ = __webpack_require__(347); +/* harmony import */ var _internal_operators_timestamp__WEBPACK_IMPORTED_MODULE_93__ = __webpack_require__(348); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timestamp", function() { return _internal_operators_timestamp__WEBPACK_IMPORTED_MODULE_93__["timestamp"]; }); -/* harmony import */ var _internal_operators_toArray__WEBPACK_IMPORTED_MODULE_94__ = __webpack_require__(348); +/* harmony import */ var _internal_operators_toArray__WEBPACK_IMPORTED_MODULE_94__ = __webpack_require__(349); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "toArray", function() { return _internal_operators_toArray__WEBPACK_IMPORTED_MODULE_94__["toArray"]; }); -/* harmony import */ var _internal_operators_window__WEBPACK_IMPORTED_MODULE_95__ = __webpack_require__(349); +/* harmony import */ var _internal_operators_window__WEBPACK_IMPORTED_MODULE_95__ = __webpack_require__(350); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "window", function() { return _internal_operators_window__WEBPACK_IMPORTED_MODULE_95__["window"]; }); -/* harmony import */ var _internal_operators_windowCount__WEBPACK_IMPORTED_MODULE_96__ = __webpack_require__(350); +/* harmony import */ var _internal_operators_windowCount__WEBPACK_IMPORTED_MODULE_96__ = __webpack_require__(351); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowCount", function() { return _internal_operators_windowCount__WEBPACK_IMPORTED_MODULE_96__["windowCount"]; }); -/* harmony import */ var _internal_operators_windowTime__WEBPACK_IMPORTED_MODULE_97__ = __webpack_require__(351); +/* harmony import */ var _internal_operators_windowTime__WEBPACK_IMPORTED_MODULE_97__ = __webpack_require__(352); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowTime", function() { return _internal_operators_windowTime__WEBPACK_IMPORTED_MODULE_97__["windowTime"]; }); -/* harmony import */ var _internal_operators_windowToggle__WEBPACK_IMPORTED_MODULE_98__ = __webpack_require__(352); +/* harmony import */ var _internal_operators_windowToggle__WEBPACK_IMPORTED_MODULE_98__ = __webpack_require__(353); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowToggle", function() { return _internal_operators_windowToggle__WEBPACK_IMPORTED_MODULE_98__["windowToggle"]; }); -/* harmony import */ var _internal_operators_windowWhen__WEBPACK_IMPORTED_MODULE_99__ = __webpack_require__(353); +/* harmony import */ var _internal_operators_windowWhen__WEBPACK_IMPORTED_MODULE_99__ = __webpack_require__(354); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowWhen", function() { return _internal_operators_windowWhen__WEBPACK_IMPORTED_MODULE_99__["windowWhen"]; }); -/* harmony import */ var _internal_operators_withLatestFrom__WEBPACK_IMPORTED_MODULE_100__ = __webpack_require__(354); +/* harmony import */ var _internal_operators_withLatestFrom__WEBPACK_IMPORTED_MODULE_100__ = __webpack_require__(355); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "withLatestFrom", function() { return _internal_operators_withLatestFrom__WEBPACK_IMPORTED_MODULE_100__["withLatestFrom"]; }); -/* harmony import */ var _internal_operators_zip__WEBPACK_IMPORTED_MODULE_101__ = __webpack_require__(355); +/* harmony import */ var _internal_operators_zip__WEBPACK_IMPORTED_MODULE_101__ = __webpack_require__(356); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "zip", function() { return _internal_operators_zip__WEBPACK_IMPORTED_MODULE_101__["zip"]; }); -/* harmony import */ var _internal_operators_zipAll__WEBPACK_IMPORTED_MODULE_102__ = __webpack_require__(356); +/* harmony import */ var _internal_operators_zipAll__WEBPACK_IMPORTED_MODULE_102__ = __webpack_require__(357); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "zipAll", function() { return _internal_operators_zipAll__WEBPACK_IMPORTED_MODULE_102__["zipAll"]; }); /** PURE_IMPORTS_START PURE_IMPORTS_END */ @@ -25460,17 +25536,17 @@ __webpack_require__.r(__webpack_exports__); /***/ }), -/* 258 */ +/* 259 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "audit", function() { return audit; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _util_tryCatch__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(170); -/* harmony import */ var _util_errorObject__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(171); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(220); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(221); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _util_tryCatch__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(171); +/* harmony import */ var _util_errorObject__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(172); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(221); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(222); /** PURE_IMPORTS_START tslib,_util_tryCatch,_util_errorObject,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ @@ -25543,15 +25619,15 @@ var AuditSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 259 */ +/* 260 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "auditTime", function() { return auditTime; }); -/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(206); -/* harmony import */ var _audit__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(258); -/* harmony import */ var _observable_timer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(254); +/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(207); +/* harmony import */ var _audit__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(259); +/* harmony import */ var _observable_timer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(255); /** PURE_IMPORTS_START _scheduler_async,_audit,_observable_timer PURE_IMPORTS_END */ @@ -25566,15 +25642,15 @@ function auditTime(duration, scheduler) { /***/ }), -/* 260 */ +/* 261 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buffer", function() { return buffer; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(220); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(221); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(221); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(222); /** PURE_IMPORTS_START tslib,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ @@ -25615,14 +25691,14 @@ var BufferSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 261 */ +/* 262 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "bufferCount", function() { return bufferCount; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); /** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ @@ -25716,16 +25792,16 @@ var BufferSkipCountSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 262 */ +/* 263 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "bufferTime", function() { return bufferTime; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(206); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(161); -/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(196); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(207); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(162); +/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(197); /** PURE_IMPORTS_START tslib,_scheduler_async,_Subscriber,_util_isScheduler PURE_IMPORTS_END */ @@ -25877,16 +25953,16 @@ function dispatchBufferClose(arg) { /***/ }), -/* 263 */ +/* 264 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "bufferToggle", function() { return bufferToggle; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(167); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(221); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(220); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(168); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(222); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(221); /** PURE_IMPORTS_START tslib,_Subscription,_util_subscribeToResult,_OuterSubscriber PURE_IMPORTS_END */ @@ -25997,18 +26073,18 @@ var BufferToggleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 264 */ +/* 265 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "bufferWhen", function() { return bufferWhen; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(167); -/* harmony import */ var _util_tryCatch__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(170); -/* harmony import */ var _util_errorObject__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(171); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(220); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(221); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(168); +/* harmony import */ var _util_tryCatch__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(171); +/* harmony import */ var _util_errorObject__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(172); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(221); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(222); /** PURE_IMPORTS_START tslib,_Subscription,_util_tryCatch,_util_errorObject,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ @@ -26094,15 +26170,15 @@ var BufferWhenSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 265 */ +/* 266 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "catchError", function() { return catchError; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(220); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(221); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(221); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(222); /** PURE_IMPORTS_START tslib,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ @@ -26151,13 +26227,13 @@ var CatchSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 266 */ +/* 267 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "combineAll", function() { return combineAll; }); -/* harmony import */ var _observable_combineLatest__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(219); +/* harmony import */ var _observable_combineLatest__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(220); /** PURE_IMPORTS_START _observable_combineLatest PURE_IMPORTS_END */ function combineAll(project) { @@ -26167,15 +26243,15 @@ function combineAll(project) { /***/ }), -/* 267 */ +/* 268 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "combineLatest", function() { return combineLatest; }); -/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(168); -/* harmony import */ var _observable_combineLatest__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(219); -/* harmony import */ var _observable_from__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(231); +/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(169); +/* harmony import */ var _observable_combineLatest__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(220); +/* harmony import */ var _observable_from__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(232); /** PURE_IMPORTS_START _util_isArray,_observable_combineLatest,_observable_from PURE_IMPORTS_END */ @@ -26199,13 +26275,13 @@ function combineLatest() { /***/ }), -/* 268 */ +/* 269 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "concat", function() { return concat; }); -/* harmony import */ var _observable_concat__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(230); +/* harmony import */ var _observable_concat__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(231); /** PURE_IMPORTS_START _observable_concat PURE_IMPORTS_END */ function concat() { @@ -26219,13 +26295,13 @@ function concat() { /***/ }), -/* 269 */ +/* 270 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "concatMap", function() { return concatMap; }); -/* harmony import */ var _mergeMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(239); +/* harmony import */ var _mergeMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(240); /** PURE_IMPORTS_START _mergeMap PURE_IMPORTS_END */ function concatMap(project, resultSelector) { @@ -26235,13 +26311,13 @@ function concatMap(project, resultSelector) { /***/ }), -/* 270 */ +/* 271 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "concatMapTo", function() { return concatMapTo; }); -/* harmony import */ var _concatMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(269); +/* harmony import */ var _concatMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(270); /** PURE_IMPORTS_START _concatMap PURE_IMPORTS_END */ function concatMapTo(innerObservable, resultSelector) { @@ -26251,14 +26327,14 @@ function concatMapTo(innerObservable, resultSelector) { /***/ }), -/* 271 */ +/* 272 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "count", function() { return count; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); /** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ @@ -26316,15 +26392,15 @@ var CountSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 272 */ +/* 273 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "debounce", function() { return debounce; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(220); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(221); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(221); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(222); /** PURE_IMPORTS_START tslib,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ @@ -26404,15 +26480,15 @@ var DebounceSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 273 */ +/* 274 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "debounceTime", function() { return debounceTime; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); -/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(206); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); +/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(207); /** PURE_IMPORTS_START tslib,_Subscriber,_scheduler_async PURE_IMPORTS_END */ @@ -26480,14 +26556,14 @@ function dispatchNext(subscriber) { /***/ }), -/* 274 */ +/* 275 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "defaultIfEmpty", function() { return defaultIfEmpty; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); /** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ @@ -26530,17 +26606,17 @@ var DefaultIfEmptySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 275 */ +/* 276 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "delay", function() { return delay; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(206); -/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(276); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(161); -/* harmony import */ var _Notification__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(193); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(207); +/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(277); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(162); +/* harmony import */ var _Notification__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(194); /** PURE_IMPORTS_START tslib,_scheduler_async,_util_isDate,_Subscriber,_Notification PURE_IMPORTS_END */ @@ -26634,7 +26710,7 @@ var DelayMessage = /*@__PURE__*/ (function () { /***/ }), -/* 276 */ +/* 277 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -26648,17 +26724,17 @@ function isDate(value) { /***/ }), -/* 277 */ +/* 278 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "delayWhen", function() { return delayWhen; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(159); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(220); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(221); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(160); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(221); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(222); /** PURE_IMPORTS_START tslib,_Subscriber,_Observable,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ @@ -26789,14 +26865,14 @@ var SubscriptionDelaySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 278 */ +/* 279 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "dematerialize", function() { return dematerialize; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); /** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ @@ -26827,16 +26903,16 @@ var DeMaterializeSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 279 */ +/* 280 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "distinct", function() { return distinct; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "DistinctSubscriber", function() { return DistinctSubscriber; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(220); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(221); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(221); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(222); /** PURE_IMPORTS_START tslib,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ @@ -26905,16 +26981,16 @@ var DistinctSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 280 */ +/* 281 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "distinctUntilChanged", function() { return distinctUntilChanged; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); -/* harmony import */ var _util_tryCatch__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(170); -/* harmony import */ var _util_errorObject__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(171); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); +/* harmony import */ var _util_tryCatch__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(171); +/* harmony import */ var _util_errorObject__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(172); /** PURE_IMPORTS_START tslib,_Subscriber,_util_tryCatch,_util_errorObject PURE_IMPORTS_END */ @@ -26977,13 +27053,13 @@ var DistinctUntilChangedSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 281 */ +/* 282 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "distinctUntilKeyChanged", function() { return distinctUntilKeyChanged; }); -/* harmony import */ var _distinctUntilChanged__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(280); +/* harmony import */ var _distinctUntilChanged__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(281); /** PURE_IMPORTS_START _distinctUntilChanged PURE_IMPORTS_END */ function distinctUntilKeyChanged(key, compare) { @@ -26993,17 +27069,17 @@ function distinctUntilKeyChanged(key, compare) { /***/ }), -/* 282 */ +/* 283 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "elementAt", function() { return elementAt; }); -/* harmony import */ var _util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(213); -/* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(283); -/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(284); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(274); -/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(286); +/* harmony import */ var _util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(214); +/* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(284); +/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(285); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(275); +/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(287); /** PURE_IMPORTS_START _util_ArgumentOutOfRangeError,_filter,_throwIfEmpty,_defaultIfEmpty,_take PURE_IMPORTS_END */ @@ -27025,14 +27101,14 @@ function elementAt(index, defaultValue) { /***/ }), -/* 283 */ +/* 284 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "filter", function() { return filter; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); /** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ @@ -27079,14 +27155,14 @@ var FilterSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 284 */ +/* 285 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "throwIfEmpty", function() { return throwIfEmpty; }); -/* harmony import */ var _tap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(285); -/* harmony import */ var _util_EmptyError__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(214); +/* harmony import */ var _tap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(286); +/* harmony import */ var _util_EmptyError__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(215); /** PURE_IMPORTS_START _tap,_util_EmptyError PURE_IMPORTS_END */ @@ -27111,16 +27187,16 @@ function defaultErrorFactory() { /***/ }), -/* 285 */ +/* 286 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "tap", function() { return tap; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); -/* harmony import */ var _util_noop__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(176); -/* harmony import */ var _util_isFunction__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(163); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); +/* harmony import */ var _util_noop__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(177); +/* harmony import */ var _util_isFunction__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(164); /** PURE_IMPORTS_START tslib,_Subscriber,_util_noop,_util_isFunction PURE_IMPORTS_END */ @@ -27199,16 +27275,16 @@ var TapSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 286 */ +/* 287 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "take", function() { return take; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); -/* harmony import */ var _util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(213); -/* harmony import */ var _observable_empty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(194); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); +/* harmony import */ var _util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(214); +/* harmony import */ var _observable_empty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(195); /** PURE_IMPORTS_START tslib,_Subscriber,_util_ArgumentOutOfRangeError,_observable_empty PURE_IMPORTS_END */ @@ -27261,17 +27337,17 @@ var TakeSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 287 */ +/* 288 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "endWith", function() { return endWith; }); -/* harmony import */ var _observable_fromArray__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(197); -/* harmony import */ var _observable_scalar__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(199); -/* harmony import */ var _observable_empty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(194); -/* harmony import */ var _observable_concat__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(230); -/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(196); +/* harmony import */ var _observable_fromArray__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(198); +/* harmony import */ var _observable_scalar__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(200); +/* harmony import */ var _observable_empty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(195); +/* harmony import */ var _observable_concat__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(231); +/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(197); /** PURE_IMPORTS_START _observable_fromArray,_observable_scalar,_observable_empty,_observable_concat,_util_isScheduler PURE_IMPORTS_END */ @@ -27307,14 +27383,14 @@ function endWith() { /***/ }), -/* 288 */ +/* 289 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "every", function() { return every; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); /** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ @@ -27369,15 +27445,15 @@ var EverySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 289 */ +/* 290 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "exhaust", function() { return exhaust; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(220); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(221); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(221); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(222); /** PURE_IMPORTS_START tslib,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ @@ -27426,17 +27502,17 @@ var SwitchFirstSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 290 */ +/* 291 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "exhaustMap", function() { return exhaustMap; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(220); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(221); -/* harmony import */ var _map__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(217); -/* harmony import */ var _observable_from__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(231); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(221); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(222); +/* harmony import */ var _map__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(218); +/* harmony import */ var _observable_from__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(232); /** PURE_IMPORTS_START tslib,_OuterSubscriber,_util_subscribeToResult,_map,_observable_from PURE_IMPORTS_END */ @@ -27512,7 +27588,7 @@ var ExhaustMapSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 291 */ +/* 292 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -27520,11 +27596,11 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "expand", function() { return expand; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ExpandOperator", function() { return ExpandOperator; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ExpandSubscriber", function() { return ExpandSubscriber; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _util_tryCatch__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(170); -/* harmony import */ var _util_errorObject__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(171); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(220); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(221); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _util_tryCatch__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(171); +/* harmony import */ var _util_errorObject__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(172); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(221); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(222); /** PURE_IMPORTS_START tslib,_util_tryCatch,_util_errorObject,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ @@ -27628,15 +27704,15 @@ var ExpandSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 292 */ +/* 293 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "finalize", function() { return finalize; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); -/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(167); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); +/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(168); /** PURE_IMPORTS_START tslib,_Subscriber,_Subscription PURE_IMPORTS_END */ @@ -27666,7 +27742,7 @@ var FinallySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 293 */ +/* 294 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -27674,8 +27750,8 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "find", function() { return find; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "FindValueOperator", function() { return FindValueOperator; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "FindValueSubscriber", function() { return FindValueSubscriber; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); /** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ @@ -27737,13 +27813,13 @@ var FindValueSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 294 */ +/* 295 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "findIndex", function() { return findIndex; }); -/* harmony import */ var _operators_find__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(293); +/* harmony import */ var _operators_find__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(294); /** PURE_IMPORTS_START _operators_find PURE_IMPORTS_END */ function findIndex(predicate, thisArg) { @@ -27753,18 +27829,18 @@ function findIndex(predicate, thisArg) { /***/ }), -/* 295 */ +/* 296 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "first", function() { return first; }); -/* harmony import */ var _util_EmptyError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(214); -/* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(283); -/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(286); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(274); -/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(284); -/* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(211); +/* harmony import */ var _util_EmptyError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(215); +/* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(284); +/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(287); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(275); +/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(285); +/* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(212); /** PURE_IMPORTS_START _util_EmptyError,_filter,_take,_defaultIfEmpty,_throwIfEmpty,_util_identity PURE_IMPORTS_END */ @@ -27780,14 +27856,14 @@ function first(predicate, defaultValue) { /***/ }), -/* 296 */ +/* 297 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ignoreElements", function() { return ignoreElements; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); /** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ @@ -27817,14 +27893,14 @@ var IgnoreElementsSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 297 */ +/* 298 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isEmpty", function() { return isEmpty; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); /** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ @@ -27861,18 +27937,18 @@ var IsEmptySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 298 */ +/* 299 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "last", function() { return last; }); -/* harmony import */ var _util_EmptyError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(214); -/* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(283); -/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(299); -/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(284); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(274); -/* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(211); +/* harmony import */ var _util_EmptyError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(215); +/* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(284); +/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(300); +/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(285); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(275); +/* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(212); /** PURE_IMPORTS_START _util_EmptyError,_filter,_takeLast,_throwIfEmpty,_defaultIfEmpty,_util_identity PURE_IMPORTS_END */ @@ -27888,16 +27964,16 @@ function last(predicate, defaultValue) { /***/ }), -/* 299 */ +/* 300 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "takeLast", function() { return takeLast; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); -/* harmony import */ var _util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(213); -/* harmony import */ var _observable_empty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(194); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); +/* harmony import */ var _util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(214); +/* harmony import */ var _observable_empty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(195); /** PURE_IMPORTS_START tslib,_Subscriber,_util_ArgumentOutOfRangeError,_observable_empty PURE_IMPORTS_END */ @@ -27965,14 +28041,14 @@ var TakeLastSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 300 */ +/* 301 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "mapTo", function() { return mapTo; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); /** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ @@ -28004,15 +28080,15 @@ var MapToSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 301 */ +/* 302 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "materialize", function() { return materialize; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); -/* harmony import */ var _Notification__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(193); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); +/* harmony import */ var _Notification__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(194); /** PURE_IMPORTS_START tslib,_Subscriber,_Notification PURE_IMPORTS_END */ @@ -28054,13 +28130,13 @@ var MaterializeSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 302 */ +/* 303 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "max", function() { return max; }); -/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(303); +/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(304); /** PURE_IMPORTS_START _reduce PURE_IMPORTS_END */ function max(comparer) { @@ -28073,16 +28149,16 @@ function max(comparer) { /***/ }), -/* 303 */ +/* 304 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "reduce", function() { return reduce; }); -/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(304); -/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(299); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(274); -/* harmony import */ var _util_pipe__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(175); +/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(305); +/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(300); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(275); +/* harmony import */ var _util_pipe__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(176); /** PURE_IMPORTS_START _scan,_takeLast,_defaultIfEmpty,_util_pipe PURE_IMPORTS_END */ @@ -28104,14 +28180,14 @@ function reduce(accumulator, seed) { /***/ }), -/* 304 */ +/* 305 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "scan", function() { return scan; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); /** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ @@ -28186,13 +28262,13 @@ var ScanSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 305 */ +/* 306 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "merge", function() { return merge; }); -/* harmony import */ var _observable_merge__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(248); +/* harmony import */ var _observable_merge__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(249); /** PURE_IMPORTS_START _observable_merge PURE_IMPORTS_END */ function merge() { @@ -28206,13 +28282,13 @@ function merge() { /***/ }), -/* 306 */ +/* 307 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "mergeMapTo", function() { return mergeMapTo; }); -/* harmony import */ var _mergeMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(239); +/* harmony import */ var _mergeMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(240); /** PURE_IMPORTS_START _mergeMap PURE_IMPORTS_END */ function mergeMapTo(innerObservable, resultSelector, concurrent) { @@ -28231,7 +28307,7 @@ function mergeMapTo(innerObservable, resultSelector, concurrent) { /***/ }), -/* 307 */ +/* 308 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -28239,11 +28315,11 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "mergeScan", function() { return mergeScan; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MergeScanOperator", function() { return MergeScanOperator; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MergeScanSubscriber", function() { return MergeScanSubscriber; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _util_tryCatch__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(170); -/* harmony import */ var _util_errorObject__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(171); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(221); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(220); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _util_tryCatch__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(171); +/* harmony import */ var _util_errorObject__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(172); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(222); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(221); /** PURE_IMPORTS_START tslib,_util_tryCatch,_util_errorObject,_util_subscribeToResult,_OuterSubscriber PURE_IMPORTS_END */ @@ -28338,13 +28414,13 @@ var MergeScanSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 308 */ +/* 309 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "min", function() { return min; }); -/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(303); +/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(304); /** PURE_IMPORTS_START _reduce PURE_IMPORTS_END */ function min(comparer) { @@ -28357,14 +28433,14 @@ function min(comparer) { /***/ }), -/* 309 */ +/* 310 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "multicast", function() { return multicast; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MulticastOperator", function() { return MulticastOperator; }); -/* harmony import */ var _observable_ConnectableObservable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(177); +/* harmony import */ var _observable_ConnectableObservable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(178); /** PURE_IMPORTS_START _observable_ConnectableObservable PURE_IMPORTS_END */ function multicast(subjectOrSubjectFactory, selector) { @@ -28406,18 +28482,18 @@ var MulticastOperator = /*@__PURE__*/ (function () { /***/ }), -/* 310 */ +/* 311 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "onErrorResumeNext", function() { return onErrorResumeNext; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "onErrorResumeNextStatic", function() { return onErrorResumeNextStatic; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _observable_from__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(231); -/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(168); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(220); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(221); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _observable_from__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(232); +/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(169); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(221); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(222); /** PURE_IMPORTS_START tslib,_observable_from,_util_isArray,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ @@ -28490,14 +28566,14 @@ var OnErrorResumeNextSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 311 */ +/* 312 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "pairwise", function() { return pairwise; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); /** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ @@ -28534,14 +28610,14 @@ var PairwiseSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 312 */ +/* 313 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "partition", function() { return partition; }); -/* harmony import */ var _util_not__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(313); -/* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(283); +/* harmony import */ var _util_not__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(314); +/* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(284); /** PURE_IMPORTS_START _util_not,_filter PURE_IMPORTS_END */ @@ -28557,7 +28633,7 @@ function partition(predicate, thisArg) { /***/ }), -/* 313 */ +/* 314 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -28576,13 +28652,13 @@ function not(pred, thisArg) { /***/ }), -/* 314 */ +/* 315 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "pluck", function() { return pluck; }); -/* harmony import */ var _map__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(217); +/* harmony import */ var _map__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(218); /** PURE_IMPORTS_START _map PURE_IMPORTS_END */ function pluck() { @@ -28616,14 +28692,14 @@ function plucker(props, length) { /***/ }), -/* 315 */ +/* 316 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publish", function() { return publish; }); -/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(178); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(309); +/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(179); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(310); /** PURE_IMPORTS_START _Subject,_multicast PURE_IMPORTS_END */ @@ -28636,14 +28712,14 @@ function publish(selector) { /***/ }), -/* 316 */ +/* 317 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publishBehavior", function() { return publishBehavior; }); -/* harmony import */ var _BehaviorSubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(183); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(309); +/* harmony import */ var _BehaviorSubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(184); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(310); /** PURE_IMPORTS_START _BehaviorSubject,_multicast PURE_IMPORTS_END */ @@ -28654,14 +28730,14 @@ function publishBehavior(value) { /***/ }), -/* 317 */ +/* 318 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publishLast", function() { return publishLast; }); -/* harmony import */ var _AsyncSubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(201); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(309); +/* harmony import */ var _AsyncSubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(202); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(310); /** PURE_IMPORTS_START _AsyncSubject,_multicast PURE_IMPORTS_END */ @@ -28672,14 +28748,14 @@ function publishLast() { /***/ }), -/* 318 */ +/* 319 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publishReplay", function() { return publishReplay; }); -/* harmony import */ var _ReplaySubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(184); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(309); +/* harmony import */ var _ReplaySubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(185); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(310); /** PURE_IMPORTS_START _ReplaySubject,_multicast PURE_IMPORTS_END */ @@ -28695,14 +28771,14 @@ function publishReplay(bufferSize, windowTime, selectorOrScheduler, scheduler) { /***/ }), -/* 319 */ +/* 320 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "race", function() { return race; }); -/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(168); -/* harmony import */ var _observable_race__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(252); +/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(169); +/* harmony import */ var _observable_race__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(253); /** PURE_IMPORTS_START _util_isArray,_observable_race PURE_IMPORTS_END */ @@ -28722,15 +28798,15 @@ function race() { /***/ }), -/* 320 */ +/* 321 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "repeat", function() { return repeat; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); -/* harmony import */ var _observable_empty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(194); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); +/* harmony import */ var _observable_empty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(195); /** PURE_IMPORTS_START tslib,_Subscriber,_observable_empty PURE_IMPORTS_END */ @@ -28787,18 +28863,18 @@ var RepeatSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 321 */ +/* 322 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "repeatWhen", function() { return repeatWhen; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(178); -/* harmony import */ var _util_tryCatch__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(170); -/* harmony import */ var _util_errorObject__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(171); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(220); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(221); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(179); +/* harmony import */ var _util_tryCatch__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(171); +/* harmony import */ var _util_errorObject__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(172); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(221); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(222); /** PURE_IMPORTS_START tslib,_Subject,_util_tryCatch,_util_errorObject,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ @@ -28883,14 +28959,14 @@ var RepeatWhenSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 322 */ +/* 323 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "retry", function() { return retry; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); /** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ @@ -28936,18 +29012,18 @@ var RetrySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 323 */ +/* 324 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "retryWhen", function() { return retryWhen; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(178); -/* harmony import */ var _util_tryCatch__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(170); -/* harmony import */ var _util_errorObject__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(171); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(220); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(221); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(179); +/* harmony import */ var _util_tryCatch__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(171); +/* harmony import */ var _util_errorObject__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(172); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(221); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(222); /** PURE_IMPORTS_START tslib,_Subject,_util_tryCatch,_util_errorObject,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ @@ -29025,15 +29101,15 @@ var RetryWhenSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 324 */ +/* 325 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "sample", function() { return sample; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(220); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(221); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(221); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(222); /** PURE_IMPORTS_START tslib,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ @@ -29082,15 +29158,15 @@ var SampleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 325 */ +/* 326 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "sampleTime", function() { return sampleTime; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); -/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(206); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); +/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(207); /** PURE_IMPORTS_START tslib,_Subscriber,_scheduler_async PURE_IMPORTS_END */ @@ -29142,7 +29218,7 @@ function dispatchNotification(state) { /***/ }), -/* 326 */ +/* 327 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -29150,10 +29226,10 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "sequenceEqual", function() { return sequenceEqual; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SequenceEqualOperator", function() { return SequenceEqualOperator; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SequenceEqualSubscriber", function() { return SequenceEqualSubscriber; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); -/* harmony import */ var _util_tryCatch__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(170); -/* harmony import */ var _util_errorObject__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(171); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); +/* harmony import */ var _util_tryCatch__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(171); +/* harmony import */ var _util_errorObject__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(172); /** PURE_IMPORTS_START tslib,_Subscriber,_util_tryCatch,_util_errorObject PURE_IMPORTS_END */ @@ -29261,15 +29337,15 @@ var SequenceEqualCompareToSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 327 */ +/* 328 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "share", function() { return share; }); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(309); -/* harmony import */ var _refCount__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(181); -/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(178); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(310); +/* harmony import */ var _refCount__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(182); +/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(179); /** PURE_IMPORTS_START _multicast,_refCount,_Subject PURE_IMPORTS_END */ @@ -29284,13 +29360,13 @@ function share() { /***/ }), -/* 328 */ +/* 329 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "shareReplay", function() { return shareReplay; }); -/* harmony import */ var _ReplaySubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(184); +/* harmony import */ var _ReplaySubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(185); /** PURE_IMPORTS_START _ReplaySubject PURE_IMPORTS_END */ function shareReplay(bufferSize, windowTime, scheduler) { @@ -29333,15 +29409,15 @@ function shareReplayOperator(bufferSize, windowTime, scheduler) { /***/ }), -/* 329 */ +/* 330 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "single", function() { return single; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); -/* harmony import */ var _util_EmptyError__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(214); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); +/* harmony import */ var _util_EmptyError__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(215); /** PURE_IMPORTS_START tslib,_Subscriber,_util_EmptyError PURE_IMPORTS_END */ @@ -29413,14 +29489,14 @@ var SingleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 330 */ +/* 331 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "skip", function() { return skip; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); /** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ @@ -29455,15 +29531,15 @@ var SkipSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 331 */ +/* 332 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "skipLast", function() { return skipLast; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); -/* harmony import */ var _util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(213); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); +/* harmony import */ var _util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(214); /** PURE_IMPORTS_START tslib,_Subscriber,_util_ArgumentOutOfRangeError PURE_IMPORTS_END */ @@ -29517,15 +29593,15 @@ var SkipLastSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 332 */ +/* 333 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "skipUntil", function() { return skipUntil; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(220); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(221); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(221); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(222); /** PURE_IMPORTS_START tslib,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ @@ -29569,14 +29645,14 @@ var SkipUntilSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 333 */ +/* 334 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "skipWhile", function() { return skipWhile; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); /** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ @@ -29625,17 +29701,17 @@ var SkipWhileSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 334 */ +/* 335 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "startWith", function() { return startWith; }); -/* harmony import */ var _observable_fromArray__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(197); -/* harmony import */ var _observable_scalar__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(199); -/* harmony import */ var _observable_empty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(194); -/* harmony import */ var _observable_concat__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(230); -/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(196); +/* harmony import */ var _observable_fromArray__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(198); +/* harmony import */ var _observable_scalar__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(200); +/* harmony import */ var _observable_empty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(195); +/* harmony import */ var _observable_concat__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(231); +/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(197); /** PURE_IMPORTS_START _observable_fromArray,_observable_scalar,_observable_empty,_observable_concat,_util_isScheduler PURE_IMPORTS_END */ @@ -29671,13 +29747,13 @@ function startWith() { /***/ }), -/* 335 */ +/* 336 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "subscribeOn", function() { return subscribeOn; }); -/* harmony import */ var _observable_SubscribeOnObservable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(336); +/* harmony import */ var _observable_SubscribeOnObservable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(337); /** PURE_IMPORTS_START _observable_SubscribeOnObservable PURE_IMPORTS_END */ function subscribeOn(scheduler, delay) { @@ -29702,16 +29778,16 @@ var SubscribeOnOperator = /*@__PURE__*/ (function () { /***/ }), -/* 336 */ +/* 337 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SubscribeOnObservable", function() { return SubscribeOnObservable; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(159); -/* harmony import */ var _scheduler_asap__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(202); -/* harmony import */ var _util_isNumeric__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(247); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(160); +/* harmony import */ var _scheduler_asap__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(203); +/* harmony import */ var _util_isNumeric__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(248); /** PURE_IMPORTS_START tslib,_Observable,_scheduler_asap,_util_isNumeric PURE_IMPORTS_END */ @@ -29766,14 +29842,14 @@ var SubscribeOnObservable = /*@__PURE__*/ (function (_super) { /***/ }), -/* 337 */ +/* 338 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "switchAll", function() { return switchAll; }); -/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(338); -/* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(211); +/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(339); +/* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(212); /** PURE_IMPORTS_START _switchMap,_util_identity PURE_IMPORTS_END */ @@ -29784,17 +29860,17 @@ function switchAll() { /***/ }), -/* 338 */ +/* 339 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "switchMap", function() { return switchMap; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(220); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(221); -/* harmony import */ var _map__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(217); -/* harmony import */ var _observable_from__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(231); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(221); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(222); +/* harmony import */ var _map__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(218); +/* harmony import */ var _observable_from__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(232); /** PURE_IMPORTS_START tslib,_OuterSubscriber,_util_subscribeToResult,_map,_observable_from PURE_IMPORTS_END */ @@ -29868,13 +29944,13 @@ var SwitchMapSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 339 */ +/* 340 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "switchMapTo", function() { return switchMapTo; }); -/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(338); +/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(339); /** PURE_IMPORTS_START _switchMap PURE_IMPORTS_END */ function switchMapTo(innerObservable, resultSelector) { @@ -29884,15 +29960,15 @@ function switchMapTo(innerObservable, resultSelector) { /***/ }), -/* 340 */ +/* 341 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "takeUntil", function() { return takeUntil; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(220); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(221); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(221); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(222); /** PURE_IMPORTS_START tslib,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ @@ -29931,14 +30007,14 @@ var TakeUntilSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 341 */ +/* 342 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "takeWhile", function() { return takeWhile; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); /** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ @@ -29989,16 +30065,16 @@ var TakeWhileSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 342 */ +/* 343 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "defaultThrottleConfig", function() { return defaultThrottleConfig; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "throttle", function() { return throttle; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(220); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(221); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(221); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(222); /** PURE_IMPORTS_START tslib,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ @@ -30093,16 +30169,16 @@ var ThrottleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 343 */ +/* 344 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "throttleTime", function() { return throttleTime; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); -/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(206); -/* harmony import */ var _throttle__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(342); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); +/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(207); +/* harmony import */ var _throttle__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(343); /** PURE_IMPORTS_START tslib,_Subscriber,_scheduler_async,_throttle PURE_IMPORTS_END */ @@ -30187,17 +30263,17 @@ function dispatchNext(arg) { /***/ }), -/* 344 */ +/* 345 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timeInterval", function() { return timeInterval; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TimeInterval", function() { return TimeInterval; }); -/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(206); -/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(304); -/* harmony import */ var _observable_defer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(240); -/* harmony import */ var _map__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(217); +/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(207); +/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(305); +/* harmony import */ var _observable_defer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(241); +/* harmony import */ var _map__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(218); /** PURE_IMPORTS_START _scheduler_async,_scan,_observable_defer,_map PURE_IMPORTS_END */ @@ -30231,16 +30307,16 @@ var TimeInterval = /*@__PURE__*/ (function () { /***/ }), -/* 345 */ +/* 346 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timeout", function() { return timeout; }); -/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(206); -/* harmony import */ var _util_TimeoutError__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(215); -/* harmony import */ var _timeoutWith__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(346); -/* harmony import */ var _observable_throwError__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(200); +/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(207); +/* harmony import */ var _util_TimeoutError__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(216); +/* harmony import */ var _timeoutWith__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(347); +/* harmony import */ var _observable_throwError__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(201); /** PURE_IMPORTS_START _scheduler_async,_util_TimeoutError,_timeoutWith,_observable_throwError PURE_IMPORTS_END */ @@ -30256,17 +30332,17 @@ function timeout(due, scheduler) { /***/ }), -/* 346 */ +/* 347 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timeoutWith", function() { return timeoutWith; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(206); -/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(276); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(220); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(221); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(207); +/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(277); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(221); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(222); /** PURE_IMPORTS_START tslib,_scheduler_async,_util_isDate,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ @@ -30338,15 +30414,15 @@ var TimeoutWithSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 347 */ +/* 348 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timestamp", function() { return timestamp; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Timestamp", function() { return Timestamp; }); -/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(206); -/* harmony import */ var _map__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(217); +/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(207); +/* harmony import */ var _map__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(218); /** PURE_IMPORTS_START _scheduler_async,_map PURE_IMPORTS_END */ @@ -30368,13 +30444,13 @@ var Timestamp = /*@__PURE__*/ (function () { /***/ }), -/* 348 */ +/* 349 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "toArray", function() { return toArray; }); -/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(303); +/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(304); /** PURE_IMPORTS_START _reduce PURE_IMPORTS_END */ function toArrayReducer(arr, item, index) { @@ -30391,16 +30467,16 @@ function toArray() { /***/ }), -/* 349 */ +/* 350 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "window", function() { return window; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(178); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(220); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(221); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(179); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(221); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(222); /** PURE_IMPORTS_START tslib,_Subject,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ @@ -30471,15 +30547,15 @@ var WindowSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 350 */ +/* 351 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "windowCount", function() { return windowCount; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(161); -/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(178); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(162); +/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(179); /** PURE_IMPORTS_START tslib,_Subscriber,_Subject PURE_IMPORTS_END */ @@ -30561,18 +30637,18 @@ var WindowCountSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 351 */ +/* 352 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "windowTime", function() { return windowTime; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(178); -/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(206); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(161); -/* harmony import */ var _util_isNumeric__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(247); -/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(196); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(179); +/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(207); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(162); +/* harmony import */ var _util_isNumeric__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(248); +/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(197); /** PURE_IMPORTS_START tslib,_Subject,_scheduler_async,_Subscriber,_util_isNumeric,_util_isScheduler PURE_IMPORTS_END */ @@ -30731,19 +30807,19 @@ function dispatchWindowClose(state) { /***/ }), -/* 352 */ +/* 353 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "windowToggle", function() { return windowToggle; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(178); -/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(167); -/* harmony import */ var _util_tryCatch__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(170); -/* harmony import */ var _util_errorObject__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(171); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(220); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(221); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(179); +/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(168); +/* harmony import */ var _util_tryCatch__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(171); +/* harmony import */ var _util_errorObject__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(172); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(221); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(222); /** PURE_IMPORTS_START tslib,_Subject,_Subscription,_util_tryCatch,_util_errorObject,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ @@ -30877,18 +30953,18 @@ var WindowToggleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 353 */ +/* 354 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "windowWhen", function() { return windowWhen; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(178); -/* harmony import */ var _util_tryCatch__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(170); -/* harmony import */ var _util_errorObject__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(171); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(220); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(221); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(179); +/* harmony import */ var _util_tryCatch__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(171); +/* harmony import */ var _util_errorObject__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(172); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(221); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(222); /** PURE_IMPORTS_START tslib,_Subject,_util_tryCatch,_util_errorObject,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ @@ -30976,15 +31052,15 @@ var WindowSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 354 */ +/* 355 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "withLatestFrom", function() { return withLatestFrom; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(162); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(220); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(221); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(221); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(222); /** PURE_IMPORTS_START tslib,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ @@ -31071,13 +31147,13 @@ var WithLatestFromSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 355 */ +/* 356 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "zip", function() { return zip; }); -/* harmony import */ var _observable_zip__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(256); +/* harmony import */ var _observable_zip__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(257); /** PURE_IMPORTS_START _observable_zip PURE_IMPORTS_END */ function zip() { @@ -31093,13 +31169,13 @@ function zip() { /***/ }), -/* 356 */ +/* 357 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "zipAll", function() { return zipAll; }); -/* harmony import */ var _observable_zip__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(256); +/* harmony import */ var _observable_zip__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(257); /** PURE_IMPORTS_START _observable_zip PURE_IMPORTS_END */ function zipAll(project) { @@ -31109,7 +31185,7 @@ function zipAll(project) { /***/ }), -/* 357 */ +/* 358 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -31117,15 +31193,15 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "runCommand", function() { return runCommand; }); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(chalk__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var indent_string__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(358); +/* harmony import */ var indent_string__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(359); /* harmony import */ var indent_string__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(indent_string__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var wrap_ansi__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(359); +/* harmony import */ var wrap_ansi__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(360); /* harmony import */ var wrap_ansi__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(wrap_ansi__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(132); /* harmony import */ var _utils_errors__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(52); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(33); /* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(35); -/* harmony import */ var _utils_projects_tree__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(366); +/* harmony import */ var _utils_projects_tree__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(367); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -31203,7 +31279,7 @@ function toArray(value) { } /***/ }), -/* 358 */ +/* 359 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -31237,13 +31313,13 @@ module.exports = (str, count, opts) => { /***/ }), -/* 359 */ +/* 360 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const stringWidth = __webpack_require__(360); -const stripAnsi = __webpack_require__(364); +const stringWidth = __webpack_require__(361); +const stripAnsi = __webpack_require__(365); const ESCAPES = new Set([ '\u001B', @@ -31437,13 +31513,13 @@ module.exports = (str, cols, opts) => { /***/ }), -/* 360 */ +/* 361 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const stripAnsi = __webpack_require__(361); -const isFullwidthCodePoint = __webpack_require__(363); +const stripAnsi = __webpack_require__(362); +const isFullwidthCodePoint = __webpack_require__(364); module.exports = str => { if (typeof str !== 'string' || str.length === 0) { @@ -31480,18 +31556,18 @@ module.exports = str => { /***/ }), -/* 361 */ +/* 362 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const ansiRegex = __webpack_require__(362); +const ansiRegex = __webpack_require__(363); module.exports = input => typeof input === 'string' ? input.replace(ansiRegex(), '') : input; /***/ }), -/* 362 */ +/* 363 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -31508,7 +31584,7 @@ module.exports = () => { /***/ }), -/* 363 */ +/* 364 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -31561,18 +31637,18 @@ module.exports = x => { /***/ }), -/* 364 */ +/* 365 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const ansiRegex = __webpack_require__(365); +const ansiRegex = __webpack_require__(366); module.exports = input => typeof input === 'string' ? input.replace(ansiRegex(), '') : input; /***/ }), -/* 365 */ +/* 366 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -31589,7 +31665,7 @@ module.exports = () => { /***/ }), -/* 366 */ +/* 367 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -31742,15 +31818,15 @@ function addProjectToTree(tree, pathParts, project) { } /***/ }), -/* 367 */ +/* 368 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony import */ var _build_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(368); +/* harmony import */ var _build_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(369); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _build_production_projects__WEBPACK_IMPORTED_MODULE_0__["buildProductionProjects"]; }); -/* harmony import */ var _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(547); +/* harmony import */ var _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(548); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__["prepareExternalProjectDependencies"]; }); /* @@ -31775,13 +31851,13 @@ __webpack_require__.r(__webpack_exports__); /***/ }), -/* 368 */ +/* 369 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return buildProductionProjects; }); -/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(369); +/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(370); /* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(cpy__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(134); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(del__WEBPACK_IMPORTED_MODULE_1__); @@ -31921,17 +31997,17 @@ async function copyToBuild(project, kibanaRoot, buildRoot) { } /***/ }), -/* 369 */ +/* 370 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const EventEmitter = __webpack_require__(45); const path = __webpack_require__(16); -const arrify = __webpack_require__(370); -const globby = __webpack_require__(371); -const cpFile = __webpack_require__(539); -const CpyError = __webpack_require__(546); +const arrify = __webpack_require__(371); +const globby = __webpack_require__(372); +const cpFile = __webpack_require__(540); +const CpyError = __webpack_require__(547); const preprocessSrcPath = (srcPath, options) => options.cwd ? path.resolve(options.cwd, srcPath) : srcPath; @@ -32030,7 +32106,7 @@ module.exports = (src, dest, options = {}) => { /***/ }), -/* 370 */ +/* 371 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -32045,16 +32121,16 @@ module.exports = function (val) { /***/ }), -/* 371 */ +/* 372 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const arrayUnion = __webpack_require__(138); const glob = __webpack_require__(36); -const fastGlob = __webpack_require__(372); -const dirGlob = __webpack_require__(535); -const gitignore = __webpack_require__(536); +const fastGlob = __webpack_require__(373); +const dirGlob = __webpack_require__(536); +const gitignore = __webpack_require__(537); const DEFAULT_FILTER = () => false; @@ -32180,10 +32256,10 @@ module.exports.gitignore = gitignore; /***/ }), -/* 372 */ +/* 373 */ /***/ (function(module, exports, __webpack_require__) { -const pkg = __webpack_require__(373); +const pkg = __webpack_require__(374); module.exports = pkg.async; module.exports.default = pkg.async; @@ -32194,19 +32270,19 @@ module.exports.stream = pkg.stream; /***/ }), -/* 373 */ +/* 374 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var merge2 = __webpack_require__(374); -var optionsManager = __webpack_require__(375); -var taskManager = __webpack_require__(376); -var reader_async_1 = __webpack_require__(514); -var reader_stream_1 = __webpack_require__(533); -var reader_sync_1 = __webpack_require__(534); -var arrayUtils = __webpack_require__(530); +var merge2 = __webpack_require__(375); +var optionsManager = __webpack_require__(376); +var taskManager = __webpack_require__(377); +var reader_async_1 = __webpack_require__(515); +var reader_stream_1 = __webpack_require__(534); +var reader_sync_1 = __webpack_require__(535); +var arrayUtils = __webpack_require__(531); /** * Returns a set of works based on provided tasks and class of the reader. */ @@ -32244,7 +32320,7 @@ exports.stream = stream; /***/ }), -/* 374 */ +/* 375 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -32358,7 +32434,7 @@ function pauseStreams (streams, options) { /***/ }), -/* 375 */ +/* 376 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -32393,14 +32469,14 @@ exports.prepare = prepare; /***/ }), -/* 376 */ +/* 377 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var objectUtils = __webpack_require__(377); -var patternUtils = __webpack_require__(378); +var objectUtils = __webpack_require__(378); +var patternUtils = __webpack_require__(379); /** * Returns grouped patterns by base directory of each pattern. */ @@ -32512,7 +32588,7 @@ exports.generate = generate; /***/ }), -/* 377 */ +/* 378 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -32525,7 +32601,7 @@ exports.values = values; /***/ }), -/* 378 */ +/* 379 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -32541,8 +32617,8 @@ var __values = (this && this.__values) || function (o) { }; }; Object.defineProperty(exports, "__esModule", { value: true }); -var globParent = __webpack_require__(379); -var micromatch = __webpack_require__(383); +var globParent = __webpack_require__(380); +var micromatch = __webpack_require__(384); var GLOBSTAR = '**'; /** * Convert a windows «path» to a unix-style «path». @@ -32675,15 +32751,15 @@ exports.match = match; /***/ }), -/* 379 */ +/* 380 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var path = __webpack_require__(16); -var isglob = __webpack_require__(380); -var pathDirname = __webpack_require__(382); +var isglob = __webpack_require__(381); +var pathDirname = __webpack_require__(383); var isWin32 = __webpack_require__(11).platform() === 'win32'; module.exports = function globParent(str) { @@ -32706,7 +32782,7 @@ module.exports = function globParent(str) { /***/ }), -/* 380 */ +/* 381 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -32716,7 +32792,7 @@ module.exports = function globParent(str) { * Licensed under the MIT License. */ -var isExtglob = __webpack_require__(381); +var isExtglob = __webpack_require__(382); module.exports = function isGlob(str) { if (typeof str !== 'string' || str === '') { @@ -32737,7 +32813,7 @@ module.exports = function isGlob(str) { /***/ }), -/* 381 */ +/* 382 */ /***/ (function(module, exports) { /*! @@ -32763,7 +32839,7 @@ module.exports = function isExtglob(str) { /***/ }), -/* 382 */ +/* 383 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -32913,7 +32989,7 @@ module.exports.win32 = win32; /***/ }), -/* 383 */ +/* 384 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -32924,18 +33000,18 @@ module.exports.win32 = win32; */ var util = __webpack_require__(29); -var braces = __webpack_require__(384); -var toRegex = __webpack_require__(385); -var extend = __webpack_require__(394); +var braces = __webpack_require__(385); +var toRegex = __webpack_require__(386); +var extend = __webpack_require__(395); /** * Local dependencies */ -var compilers = __webpack_require__(488); -var parsers = __webpack_require__(510); -var cache = __webpack_require__(511); -var utils = __webpack_require__(512); +var compilers = __webpack_require__(489); +var parsers = __webpack_require__(511); +var cache = __webpack_require__(512); +var utils = __webpack_require__(513); var MAX_LENGTH = 1024 * 64; /** @@ -33799,7 +33875,7 @@ module.exports = micromatch; /***/ }), -/* 384 */ +/* 385 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -33809,18 +33885,18 @@ module.exports = micromatch; * Module dependencies */ -var toRegex = __webpack_require__(385); -var unique = __webpack_require__(397); -var extend = __webpack_require__(394); +var toRegex = __webpack_require__(386); +var unique = __webpack_require__(398); +var extend = __webpack_require__(395); /** * Local dependencies */ -var compilers = __webpack_require__(398); -var parsers = __webpack_require__(413); -var Braces = __webpack_require__(423); -var utils = __webpack_require__(399); +var compilers = __webpack_require__(399); +var parsers = __webpack_require__(414); +var Braces = __webpack_require__(424); +var utils = __webpack_require__(400); var MAX_LENGTH = 1024 * 64; var cache = {}; @@ -34124,15 +34200,15 @@ module.exports = braces; /***/ }), -/* 385 */ +/* 386 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(386); -var extend = __webpack_require__(394); -var not = __webpack_require__(396); +var define = __webpack_require__(387); +var extend = __webpack_require__(395); +var not = __webpack_require__(397); var MAX_LENGTH = 1024 * 64; /** @@ -34279,7 +34355,7 @@ module.exports.makeRe = makeRe; /***/ }), -/* 386 */ +/* 387 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -34292,7 +34368,7 @@ module.exports.makeRe = makeRe; -var isDescriptor = __webpack_require__(387); +var isDescriptor = __webpack_require__(388); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -34317,7 +34393,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 387 */ +/* 388 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -34330,9 +34406,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(388); -var isAccessor = __webpack_require__(389); -var isData = __webpack_require__(392); +var typeOf = __webpack_require__(389); +var isAccessor = __webpack_require__(390); +var isData = __webpack_require__(393); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -34346,7 +34422,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 388 */ +/* 389 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -34499,7 +34575,7 @@ function isBuffer(val) { /***/ }), -/* 389 */ +/* 390 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -34512,7 +34588,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(390); +var typeOf = __webpack_require__(391); // accessor descriptor properties var accessor = { @@ -34575,10 +34651,10 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 390 */ +/* 391 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(391); +var isBuffer = __webpack_require__(392); var toString = Object.prototype.toString; /** @@ -34697,7 +34773,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 391 */ +/* 392 */ /***/ (function(module, exports) { /*! @@ -34724,7 +34800,7 @@ function isSlowBuffer (obj) { /***/ }), -/* 392 */ +/* 393 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -34737,7 +34813,7 @@ function isSlowBuffer (obj) { -var typeOf = __webpack_require__(393); +var typeOf = __webpack_require__(394); // data descriptor properties var data = { @@ -34786,10 +34862,10 @@ module.exports = isDataDescriptor; /***/ }), -/* 393 */ +/* 394 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(391); +var isBuffer = __webpack_require__(392); var toString = Object.prototype.toString; /** @@ -34908,13 +34984,13 @@ module.exports = function kindOf(val) { /***/ }), -/* 394 */ +/* 395 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(395); +var isObject = __webpack_require__(396); module.exports = function extend(o/*, objects*/) { if (!isObject(o)) { o = {}; } @@ -34948,7 +35024,7 @@ function hasOwn(obj, key) { /***/ }), -/* 395 */ +/* 396 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -34968,13 +35044,13 @@ module.exports = function isExtendable(val) { /***/ }), -/* 396 */ +/* 397 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(394); +var extend = __webpack_require__(395); /** * The main export is a function that takes a `pattern` string and an `options` object. @@ -35041,7 +35117,7 @@ module.exports = toRegex; /***/ }), -/* 397 */ +/* 398 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -35091,13 +35167,13 @@ module.exports.immutable = function uniqueImmutable(arr) { /***/ }), -/* 398 */ +/* 399 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(399); +var utils = __webpack_require__(400); module.exports = function(braces, options) { braces.compiler @@ -35380,25 +35456,25 @@ function hasQueue(node) { /***/ }), -/* 399 */ +/* 400 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var splitString = __webpack_require__(400); +var splitString = __webpack_require__(401); var utils = module.exports; /** * Module dependencies */ -utils.extend = __webpack_require__(394); -utils.flatten = __webpack_require__(406); -utils.isObject = __webpack_require__(404); -utils.fillRange = __webpack_require__(407); -utils.repeat = __webpack_require__(412); -utils.unique = __webpack_require__(397); +utils.extend = __webpack_require__(395); +utils.flatten = __webpack_require__(407); +utils.isObject = __webpack_require__(405); +utils.fillRange = __webpack_require__(408); +utils.repeat = __webpack_require__(413); +utils.unique = __webpack_require__(398); utils.define = function(obj, key, val) { Object.defineProperty(obj, key, { @@ -35730,7 +35806,7 @@ utils.escapeRegex = function(str) { /***/ }), -/* 400 */ +/* 401 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -35743,7 +35819,7 @@ utils.escapeRegex = function(str) { -var extend = __webpack_require__(401); +var extend = __webpack_require__(402); module.exports = function(str, options, fn) { if (typeof str !== 'string') { @@ -35908,14 +35984,14 @@ function keepEscaping(opts, str, idx) { /***/ }), -/* 401 */ +/* 402 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(402); -var assignSymbols = __webpack_require__(405); +var isExtendable = __webpack_require__(403); +var assignSymbols = __webpack_require__(406); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -35975,7 +36051,7 @@ function isEnum(obj, key) { /***/ }), -/* 402 */ +/* 403 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -35988,7 +36064,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(403); +var isPlainObject = __webpack_require__(404); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -35996,7 +36072,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 403 */ +/* 404 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36009,7 +36085,7 @@ module.exports = function isExtendable(val) { -var isObject = __webpack_require__(404); +var isObject = __webpack_require__(405); function isObjectObject(o) { return isObject(o) === true @@ -36040,7 +36116,7 @@ module.exports = function isPlainObject(o) { /***/ }), -/* 404 */ +/* 405 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36059,7 +36135,7 @@ module.exports = function isObject(val) { /***/ }), -/* 405 */ +/* 406 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36106,7 +36182,7 @@ module.exports = function(receiver, objects) { /***/ }), -/* 406 */ +/* 407 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36135,7 +36211,7 @@ function flat(arr, res) { /***/ }), -/* 407 */ +/* 408 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36149,10 +36225,10 @@ function flat(arr, res) { var util = __webpack_require__(29); -var isNumber = __webpack_require__(408); -var extend = __webpack_require__(394); -var repeat = __webpack_require__(410); -var toRegex = __webpack_require__(411); +var isNumber = __webpack_require__(409); +var extend = __webpack_require__(395); +var repeat = __webpack_require__(411); +var toRegex = __webpack_require__(412); /** * Return a range of numbers or letters. @@ -36350,7 +36426,7 @@ module.exports = fillRange; /***/ }), -/* 408 */ +/* 409 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36363,7 +36439,7 @@ module.exports = fillRange; -var typeOf = __webpack_require__(409); +var typeOf = __webpack_require__(410); module.exports = function isNumber(num) { var type = typeOf(num); @@ -36379,10 +36455,10 @@ module.exports = function isNumber(num) { /***/ }), -/* 409 */ +/* 410 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(391); +var isBuffer = __webpack_require__(392); var toString = Object.prototype.toString; /** @@ -36501,7 +36577,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 410 */ +/* 411 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36578,7 +36654,7 @@ function repeat(str, num) { /***/ }), -/* 411 */ +/* 412 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36591,8 +36667,8 @@ function repeat(str, num) { -var repeat = __webpack_require__(410); -var isNumber = __webpack_require__(408); +var repeat = __webpack_require__(411); +var isNumber = __webpack_require__(409); var cache = {}; function toRegexRange(min, max, options) { @@ -36879,7 +36955,7 @@ module.exports = toRegexRange; /***/ }), -/* 412 */ +/* 413 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36904,14 +36980,14 @@ module.exports = function repeat(ele, num) { /***/ }), -/* 413 */ +/* 414 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Node = __webpack_require__(414); -var utils = __webpack_require__(399); +var Node = __webpack_require__(415); +var utils = __webpack_require__(400); /** * Braces parsers @@ -37271,15 +37347,15 @@ function concatNodes(pos, node, parent, options) { /***/ }), -/* 414 */ +/* 415 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(404); -var define = __webpack_require__(415); -var utils = __webpack_require__(422); +var isObject = __webpack_require__(405); +var define = __webpack_require__(416); +var utils = __webpack_require__(423); var ownNames; /** @@ -37770,7 +37846,7 @@ exports = module.exports = Node; /***/ }), -/* 415 */ +/* 416 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -37783,7 +37859,7 @@ exports = module.exports = Node; -var isDescriptor = __webpack_require__(416); +var isDescriptor = __webpack_require__(417); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -37808,7 +37884,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 416 */ +/* 417 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -37821,9 +37897,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(417); -var isAccessor = __webpack_require__(418); -var isData = __webpack_require__(420); +var typeOf = __webpack_require__(418); +var isAccessor = __webpack_require__(419); +var isData = __webpack_require__(421); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -37837,7 +37913,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 417 */ +/* 418 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -37972,7 +38048,7 @@ function isBuffer(val) { /***/ }), -/* 418 */ +/* 419 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -37985,7 +38061,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(419); +var typeOf = __webpack_require__(420); // accessor descriptor properties var accessor = { @@ -38048,7 +38124,7 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 419 */ +/* 420 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -38183,7 +38259,7 @@ function isBuffer(val) { /***/ }), -/* 420 */ +/* 421 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38196,7 +38272,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(421); +var typeOf = __webpack_require__(422); module.exports = function isDataDescriptor(obj, prop) { // data descriptor properties @@ -38239,7 +38315,7 @@ module.exports = function isDataDescriptor(obj, prop) { /***/ }), -/* 421 */ +/* 422 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -38374,13 +38450,13 @@ function isBuffer(val) { /***/ }), -/* 422 */ +/* 423 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(409); +var typeOf = __webpack_require__(410); var utils = module.exports; /** @@ -39400,17 +39476,17 @@ function assert(val, message) { /***/ }), -/* 423 */ +/* 424 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(394); -var Snapdragon = __webpack_require__(424); -var compilers = __webpack_require__(398); -var parsers = __webpack_require__(413); -var utils = __webpack_require__(399); +var extend = __webpack_require__(395); +var Snapdragon = __webpack_require__(425); +var compilers = __webpack_require__(399); +var parsers = __webpack_require__(414); +var utils = __webpack_require__(400); /** * Customize Snapdragon parser and renderer @@ -39511,17 +39587,17 @@ module.exports = Braces; /***/ }), -/* 424 */ +/* 425 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Base = __webpack_require__(425); -var define = __webpack_require__(386); -var Compiler = __webpack_require__(455); -var Parser = __webpack_require__(485); -var utils = __webpack_require__(465); +var Base = __webpack_require__(426); +var define = __webpack_require__(387); +var Compiler = __webpack_require__(456); +var Parser = __webpack_require__(486); +var utils = __webpack_require__(466); var regexCache = {}; var cache = {}; @@ -39692,20 +39768,20 @@ module.exports.Parser = Parser; /***/ }), -/* 425 */ +/* 426 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var define = __webpack_require__(426); -var CacheBase = __webpack_require__(427); -var Emitter = __webpack_require__(428); -var isObject = __webpack_require__(404); -var merge = __webpack_require__(446); -var pascal = __webpack_require__(449); -var cu = __webpack_require__(450); +var define = __webpack_require__(427); +var CacheBase = __webpack_require__(428); +var Emitter = __webpack_require__(429); +var isObject = __webpack_require__(405); +var merge = __webpack_require__(447); +var pascal = __webpack_require__(450); +var cu = __webpack_require__(451); /** * Optionally define a custom `cache` namespace to use. @@ -40134,7 +40210,7 @@ module.exports.namespace = namespace; /***/ }), -/* 426 */ +/* 427 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40147,7 +40223,7 @@ module.exports.namespace = namespace; -var isDescriptor = __webpack_require__(416); +var isDescriptor = __webpack_require__(417); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -40172,21 +40248,21 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 427 */ +/* 428 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(404); -var Emitter = __webpack_require__(428); -var visit = __webpack_require__(429); -var toPath = __webpack_require__(432); -var union = __webpack_require__(433); -var del = __webpack_require__(437); -var get = __webpack_require__(435); -var has = __webpack_require__(442); -var set = __webpack_require__(445); +var isObject = __webpack_require__(405); +var Emitter = __webpack_require__(429); +var visit = __webpack_require__(430); +var toPath = __webpack_require__(433); +var union = __webpack_require__(434); +var del = __webpack_require__(438); +var get = __webpack_require__(436); +var has = __webpack_require__(443); +var set = __webpack_require__(446); /** * Create a `Cache` constructor that when instantiated will @@ -40440,7 +40516,7 @@ module.exports.namespace = namespace; /***/ }), -/* 428 */ +/* 429 */ /***/ (function(module, exports, __webpack_require__) { @@ -40609,7 +40685,7 @@ Emitter.prototype.hasListeners = function(event){ /***/ }), -/* 429 */ +/* 430 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40622,8 +40698,8 @@ Emitter.prototype.hasListeners = function(event){ -var visit = __webpack_require__(430); -var mapVisit = __webpack_require__(431); +var visit = __webpack_require__(431); +var mapVisit = __webpack_require__(432); module.exports = function(collection, method, val) { var result; @@ -40646,7 +40722,7 @@ module.exports = function(collection, method, val) { /***/ }), -/* 430 */ +/* 431 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40659,7 +40735,7 @@ module.exports = function(collection, method, val) { -var isObject = __webpack_require__(404); +var isObject = __webpack_require__(405); module.exports = function visit(thisArg, method, target, val) { if (!isObject(thisArg) && typeof thisArg !== 'function') { @@ -40686,14 +40762,14 @@ module.exports = function visit(thisArg, method, target, val) { /***/ }), -/* 431 */ +/* 432 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var visit = __webpack_require__(430); +var visit = __webpack_require__(431); /** * Map `visit` over an array of objects. @@ -40730,7 +40806,7 @@ function isObject(val) { /***/ }), -/* 432 */ +/* 433 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40743,7 +40819,7 @@ function isObject(val) { -var typeOf = __webpack_require__(409); +var typeOf = __webpack_require__(410); module.exports = function toPath(args) { if (typeOf(args) !== 'arguments') { @@ -40770,16 +40846,16 @@ function filter(arr) { /***/ }), -/* 433 */ +/* 434 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(395); -var union = __webpack_require__(434); -var get = __webpack_require__(435); -var set = __webpack_require__(436); +var isObject = __webpack_require__(396); +var union = __webpack_require__(435); +var get = __webpack_require__(436); +var set = __webpack_require__(437); module.exports = function unionValue(obj, prop, value) { if (!isObject(obj)) { @@ -40807,7 +40883,7 @@ function arrayify(val) { /***/ }), -/* 434 */ +/* 435 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40843,7 +40919,7 @@ module.exports = function union(init) { /***/ }), -/* 435 */ +/* 436 */ /***/ (function(module, exports) { /*! @@ -40899,7 +40975,7 @@ function toString(val) { /***/ }), -/* 436 */ +/* 437 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40912,10 +40988,10 @@ function toString(val) { -var toPath = __webpack_require__(432); -var extend = __webpack_require__(394); -var isPlainObject = __webpack_require__(403); -var isObject = __webpack_require__(395); +var toPath = __webpack_require__(433); +var extend = __webpack_require__(395); +var isPlainObject = __webpack_require__(404); +var isObject = __webpack_require__(396); module.exports = function(obj, path, val) { if (!isObject(obj)) { @@ -40969,7 +41045,7 @@ module.exports = function(obj, path, val) { /***/ }), -/* 437 */ +/* 438 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40982,8 +41058,8 @@ module.exports = function(obj, path, val) { -var isObject = __webpack_require__(404); -var has = __webpack_require__(438); +var isObject = __webpack_require__(405); +var has = __webpack_require__(439); module.exports = function unset(obj, prop) { if (!isObject(obj)) { @@ -41008,7 +41084,7 @@ module.exports = function unset(obj, prop) { /***/ }), -/* 438 */ +/* 439 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -41021,9 +41097,9 @@ module.exports = function unset(obj, prop) { -var isObject = __webpack_require__(439); -var hasValues = __webpack_require__(441); -var get = __webpack_require__(435); +var isObject = __webpack_require__(440); +var hasValues = __webpack_require__(442); +var get = __webpack_require__(436); module.exports = function(obj, prop, noZero) { if (isObject(obj)) { @@ -41034,7 +41110,7 @@ module.exports = function(obj, prop, noZero) { /***/ }), -/* 439 */ +/* 440 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -41047,7 +41123,7 @@ module.exports = function(obj, prop, noZero) { -var isArray = __webpack_require__(440); +var isArray = __webpack_require__(441); module.exports = function isObject(val) { return val != null && typeof val === 'object' && isArray(val) === false; @@ -41055,7 +41131,7 @@ module.exports = function isObject(val) { /***/ }), -/* 440 */ +/* 441 */ /***/ (function(module, exports) { var toString = {}.toString; @@ -41066,7 +41142,7 @@ module.exports = Array.isArray || function (arr) { /***/ }), -/* 441 */ +/* 442 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -41109,7 +41185,7 @@ module.exports = function hasValue(o, noZero) { /***/ }), -/* 442 */ +/* 443 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -41122,9 +41198,9 @@ module.exports = function hasValue(o, noZero) { -var isObject = __webpack_require__(404); -var hasValues = __webpack_require__(443); -var get = __webpack_require__(435); +var isObject = __webpack_require__(405); +var hasValues = __webpack_require__(444); +var get = __webpack_require__(436); module.exports = function(val, prop) { return hasValues(isObject(val) && prop ? get(val, prop) : val); @@ -41132,7 +41208,7 @@ module.exports = function(val, prop) { /***/ }), -/* 443 */ +/* 444 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -41145,8 +41221,8 @@ module.exports = function(val, prop) { -var typeOf = __webpack_require__(444); -var isNumber = __webpack_require__(408); +var typeOf = __webpack_require__(445); +var isNumber = __webpack_require__(409); module.exports = function hasValue(val) { // is-number checks for NaN and other edge cases @@ -41199,10 +41275,10 @@ module.exports = function hasValue(val) { /***/ }), -/* 444 */ +/* 445 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(391); +var isBuffer = __webpack_require__(392); var toString = Object.prototype.toString; /** @@ -41324,7 +41400,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 445 */ +/* 446 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -41337,10 +41413,10 @@ module.exports = function kindOf(val) { -var split = __webpack_require__(400); -var extend = __webpack_require__(394); -var isPlainObject = __webpack_require__(403); -var isObject = __webpack_require__(395); +var split = __webpack_require__(401); +var extend = __webpack_require__(395); +var isPlainObject = __webpack_require__(404); +var isObject = __webpack_require__(396); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -41382,14 +41458,14 @@ module.exports = function(obj, prop, val) { /***/ }), -/* 446 */ +/* 447 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(447); -var forIn = __webpack_require__(448); +var isExtendable = __webpack_require__(448); +var forIn = __webpack_require__(449); function mixinDeep(target, objects) { var len = arguments.length, i = 0; @@ -41411,6 +41487,10 @@ function mixinDeep(target, objects) { */ function copy(val, key) { + if (key === '__proto__') { + return; + } + var obj = this[key]; if (isObject(val) && isObject(obj)) { mixinDeep(obj, val); @@ -41438,7 +41518,7 @@ module.exports = mixinDeep; /***/ }), -/* 447 */ +/* 448 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -41451,7 +41531,7 @@ module.exports = mixinDeep; -var isPlainObject = __webpack_require__(403); +var isPlainObject = __webpack_require__(404); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -41459,7 +41539,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 448 */ +/* 449 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -41482,7 +41562,7 @@ module.exports = function forIn(obj, fn, thisArg) { /***/ }), -/* 449 */ +/* 450 */ /***/ (function(module, exports) { /*! @@ -41509,14 +41589,14 @@ module.exports = pascalcase; /***/ }), -/* 450 */ +/* 451 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var utils = __webpack_require__(451); +var utils = __webpack_require__(452); /** * Expose class utils @@ -41881,7 +41961,7 @@ cu.bubble = function(Parent, events) { /***/ }), -/* 451 */ +/* 452 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -41895,10 +41975,10 @@ var utils = {}; * Lazily required module dependencies */ -utils.union = __webpack_require__(434); -utils.define = __webpack_require__(386); -utils.isObj = __webpack_require__(404); -utils.staticExtend = __webpack_require__(452); +utils.union = __webpack_require__(435); +utils.define = __webpack_require__(387); +utils.isObj = __webpack_require__(405); +utils.staticExtend = __webpack_require__(453); /** @@ -41909,7 +41989,7 @@ module.exports = utils; /***/ }), -/* 452 */ +/* 453 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -41922,8 +42002,8 @@ module.exports = utils; -var copy = __webpack_require__(453); -var define = __webpack_require__(386); +var copy = __webpack_require__(454); +var define = __webpack_require__(387); var util = __webpack_require__(29); /** @@ -42006,15 +42086,15 @@ module.exports = extend; /***/ }), -/* 453 */ +/* 454 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(409); -var copyDescriptor = __webpack_require__(454); -var define = __webpack_require__(386); +var typeOf = __webpack_require__(410); +var copyDescriptor = __webpack_require__(455); +var define = __webpack_require__(387); /** * Copy static properties, prototype properties, and descriptors from one object to another. @@ -42187,7 +42267,7 @@ module.exports.has = has; /***/ }), -/* 454 */ +/* 455 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -42275,16 +42355,16 @@ function isObject(val) { /***/ }), -/* 455 */ +/* 456 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(456); -var define = __webpack_require__(386); -var debug = __webpack_require__(458)('snapdragon:compiler'); -var utils = __webpack_require__(465); +var use = __webpack_require__(457); +var define = __webpack_require__(387); +var debug = __webpack_require__(459)('snapdragon:compiler'); +var utils = __webpack_require__(466); /** * Create a new `Compiler` with the given `options`. @@ -42438,7 +42518,7 @@ Compiler.prototype = { // source map support if (opts.sourcemap) { - var sourcemaps = __webpack_require__(484); + var sourcemaps = __webpack_require__(485); sourcemaps(this); this.mapVisit(this.ast.nodes); this.applySourceMaps(); @@ -42459,7 +42539,7 @@ module.exports = Compiler; /***/ }), -/* 456 */ +/* 457 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -42472,7 +42552,7 @@ module.exports = Compiler; -var utils = __webpack_require__(457); +var utils = __webpack_require__(458); module.exports = function base(app, opts) { if (!utils.isObject(app) && typeof app !== 'function') { @@ -42587,7 +42667,7 @@ module.exports = function base(app, opts) { /***/ }), -/* 457 */ +/* 458 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -42601,8 +42681,8 @@ var utils = {}; * Lazily required module dependencies */ -utils.define = __webpack_require__(386); -utils.isObject = __webpack_require__(404); +utils.define = __webpack_require__(387); +utils.isObject = __webpack_require__(405); utils.isString = function(val) { @@ -42617,7 +42697,7 @@ module.exports = utils; /***/ }), -/* 458 */ +/* 459 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -42626,14 +42706,14 @@ module.exports = utils; */ if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(459); + module.exports = __webpack_require__(460); } else { - module.exports = __webpack_require__(462); + module.exports = __webpack_require__(463); } /***/ }), -/* 459 */ +/* 460 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -42642,7 +42722,7 @@ if (typeof process !== 'undefined' && process.type === 'renderer') { * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(460); +exports = module.exports = __webpack_require__(461); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; @@ -42824,7 +42904,7 @@ function localstorage() { /***/ }), -/* 460 */ +/* 461 */ /***/ (function(module, exports, __webpack_require__) { @@ -42840,7 +42920,7 @@ exports.coerce = coerce; exports.disable = disable; exports.enable = enable; exports.enabled = enabled; -exports.humanize = __webpack_require__(461); +exports.humanize = __webpack_require__(462); /** * The currently active debug mode names, and names to skip. @@ -43032,7 +43112,7 @@ function coerce(val) { /***/ }), -/* 461 */ +/* 462 */ /***/ (function(module, exports) { /** @@ -43190,14 +43270,14 @@ function plural(ms, n, name) { /***/ }), -/* 462 */ +/* 463 */ /***/ (function(module, exports, __webpack_require__) { /** * Module dependencies. */ -var tty = __webpack_require__(463); +var tty = __webpack_require__(464); var util = __webpack_require__(29); /** @@ -43206,7 +43286,7 @@ var util = __webpack_require__(29); * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(460); +exports = module.exports = __webpack_require__(461); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; @@ -43385,7 +43465,7 @@ function createWritableStdioStream (fd) { case 'PIPE': case 'TCP': - var net = __webpack_require__(464); + var net = __webpack_require__(465); stream = new net.Socket({ fd: fd, readable: false, @@ -43444,19 +43524,19 @@ exports.enable(load()); /***/ }), -/* 463 */ +/* 464 */ /***/ (function(module, exports) { module.exports = require("tty"); /***/ }), -/* 464 */ +/* 465 */ /***/ (function(module, exports) { module.exports = require("net"); /***/ }), -/* 465 */ +/* 466 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43466,9 +43546,9 @@ module.exports = require("net"); * Module dependencies */ -exports.extend = __webpack_require__(394); -exports.SourceMap = __webpack_require__(466); -exports.sourceMapResolve = __webpack_require__(477); +exports.extend = __webpack_require__(395); +exports.SourceMap = __webpack_require__(467); +exports.sourceMapResolve = __webpack_require__(478); /** * Convert backslash in the given string to forward slashes @@ -43511,7 +43591,7 @@ exports.last = function(arr, n) { /***/ }), -/* 466 */ +/* 467 */ /***/ (function(module, exports, __webpack_require__) { /* @@ -43519,13 +43599,13 @@ exports.last = function(arr, n) { * Licensed under the New BSD license. See LICENSE.txt or: * http://opensource.org/licenses/BSD-3-Clause */ -exports.SourceMapGenerator = __webpack_require__(467).SourceMapGenerator; -exports.SourceMapConsumer = __webpack_require__(473).SourceMapConsumer; -exports.SourceNode = __webpack_require__(476).SourceNode; +exports.SourceMapGenerator = __webpack_require__(468).SourceMapGenerator; +exports.SourceMapConsumer = __webpack_require__(474).SourceMapConsumer; +exports.SourceNode = __webpack_require__(477).SourceNode; /***/ }), -/* 467 */ +/* 468 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -43535,10 +43615,10 @@ exports.SourceNode = __webpack_require__(476).SourceNode; * http://opensource.org/licenses/BSD-3-Clause */ -var base64VLQ = __webpack_require__(468); -var util = __webpack_require__(470); -var ArraySet = __webpack_require__(471).ArraySet; -var MappingList = __webpack_require__(472).MappingList; +var base64VLQ = __webpack_require__(469); +var util = __webpack_require__(471); +var ArraySet = __webpack_require__(472).ArraySet; +var MappingList = __webpack_require__(473).MappingList; /** * An instance of the SourceMapGenerator represents a source map which is @@ -43947,7 +44027,7 @@ exports.SourceMapGenerator = SourceMapGenerator; /***/ }), -/* 468 */ +/* 469 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -43987,7 +44067,7 @@ exports.SourceMapGenerator = SourceMapGenerator; * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -var base64 = __webpack_require__(469); +var base64 = __webpack_require__(470); // A single base 64 digit can contain 6 bits of data. For the base 64 variable // length quantities we use in the source map spec, the first bit is the sign, @@ -44093,7 +44173,7 @@ exports.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) { /***/ }), -/* 469 */ +/* 470 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -44166,7 +44246,7 @@ exports.decode = function (charCode) { /***/ }), -/* 470 */ +/* 471 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -44589,7 +44669,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate /***/ }), -/* 471 */ +/* 472 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -44599,7 +44679,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(470); +var util = __webpack_require__(471); var has = Object.prototype.hasOwnProperty; var hasNativeMap = typeof Map !== "undefined"; @@ -44716,7 +44796,7 @@ exports.ArraySet = ArraySet; /***/ }), -/* 472 */ +/* 473 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -44726,7 +44806,7 @@ exports.ArraySet = ArraySet; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(470); +var util = __webpack_require__(471); /** * Determine whether mappingB is after mappingA with respect to generated @@ -44801,7 +44881,7 @@ exports.MappingList = MappingList; /***/ }), -/* 473 */ +/* 474 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -44811,11 +44891,11 @@ exports.MappingList = MappingList; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(470); -var binarySearch = __webpack_require__(474); -var ArraySet = __webpack_require__(471).ArraySet; -var base64VLQ = __webpack_require__(468); -var quickSort = __webpack_require__(475).quickSort; +var util = __webpack_require__(471); +var binarySearch = __webpack_require__(475); +var ArraySet = __webpack_require__(472).ArraySet; +var base64VLQ = __webpack_require__(469); +var quickSort = __webpack_require__(476).quickSort; function SourceMapConsumer(aSourceMap) { var sourceMap = aSourceMap; @@ -45889,7 +45969,7 @@ exports.IndexedSourceMapConsumer = IndexedSourceMapConsumer; /***/ }), -/* 474 */ +/* 475 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -46006,7 +46086,7 @@ exports.search = function search(aNeedle, aHaystack, aCompare, aBias) { /***/ }), -/* 475 */ +/* 476 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -46126,7 +46206,7 @@ exports.quickSort = function (ary, comparator) { /***/ }), -/* 476 */ +/* 477 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -46136,8 +46216,8 @@ exports.quickSort = function (ary, comparator) { * http://opensource.org/licenses/BSD-3-Clause */ -var SourceMapGenerator = __webpack_require__(467).SourceMapGenerator; -var util = __webpack_require__(470); +var SourceMapGenerator = __webpack_require__(468).SourceMapGenerator; +var util = __webpack_require__(471); // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other // operating systems these days (capturing the result). @@ -46545,17 +46625,17 @@ exports.SourceNode = SourceNode; /***/ }), -/* 477 */ +/* 478 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014, 2015, 2016, 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var sourceMappingURL = __webpack_require__(478) -var resolveUrl = __webpack_require__(479) -var decodeUriComponent = __webpack_require__(480) -var urix = __webpack_require__(482) -var atob = __webpack_require__(483) +var sourceMappingURL = __webpack_require__(479) +var resolveUrl = __webpack_require__(480) +var decodeUriComponent = __webpack_require__(481) +var urix = __webpack_require__(483) +var atob = __webpack_require__(484) @@ -46853,7 +46933,7 @@ module.exports = { /***/ }), -/* 478 */ +/* 479 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;// Copyright 2014 Simon Lydell @@ -46916,7 +46996,7 @@ void (function(root, factory) { /***/ }), -/* 479 */ +/* 480 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -46934,13 +47014,13 @@ module.exports = resolveUrl /***/ }), -/* 480 */ +/* 481 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var decodeUriComponent = __webpack_require__(481) +var decodeUriComponent = __webpack_require__(482) function customDecodeUriComponent(string) { // `decodeUriComponent` turns `+` into ` `, but that's not wanted. @@ -46951,7 +47031,7 @@ module.exports = customDecodeUriComponent /***/ }), -/* 481 */ +/* 482 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -47052,7 +47132,7 @@ module.exports = function (encodedURI) { /***/ }), -/* 482 */ +/* 483 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -47075,7 +47155,7 @@ module.exports = urix /***/ }), -/* 483 */ +/* 484 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -47089,7 +47169,7 @@ module.exports = atob.atob = atob; /***/ }), -/* 484 */ +/* 485 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -47097,8 +47177,8 @@ module.exports = atob.atob = atob; var fs = __webpack_require__(23); var path = __webpack_require__(16); -var define = __webpack_require__(386); -var utils = __webpack_require__(465); +var define = __webpack_require__(387); +var utils = __webpack_require__(466); /** * Expose `mixin()`. @@ -47241,19 +47321,19 @@ exports.comment = function(node) { /***/ }), -/* 485 */ +/* 486 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(456); +var use = __webpack_require__(457); var util = __webpack_require__(29); -var Cache = __webpack_require__(486); -var define = __webpack_require__(386); -var debug = __webpack_require__(458)('snapdragon:parser'); -var Position = __webpack_require__(487); -var utils = __webpack_require__(465); +var Cache = __webpack_require__(487); +var define = __webpack_require__(387); +var debug = __webpack_require__(459)('snapdragon:parser'); +var Position = __webpack_require__(488); +var utils = __webpack_require__(466); /** * Create a new `Parser` with the given `input` and `options`. @@ -47781,7 +47861,7 @@ module.exports = Parser; /***/ }), -/* 486 */ +/* 487 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -47888,13 +47968,13 @@ MapCache.prototype.del = function mapDelete(key) { /***/ }), -/* 487 */ +/* 488 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(386); +var define = __webpack_require__(387); /** * Store position for a node @@ -47909,14 +47989,14 @@ module.exports = function Position(start, parser) { /***/ }), -/* 488 */ +/* 489 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var nanomatch = __webpack_require__(489); -var extglob = __webpack_require__(500); +var nanomatch = __webpack_require__(490); +var extglob = __webpack_require__(501); module.exports = function(snapdragon) { var compilers = snapdragon.compiler.compilers; @@ -47993,7 +48073,7 @@ function escapeExtglobs(compiler) { /***/ }), -/* 489 */ +/* 490 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -48004,17 +48084,17 @@ function escapeExtglobs(compiler) { */ var util = __webpack_require__(29); -var toRegex = __webpack_require__(385); -var extend = __webpack_require__(394); +var toRegex = __webpack_require__(386); +var extend = __webpack_require__(395); /** * Local dependencies */ -var compilers = __webpack_require__(490); -var parsers = __webpack_require__(491); -var cache = __webpack_require__(493); -var utils = __webpack_require__(495); +var compilers = __webpack_require__(491); +var parsers = __webpack_require__(492); +var cache = __webpack_require__(494); +var utils = __webpack_require__(496); var MAX_LENGTH = 1024 * 64; /** @@ -48843,7 +48923,7 @@ module.exports = nanomatch; /***/ }), -/* 490 */ +/* 491 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -49169,15 +49249,15 @@ module.exports = function(nanomatch, options) { /***/ }), -/* 491 */ +/* 492 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regexNot = __webpack_require__(396); -var toRegex = __webpack_require__(385); -var isOdd = __webpack_require__(492); +var regexNot = __webpack_require__(397); +var toRegex = __webpack_require__(386); +var isOdd = __webpack_require__(493); /** * Characters to use in negation regex (we want to "not" match @@ -49564,7 +49644,7 @@ module.exports.not = NOT_REGEX; /***/ }), -/* 492 */ +/* 493 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -49577,7 +49657,7 @@ module.exports.not = NOT_REGEX; -var isNumber = __webpack_require__(408); +var isNumber = __webpack_require__(409); module.exports = function isOdd(i) { if (!isNumber(i)) { @@ -49591,14 +49671,14 @@ module.exports = function isOdd(i) { /***/ }), -/* 493 */ +/* 494 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(494))(); +module.exports = new (__webpack_require__(495))(); /***/ }), -/* 494 */ +/* 495 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -49611,7 +49691,7 @@ module.exports = new (__webpack_require__(494))(); -var MapCache = __webpack_require__(486); +var MapCache = __webpack_require__(487); /** * Create a new `FragmentCache` with an optional object to use for `caches`. @@ -49733,7 +49813,7 @@ exports = module.exports = FragmentCache; /***/ }), -/* 495 */ +/* 496 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -49746,13 +49826,13 @@ var path = __webpack_require__(16); * Module dependencies */ -var Snapdragon = __webpack_require__(424); -utils.define = __webpack_require__(496); -utils.diff = __webpack_require__(497); -utils.extend = __webpack_require__(394); -utils.pick = __webpack_require__(498); -utils.typeOf = __webpack_require__(499); -utils.unique = __webpack_require__(397); +var Snapdragon = __webpack_require__(425); +utils.define = __webpack_require__(497); +utils.diff = __webpack_require__(498); +utils.extend = __webpack_require__(395); +utils.pick = __webpack_require__(499); +utils.typeOf = __webpack_require__(500); +utils.unique = __webpack_require__(398); /** * Returns true if the given value is effectively an empty string @@ -50118,7 +50198,7 @@ utils.unixify = function(options) { /***/ }), -/* 496 */ +/* 497 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -50131,7 +50211,7 @@ utils.unixify = function(options) { -var isDescriptor = __webpack_require__(416); +var isDescriptor = __webpack_require__(417); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -50156,7 +50236,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 497 */ +/* 498 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -50210,7 +50290,7 @@ function diffArray(one, two) { /***/ }), -/* 498 */ +/* 499 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -50223,7 +50303,7 @@ function diffArray(one, two) { -var isObject = __webpack_require__(404); +var isObject = __webpack_require__(405); module.exports = function pick(obj, keys) { if (!isObject(obj) && typeof obj !== 'function') { @@ -50252,7 +50332,7 @@ module.exports = function pick(obj, keys) { /***/ }), -/* 499 */ +/* 500 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -50405,7 +50485,7 @@ function isBuffer(val) { /***/ }), -/* 500 */ +/* 501 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -50415,18 +50495,18 @@ function isBuffer(val) { * Module dependencies */ -var extend = __webpack_require__(394); -var unique = __webpack_require__(397); -var toRegex = __webpack_require__(385); +var extend = __webpack_require__(395); +var unique = __webpack_require__(398); +var toRegex = __webpack_require__(386); /** * Local dependencies */ -var compilers = __webpack_require__(501); -var parsers = __webpack_require__(507); -var Extglob = __webpack_require__(509); -var utils = __webpack_require__(508); +var compilers = __webpack_require__(502); +var parsers = __webpack_require__(508); +var Extglob = __webpack_require__(510); +var utils = __webpack_require__(509); var MAX_LENGTH = 1024 * 64; /** @@ -50743,13 +50823,13 @@ module.exports = extglob; /***/ }), -/* 501 */ +/* 502 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(502); +var brackets = __webpack_require__(503); /** * Extglob compilers @@ -50919,7 +50999,7 @@ module.exports = function(extglob) { /***/ }), -/* 502 */ +/* 503 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -50929,17 +51009,17 @@ module.exports = function(extglob) { * Local dependencies */ -var compilers = __webpack_require__(503); -var parsers = __webpack_require__(505); +var compilers = __webpack_require__(504); +var parsers = __webpack_require__(506); /** * Module dependencies */ -var debug = __webpack_require__(458)('expand-brackets'); -var extend = __webpack_require__(394); -var Snapdragon = __webpack_require__(424); -var toRegex = __webpack_require__(385); +var debug = __webpack_require__(459)('expand-brackets'); +var extend = __webpack_require__(395); +var Snapdragon = __webpack_require__(425); +var toRegex = __webpack_require__(386); /** * Parses the given POSIX character class `pattern` and returns a @@ -51137,13 +51217,13 @@ module.exports = brackets; /***/ }), -/* 503 */ +/* 504 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var posix = __webpack_require__(504); +var posix = __webpack_require__(505); module.exports = function(brackets) { brackets.compiler @@ -51231,7 +51311,7 @@ module.exports = function(brackets) { /***/ }), -/* 504 */ +/* 505 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -51260,14 +51340,14 @@ module.exports = { /***/ }), -/* 505 */ +/* 506 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(506); -var define = __webpack_require__(386); +var utils = __webpack_require__(507); +var define = __webpack_require__(387); /** * Text regex @@ -51486,14 +51566,14 @@ module.exports.TEXT_REGEX = TEXT_REGEX; /***/ }), -/* 506 */ +/* 507 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var toRegex = __webpack_require__(385); -var regexNot = __webpack_require__(396); +var toRegex = __webpack_require__(386); +var regexNot = __webpack_require__(397); var cached; /** @@ -51527,15 +51607,15 @@ exports.createRegex = function(pattern, include) { /***/ }), -/* 507 */ +/* 508 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(502); -var define = __webpack_require__(496); -var utils = __webpack_require__(508); +var brackets = __webpack_require__(503); +var define = __webpack_require__(497); +var utils = __webpack_require__(509); /** * Characters to use in text regex (we want to "not" match @@ -51690,14 +51770,14 @@ module.exports = parsers; /***/ }), -/* 508 */ +/* 509 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regex = __webpack_require__(396); -var Cache = __webpack_require__(494); +var regex = __webpack_require__(397); +var Cache = __webpack_require__(495); /** * Utils @@ -51766,7 +51846,7 @@ utils.createRegex = function(str) { /***/ }), -/* 509 */ +/* 510 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -51776,16 +51856,16 @@ utils.createRegex = function(str) { * Module dependencies */ -var Snapdragon = __webpack_require__(424); -var define = __webpack_require__(496); -var extend = __webpack_require__(394); +var Snapdragon = __webpack_require__(425); +var define = __webpack_require__(497); +var extend = __webpack_require__(395); /** * Local dependencies */ -var compilers = __webpack_require__(501); -var parsers = __webpack_require__(507); +var compilers = __webpack_require__(502); +var parsers = __webpack_require__(508); /** * Customize Snapdragon parser and renderer @@ -51851,16 +51931,16 @@ module.exports = Extglob; /***/ }), -/* 510 */ +/* 511 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extglob = __webpack_require__(500); -var nanomatch = __webpack_require__(489); -var regexNot = __webpack_require__(396); -var toRegex = __webpack_require__(385); +var extglob = __webpack_require__(501); +var nanomatch = __webpack_require__(490); +var regexNot = __webpack_require__(397); +var toRegex = __webpack_require__(386); var not; /** @@ -51941,14 +52021,14 @@ function textRegex(pattern) { /***/ }), -/* 511 */ +/* 512 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(494))(); +module.exports = new (__webpack_require__(495))(); /***/ }), -/* 512 */ +/* 513 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -51961,13 +52041,13 @@ var path = __webpack_require__(16); * Module dependencies */ -var Snapdragon = __webpack_require__(424); -utils.define = __webpack_require__(496); -utils.diff = __webpack_require__(497); -utils.extend = __webpack_require__(394); -utils.pick = __webpack_require__(498); -utils.typeOf = __webpack_require__(513); -utils.unique = __webpack_require__(397); +var Snapdragon = __webpack_require__(425); +utils.define = __webpack_require__(497); +utils.diff = __webpack_require__(498); +utils.extend = __webpack_require__(395); +utils.pick = __webpack_require__(499); +utils.typeOf = __webpack_require__(514); +utils.unique = __webpack_require__(398); /** * Returns true if the platform is windows, or `path.sep` is `\\`. @@ -52264,7 +52344,7 @@ utils.unixify = function(options) { /***/ }), -/* 513 */ +/* 514 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -52399,7 +52479,7 @@ function isBuffer(val) { /***/ }), -/* 514 */ +/* 515 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -52415,8 +52495,8 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(515); -var reader_1 = __webpack_require__(528); +var readdir = __webpack_require__(516); +var reader_1 = __webpack_require__(529); var ReaderAsync = /** @class */ (function (_super) { __extends(ReaderAsync, _super); function ReaderAsync() { @@ -52452,15 +52532,15 @@ exports.default = ReaderAsync; /***/ }), -/* 515 */ +/* 516 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readdirSync = __webpack_require__(516); -const readdirAsync = __webpack_require__(524); -const readdirStream = __webpack_require__(527); +const readdirSync = __webpack_require__(517); +const readdirAsync = __webpack_require__(525); +const readdirStream = __webpack_require__(528); module.exports = exports = readdirAsyncPath; exports.readdir = exports.readdirAsync = exports.async = readdirAsyncPath; @@ -52544,7 +52624,7 @@ function readdirStreamStat (dir, options) { /***/ }), -/* 516 */ +/* 517 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -52552,11 +52632,11 @@ function readdirStreamStat (dir, options) { module.exports = readdirSync; -const DirectoryReader = __webpack_require__(517); +const DirectoryReader = __webpack_require__(518); let syncFacade = { - fs: __webpack_require__(522), - forEach: __webpack_require__(523), + fs: __webpack_require__(523), + forEach: __webpack_require__(524), sync: true }; @@ -52585,7 +52665,7 @@ function readdirSync (dir, options, internalOptions) { /***/ }), -/* 517 */ +/* 518 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -52594,9 +52674,9 @@ function readdirSync (dir, options, internalOptions) { const Readable = __webpack_require__(28).Readable; const EventEmitter = __webpack_require__(45).EventEmitter; const path = __webpack_require__(16); -const normalizeOptions = __webpack_require__(518); -const stat = __webpack_require__(520); -const call = __webpack_require__(521); +const normalizeOptions = __webpack_require__(519); +const stat = __webpack_require__(521); +const call = __webpack_require__(522); /** * Asynchronously reads the contents of a directory and streams the results @@ -52972,14 +53052,14 @@ module.exports = DirectoryReader; /***/ }), -/* 518 */ +/* 519 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const globToRegExp = __webpack_require__(519); +const globToRegExp = __webpack_require__(520); module.exports = normalizeOptions; @@ -53156,7 +53236,7 @@ function normalizeOptions (options, internalOptions) { /***/ }), -/* 519 */ +/* 520 */ /***/ (function(module, exports) { module.exports = function (glob, opts) { @@ -53293,13 +53373,13 @@ module.exports = function (glob, opts) { /***/ }), -/* 520 */ +/* 521 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const call = __webpack_require__(521); +const call = __webpack_require__(522); module.exports = stat; @@ -53374,7 +53454,7 @@ function symlinkStat (fs, path, lstats, callback) { /***/ }), -/* 521 */ +/* 522 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -53435,14 +53515,14 @@ function callOnce (fn) { /***/ }), -/* 522 */ +/* 523 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const call = __webpack_require__(521); +const call = __webpack_require__(522); /** * A facade around {@link fs.readdirSync} that allows it to be called @@ -53506,7 +53586,7 @@ exports.lstat = function (path, callback) { /***/ }), -/* 523 */ +/* 524 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -53535,7 +53615,7 @@ function syncForEach (array, iterator, done) { /***/ }), -/* 524 */ +/* 525 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -53543,12 +53623,12 @@ function syncForEach (array, iterator, done) { module.exports = readdirAsync; -const maybe = __webpack_require__(525); -const DirectoryReader = __webpack_require__(517); +const maybe = __webpack_require__(526); +const DirectoryReader = __webpack_require__(518); let asyncFacade = { fs: __webpack_require__(23), - forEach: __webpack_require__(526), + forEach: __webpack_require__(527), async: true }; @@ -53590,7 +53670,7 @@ function readdirAsync (dir, options, callback, internalOptions) { /***/ }), -/* 525 */ +/* 526 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -53617,7 +53697,7 @@ module.exports = function maybe (cb, promise) { /***/ }), -/* 526 */ +/* 527 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -53653,7 +53733,7 @@ function asyncForEach (array, iterator, done) { /***/ }), -/* 527 */ +/* 528 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -53661,11 +53741,11 @@ function asyncForEach (array, iterator, done) { module.exports = readdirStream; -const DirectoryReader = __webpack_require__(517); +const DirectoryReader = __webpack_require__(518); let streamFacade = { fs: __webpack_require__(23), - forEach: __webpack_require__(526), + forEach: __webpack_require__(527), async: true }; @@ -53685,15 +53765,15 @@ function readdirStream (dir, options, internalOptions) { /***/ }), -/* 528 */ +/* 529 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(16); -var deep_1 = __webpack_require__(529); -var entry_1 = __webpack_require__(532); +var deep_1 = __webpack_require__(530); +var entry_1 = __webpack_require__(533); var Reader = /** @class */ (function () { function Reader(options) { this.options = options; @@ -53759,15 +53839,15 @@ exports.default = Reader; /***/ }), -/* 529 */ +/* 530 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var arrayUtils = __webpack_require__(530); -var pathUtils = __webpack_require__(531); -var patternUtils = __webpack_require__(378); +var arrayUtils = __webpack_require__(531); +var pathUtils = __webpack_require__(532); +var patternUtils = __webpack_require__(379); var DeepFilter = /** @class */ (function () { function DeepFilter(options, micromatchOptions) { this.options = options; @@ -53842,7 +53922,7 @@ exports.default = DeepFilter; /***/ }), -/* 530 */ +/* 531 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -53865,7 +53945,7 @@ exports.max = max; /***/ }), -/* 531 */ +/* 532 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -53883,13 +53963,13 @@ exports.isDotDirectory = isDotDirectory; /***/ }), -/* 532 */ +/* 533 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var patternUtils = __webpack_require__(378); +var patternUtils = __webpack_require__(379); var DeepFilter = /** @class */ (function () { function DeepFilter(options, micromatchOptions) { this.options = options; @@ -53961,7 +54041,7 @@ exports.default = DeepFilter; /***/ }), -/* 533 */ +/* 534 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -53978,8 +54058,8 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(28); -var readdir = __webpack_require__(515); -var reader_1 = __webpack_require__(528); +var readdir = __webpack_require__(516); +var reader_1 = __webpack_require__(529); var TransformStream = /** @class */ (function (_super) { __extends(TransformStream, _super); function TransformStream(reader) { @@ -54022,7 +54102,7 @@ exports.default = ReaderStream; /***/ }), -/* 534 */ +/* 535 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -54038,8 +54118,8 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(515); -var reader_1 = __webpack_require__(528); +var readdir = __webpack_require__(516); +var reader_1 = __webpack_require__(529); var ReaderSync = /** @class */ (function (_super) { __extends(ReaderSync, _super); function ReaderSync() { @@ -54074,13 +54154,13 @@ exports.default = ReaderSync; /***/ }), -/* 535 */ +/* 536 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const arrify = __webpack_require__(370); +const arrify = __webpack_require__(371); const pathType = __webpack_require__(63); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -54129,17 +54209,17 @@ module.exports.sync = (input, opts) => { /***/ }), -/* 536 */ +/* 537 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); const path = __webpack_require__(16); -const fastGlob = __webpack_require__(372); -const gitIgnore = __webpack_require__(537); +const fastGlob = __webpack_require__(373); +const gitIgnore = __webpack_require__(538); const pify = __webpack_require__(62); -const slash = __webpack_require__(538); +const slash = __webpack_require__(539); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -54231,7 +54311,7 @@ module.exports.sync = o => { /***/ }), -/* 537 */ +/* 538 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -54663,7 +54743,7 @@ typeof process !== 'undefined' && (process.env && process.env.IGNORE_TEST_WIN32 /***/ }), -/* 538 */ +/* 539 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -54681,17 +54761,17 @@ module.exports = function (str) { /***/ }), -/* 539 */ +/* 540 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); const fsConstants = __webpack_require__(23).constants; -const {Buffer} = __webpack_require__(540); -const CpFileError = __webpack_require__(542); -const fs = __webpack_require__(544); -const ProgressEmitter = __webpack_require__(545); +const {Buffer} = __webpack_require__(541); +const CpFileError = __webpack_require__(543); +const fs = __webpack_require__(545); +const ProgressEmitter = __webpack_require__(546); module.exports = (src, dest, opts) => { if (!src || !dest) { @@ -54841,11 +54921,11 @@ module.exports.sync = (src, dest, opts) => { /***/ }), -/* 540 */ +/* 541 */ /***/ (function(module, exports, __webpack_require__) { /* eslint-disable node/no-deprecated-api */ -var buffer = __webpack_require__(541) +var buffer = __webpack_require__(542) var Buffer = buffer.Buffer // alternative to using Object.keys for old browsers @@ -54909,18 +54989,18 @@ SafeBuffer.allocUnsafeSlow = function (size) { /***/ }), -/* 541 */ +/* 542 */ /***/ (function(module, exports) { module.exports = require("buffer"); /***/ }), -/* 542 */ +/* 543 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(543); +const NestedError = __webpack_require__(544); class CpFileError extends NestedError { constructor(message, nested) { @@ -54934,7 +55014,7 @@ module.exports = CpFileError; /***/ }), -/* 543 */ +/* 544 */ /***/ (function(module, exports, __webpack_require__) { var inherits = __webpack_require__(43); @@ -54988,7 +55068,7 @@ module.exports = NestedError; /***/ }), -/* 544 */ +/* 545 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -54996,7 +55076,7 @@ module.exports = NestedError; const fs = __webpack_require__(22); const makeDir = __webpack_require__(90); const pify = __webpack_require__(62); -const CpFileError = __webpack_require__(542); +const CpFileError = __webpack_require__(543); const fsP = pify(fs); @@ -55141,7 +55221,7 @@ if (fs.copyFileSync) { /***/ }), -/* 545 */ +/* 546 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -55182,12 +55262,12 @@ module.exports = ProgressEmitter; /***/ }), -/* 546 */ +/* 547 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(543); +const NestedError = __webpack_require__(544); class CpyError extends NestedError { constructor(message, nested) { @@ -55201,7 +55281,7 @@ module.exports = CpyError; /***/ }), -/* 547 */ +/* 548 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; diff --git a/packages/kbn-pm/package.json b/packages/kbn-pm/package.json index 6a008e5925023..32c9c9173ed6c 100644 --- a/packages/kbn-pm/package.json +++ b/packages/kbn-pm/package.json @@ -10,15 +10,14 @@ "prettier": "prettier --write './src/**/*.ts'" }, "devDependencies": { - "@babel/core": "^7.3.4", - "@babel/plugin-proposal-class-properties": "^7.3.4", - "@babel/plugin-proposal-object-rest-spread": "^7.3.4", - "@babel/preset-env": "^7.3.4", - "@babel/preset-typescript": "^7.3.3", + "@babel/core": "7.4.5", + "@babel/plugin-proposal-class-properties": "7.4.4", + "@babel/plugin-proposal-object-rest-spread": "7.4.4", + "@babel/preset-env": "7.4.5", + "@babel/preset-typescript": "7.3.3", "@types/cmd-shim": "^2.0.0", "@types/cpy": "^5.1.0", "@types/dedent": "^0.7.0", - "@types/del": "^3.0.0", "@types/execa": "^0.9.0", "@types/getopts": "^2.0.1", "@types/glob": "^5.0.35", @@ -34,15 +33,15 @@ "@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", + "babel-loader": "8.0.5", "chalk": "^2.4.1", "cmd-shim": "^2.0.2", "cpy": "^7.0.1", "dedent": "^0.7.0", - "del": "^3.0.0", + "del": "^4.0.0", "execa": "^1.0.0", "getopts": "^2.2.4", "glob": "^7.1.2", @@ -61,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/packages/kbn-test/package.json b/packages/kbn-test/package.json index 5a8f4fdf8521e..c38083317f3d1 100644 --- a/packages/kbn-test/package.json +++ b/packages/kbn-test/package.json @@ -10,19 +10,19 @@ "kbn:watch": "yarn build --watch" }, "devDependencies": { - "@babel/cli": "^7.2.3", + "@babel/cli": "7.4.4", "@kbn/babel-preset": "1.0.0", "@kbn/dev-utils": "1.0.0" }, "dependencies": { "chalk": "^2.4.1", "dedent": "^0.7.0", - "del": "^3.0.0", + "del": "^4.0.0", "getopts": "^2.2.4", "glob": "^7.1.2", "rxjs": "^6.2.1", "tar-fs": "^1.16.2", - "tmp": "^0.0.33", + "tmp": "^0.1.0", "zlib": "^1.0.5" } } diff --git a/packages/kbn-test/src/es/es_test_cluster.js b/packages/kbn-test/src/es/es_test_cluster.js index c8db170cc59e1..53153a21e153b 100644 --- a/packages/kbn-test/src/es/es_test_cluster.js +++ b/packages/kbn-test/src/es/es_test_cluster.js @@ -62,7 +62,7 @@ export function createEsTestCluster(options = {}) { return esFrom === 'snapshot' ? 3 * minute : 6 * minute; } - async start(esArgs = []) { + async start(esArgs = [], esEnvVars) { let installPath; if (esFrom === 'source') { @@ -87,6 +87,7 @@ export function createEsTestCluster(options = {}) { 'discovery.type=single-node', ...esArgs, ], + esEnvVars, }); } diff --git a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.js b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.js index 7971bf94f802b..2e290222b1a9d 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.js +++ b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.js @@ -27,6 +27,7 @@ export async function runElasticsearch({ config, options }) { const { log, esFrom } = options; const license = config.get('esTestCluster.license'); const esArgs = config.get('esTestCluster.serverArgs'); + const esEnvVars = config.get('esTestCluster.serverEnvVars'); const isSecurityEnabled = esArgs.includes('xpack.security.enabled=true'); const cluster = createEsTestCluster({ @@ -41,7 +42,7 @@ export async function runElasticsearch({ config, options }) { dataArchive: config.get('esTestCluster.dataArchive'), }); - await cluster.start(esArgs); + await cluster.start(esArgs, esEnvVars); if (isSecurityEnabled) { await setupUsers(log, config.get('servers.elasticsearch.port'), [ diff --git a/packages/kbn-test/src/functional_tests/lib/run_ftr.js b/packages/kbn-test/src/functional_tests/lib/run_ftr.js index d81d2e195556e..8b347d9dc595f 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_ftr.js +++ b/packages/kbn-test/src/functional_tests/lib/run_ftr.js @@ -17,22 +17,27 @@ * under the License. */ -import { FunctionalTestRunner } from '../../../../../src/functional_test_runner'; +import { FunctionalTestRunner, readConfigFile } from '../../../../../src/functional_test_runner'; import { CliError } from './run_cli'; -function createFtr({ configPath, options: { log, bail, grep, updateBaselines, suiteTags } }) { +async function createFtr({ configPath, options: { log, bail, grep, updateBaselines, suiteTags } }) { + const config = await readConfigFile(log, configPath); + return new FunctionalTestRunner(log, configPath, { mochaOpts: { bail: !!bail, grep, }, updateBaselines, - suiteTags, + suiteTags: { + include: [...suiteTags.include, ...config.get('suiteTags.include')], + exclude: [...suiteTags.exclude, ...config.get('suiteTags.exclude')], + }, }); } export async function assertNoneExcluded({ configPath, options }) { - const ftr = createFtr({ configPath, options }); + const ftr = await createFtr({ configPath, options }); const stats = await ftr.getTestStats(); if (stats.excludedTests.length > 0) { @@ -49,7 +54,7 @@ export async function assertNoneExcluded({ configPath, options }) { } export async function runFtr({ configPath, options }) { - const ftr = createFtr({ configPath, options }); + const ftr = await createFtr({ configPath, options }); const failureCount = await ftr.run(); if (failureCount > 0) { @@ -60,7 +65,7 @@ export async function runFtr({ configPath, options }) { } export async function hasTests({ configPath, options }) { - const ftr = createFtr({ configPath, options }); + const ftr = await createFtr({ configPath, options }); const stats = await ftr.getTestStats(); return stats.testCount > 0; } diff --git a/packages/kbn-test/src/functional_tests/tasks.js b/packages/kbn-test/src/functional_tests/tasks.js index b7f22836ab769..76afe15e9c0ae 100644 --- a/packages/kbn-test/src/functional_tests/tasks.js +++ b/packages/kbn-test/src/functional_tests/tasks.js @@ -109,7 +109,10 @@ export async function startServers(options) { config, options: { ...opts, - extraKbnOpts: [...options.extraKbnOpts, ...(options.installDir ? [] : ['--dev'])], + extraKbnOpts: [ + ...options.extraKbnOpts, + ...(options.installDir ? [] : ['--dev', '--no-dev-config']), + ], }, }); diff --git a/packages/kbn-ui-framework/doc_site/src/views/bar/bar_example.js b/packages/kbn-ui-framework/doc_site/src/views/bar/bar_example.js index a55b4b675e32b..b2da802ec8d84 100644 --- a/packages/kbn-ui-framework/doc_site/src/views/bar/bar_example.js +++ b/packages/kbn-ui-framework/doc_site/src/views/bar/bar_example.js @@ -17,6 +17,8 @@ * under the License. */ +/* eslint-disable import/no-duplicates */ + import React from 'react'; import { renderToHtml } from '../../services'; @@ -29,15 +31,15 @@ import { } from '../../components'; import Bar from './bar'; -const barSource = require('!!raw-loader!./bar'); +import barSource from '!!raw-loader!./bar'; const barHtml = renderToHtml(Bar); import BarOneSection from './bar_one_section'; -const barOneSectionSource = require('!!raw-loader!./bar_one_section'); +import barOneSectionSource from '!!raw-loader!./bar_one_section'; const barOneSectionHtml = renderToHtml(BarOneSection); import BarThreeSections from './bar_three_sections'; -const barThreeSectionsSource = require('!!raw-loader!./bar_three_sections'); +import barThreeSectionsSource from '!!raw-loader!./bar_three_sections'; const barThreeSectionsHtml = renderToHtml(BarThreeSections); export default props => ( diff --git a/packages/kbn-ui-framework/doc_site/src/views/button/button_example.js b/packages/kbn-ui-framework/doc_site/src/views/button/button_example.js index f265dba3164db..f1ee209126a75 100644 --- a/packages/kbn-ui-framework/doc_site/src/views/button/button_example.js +++ b/packages/kbn-ui-framework/doc_site/src/views/button/button_example.js @@ -17,6 +17,8 @@ * under the License. */ +/* eslint-disable import/no-duplicates */ + import React from 'react'; import { renderToHtml } from '../../services'; @@ -30,50 +32,50 @@ import { } from '../../components'; import Basic from './button_basic'; -const basicSource = require('!!raw-loader!./button_basic'); +import basicSource from '!!raw-loader!./button_basic'; const basicHtml = renderToHtml(Basic); import Hollow from './button_hollow'; -const hollowSource = require('!!raw-loader!./button_hollow'); +import hollowSource from '!!raw-loader!./button_hollow'; const hollowHtml = renderToHtml(Hollow); import Primary from './button_primary'; -const primarySource = require('!!raw-loader!./button_primary'); +import primarySource from '!!raw-loader!./button_primary'; const primaryHtml = renderToHtml(Primary); import Secondary from './button_secondary'; -const secondarySource = require('!!raw-loader!./button_secondary'); +import secondarySource from '!!raw-loader!./button_secondary'; const secondaryHtml = renderToHtml(Secondary); import Danger from './button_danger'; -const dangerSource = require('!!raw-loader!./button_danger'); +import dangerSource from '!!raw-loader!./button_danger'; const dangerHtml = renderToHtml(Danger); import Warning from './button_warning'; -const warningSource = require('!!raw-loader!./button_danger'); +import warningSource from '!!raw-loader!./button_danger'; const warningHtml = renderToHtml(Warning); import Loading from './button_loading'; -const loadingSource = require('!!raw-loader!./button_loading'); +import loadingSource from '!!raw-loader!./button_loading'; const loadingHtml = renderToHtml(Loading, { isLoading: true }); import WithIcon from './button_with_icon'; -const withIconSource = require('!!raw-loader!./button_with_icon'); +import withIconSource from '!!raw-loader!./button_with_icon'; const withIconHtml = renderToHtml(WithIcon); import ButtonGroup from './button_group'; -const buttonGroupSource = require('!!raw-loader!./button_group'); +import buttonGroupSource from '!!raw-loader!./button_group'; const buttonGroupHtml = renderToHtml(ButtonGroup); import ButtonGroupUnited from './button_group_united'; -const buttonGroupUnitedSource = require('!!raw-loader!./button_group_united'); +import buttonGroupUnitedSource from '!!raw-loader!./button_group_united'; const buttonGroupUnitedHtml = renderToHtml(ButtonGroupUnited); import Elements from './button_elements'; -const elementsSource = require('!!raw-loader!./button_elements'); +import elementsSource from '!!raw-loader!./button_elements'; const elementsHtml = renderToHtml(Elements); -const sizesHtml = require('./button_sizes.html'); +import sizesHtml from './button_sizes.html'; export default props => ( diff --git a/packages/kbn-ui-framework/doc_site/src/views/collapse_button/collapse_button_example.js b/packages/kbn-ui-framework/doc_site/src/views/collapse_button/collapse_button_example.js index 091600468c170..5706233d673a4 100644 --- a/packages/kbn-ui-framework/doc_site/src/views/collapse_button/collapse_button_example.js +++ b/packages/kbn-ui-framework/doc_site/src/views/collapse_button/collapse_button_example.js @@ -17,6 +17,8 @@ * under the License. */ +/* eslint-disable import/no-duplicates */ + import React from 'react'; import { renderToHtml } from '../../services'; @@ -29,11 +31,11 @@ import { } from '../../components'; import CollapseButton from './collapse_button'; -const collapseButtonSource = require('!!raw-loader!./collapse_button'); +import collapseButtonSource from '!!raw-loader!./collapse_button'; const collapseButtonHtml = renderToHtml(CollapseButton); import CollapseButtonAria from './collapse_button_aria'; -const collapseButtonAriaSource = require('!!raw-loader!./collapse_button_aria'); +import collapseButtonAriaSource from '!!raw-loader!./collapse_button_aria'; const collapseButtonAriaHtml = renderToHtml(CollapseButtonAria); export default props => ( diff --git a/packages/kbn-ui-framework/doc_site/src/views/empty_table_prompt/empty_table_prompt_example.js b/packages/kbn-ui-framework/doc_site/src/views/empty_table_prompt/empty_table_prompt_example.js index cff328d0a8299..f2824b21d6893 100644 --- a/packages/kbn-ui-framework/doc_site/src/views/empty_table_prompt/empty_table_prompt_example.js +++ b/packages/kbn-ui-framework/doc_site/src/views/empty_table_prompt/empty_table_prompt_example.js @@ -17,6 +17,8 @@ * under the License. */ +/* eslint-disable import/no-duplicates */ + import React from 'react'; import { renderToHtml } from '../../services'; @@ -29,11 +31,11 @@ import { } from '../../components'; import { EmptyTablePrompt } from './empty_table_prompt'; -const emptyTablePromptSource = require('!!raw-loader!./empty_table_prompt'); +import emptyTablePromptSource from '!!raw-loader!./empty_table_prompt'; // eslint-disable-line import/default const emptyTablePromptHtml = renderToHtml(EmptyTablePrompt); import { ControlledTableWithEmptyPrompt } from './table_with_empty_prompt'; -const tableWithEmptyPromptSource = require('!!raw-loader!./table_with_empty_prompt'); +import tableWithEmptyPromptSource from '!!raw-loader!./table_with_empty_prompt'; // eslint-disable-line import/default const tableWithEmptyPromptHtml = renderToHtml(ControlledTableWithEmptyPrompt); export default props => ( diff --git a/packages/kbn-ui-framework/doc_site/src/views/expression/expression_example.js b/packages/kbn-ui-framework/doc_site/src/views/expression/expression_example.js index a18063ab3782b..c85c8305d6852 100644 --- a/packages/kbn-ui-framework/doc_site/src/views/expression/expression_example.js +++ b/packages/kbn-ui-framework/doc_site/src/views/expression/expression_example.js @@ -17,6 +17,8 @@ * under the License. */ +/* eslint-disable import/no-duplicates */ + import React from 'react'; import { renderToHtml } from '../../services'; @@ -28,8 +30,8 @@ import { GuideText, } from '../../components'; -const Expression = require('./expression'); -const expressionSource = require('!!raw-loader!./expression'); +import Expression from './expression'; +import expressionSource from '!!raw-loader!./expression'; const expressionHtml = renderToHtml(Expression, { defaultActiveButton: 'example2' }); export default props => ( diff --git a/packages/kbn-ui-framework/doc_site/src/views/form/form_example.js b/packages/kbn-ui-framework/doc_site/src/views/form/form_example.js index 19189bd9e3b7a..c7be739ffa768 100644 --- a/packages/kbn-ui-framework/doc_site/src/views/form/form_example.js +++ b/packages/kbn-ui-framework/doc_site/src/views/form/form_example.js @@ -17,6 +17,8 @@ * under the License. */ +/* eslint-disable import/no-duplicates */ + import React from 'react'; import { renderToHtml } from '../../services'; @@ -29,34 +31,32 @@ import { GuideText, } from '../../components'; -const assistedInputHtml = require('./assisted_input.html'); - -const searchInputHtml = require('./search_input.html'); - -const staticInputHtml = require('./static_input.html'); +import assistedInputHtml from './assisted_input.html'; +import searchInputHtml from './search_input.html'; +import staticInputHtml from './static_input.html'; -const Label = require('./label'); -const labelSource = require('!!raw-loader!./label'); +import Label from './label'; +import labelSource from '!!raw-loader!./label'; const labelHtml = renderToHtml(Label); -const TextInput = require('./text_input'); -const textInputSource = require('!!raw-loader!./text_input'); +import TextInput from './text_input'; +import textInputSource from '!!raw-loader!./text_input'; const textInputHtml = renderToHtml(TextInput, { id: '1' }); -const TextArea = require('./text_area'); -const textAreaSource = require('!!raw-loader!./text_area'); +import TextArea from './text_area'; +import textAreaSource from '!!raw-loader!./text_area'; const textAreaHtml = renderToHtml(TextArea); -const TextAreaNonResizable = require('./text_area_non_resizable'); -const textAreaNonResizableSource = require('!!raw-loader!./text_area_non_resizable'); +import TextAreaNonResizable from './text_area_non_resizable'; +import textAreaNonResizableSource from '!!raw-loader!./text_area_non_resizable'; const textAreaNonResizableHtml = renderToHtml(TextAreaNonResizable); -const Select = require('./select'); -const selectSource = require('!!raw-loader!./select'); +import Select from './select'; +import selectSource from '!!raw-loader!./select'; const selectHtml = renderToHtml(Select); -const CheckBox = require('./check_box'); -const checkBoxSource = require('!!raw-loader!./check_box'); +import CheckBox from './check_box'; +import checkBoxSource from '!!raw-loader!./check_box'; const checkBoxHtml = renderToHtml(CheckBox); export default props => ( diff --git a/packages/kbn-ui-framework/doc_site/src/views/form_layout/form_layout_example.js b/packages/kbn-ui-framework/doc_site/src/views/form_layout/form_layout_example.js index 69effef603317..433e8756d37a7 100644 --- a/packages/kbn-ui-framework/doc_site/src/views/form_layout/form_layout_example.js +++ b/packages/kbn-ui-framework/doc_site/src/views/form_layout/form_layout_example.js @@ -17,6 +17,8 @@ * under the License. */ +/* eslint-disable import/no-duplicates */ + import React from 'react'; import { renderToHtml } from '../../services'; @@ -28,7 +30,7 @@ import { } from '../../components'; import FieldGroup from './field_group'; -const fieldGroupSource = require('!!raw-loader!./field_group'); +import fieldGroupSource from '!!raw-loader!./field_group'; const fieldGroupHtml = renderToHtml(FieldGroup); export default props => ( diff --git a/packages/kbn-ui-framework/doc_site/src/views/icon/icon_example.js b/packages/kbn-ui-framework/doc_site/src/views/icon/icon_example.js index 1597fdb648da7..f0ceb48d223bc 100644 --- a/packages/kbn-ui-framework/doc_site/src/views/icon/icon_example.js +++ b/packages/kbn-ui-framework/doc_site/src/views/icon/icon_example.js @@ -28,15 +28,15 @@ import { GuideText, } from '../../components'; -const iconHtml = require('./icon.html'); -const infoHtml = require('./icon_info.html'); -const basicHtml = require('./icon_basic.html'); -const successHtml = require('./icon_success.html'); -const warningHtml = require('./icon_warning.html'); -const errorHtml = require('./icon_error.html'); -const inactiveHtml = require('./icon_inactive.html'); -const spinnerHtml = require('./icon_spinner.html'); -const spinnerJs = require('raw-loader!./icon_spinner.js'); +import iconHtml from './icon.html'; +import infoHtml from './icon_info.html'; +import basicHtml from './icon_basic.html'; +import successHtml from './icon_success.html'; +import warningHtml from './icon_warning.html'; +import errorHtml from './icon_error.html'; +import inactiveHtml from './icon_inactive.html'; +import spinnerHtml from './icon_spinner.html'; +import spinnerJs from 'raw-loader!./icon_spinner.js'; export default props => ( diff --git a/packages/kbn-ui-framework/doc_site/src/views/info_panel/info_panel_example.js b/packages/kbn-ui-framework/doc_site/src/views/info_panel/info_panel_example.js index 7a3eaf4315255..a36ab229a31cc 100644 --- a/packages/kbn-ui-framework/doc_site/src/views/info_panel/info_panel_example.js +++ b/packages/kbn-ui-framework/doc_site/src/views/info_panel/info_panel_example.js @@ -27,10 +27,10 @@ import { GuideText, } from '../../components'; -const infoHtml = require('./info_panel_info.html'); -const successHtml = require('./info_panel_success.html'); -const warningHtml = require('./info_panel_warning.html'); -const errorHtml = require('./info_panel_error.html'); +import infoHtml from './info_panel_info.html'; +import successHtml from './info_panel_success.html'; +import warningHtml from './info_panel_warning.html'; +import errorHtml from './info_panel_error.html'; export default props => ( diff --git a/packages/kbn-ui-framework/doc_site/src/views/link/link_example.js b/packages/kbn-ui-framework/doc_site/src/views/link/link_example.js index 431478bab471d..f0b3741f9e113 100644 --- a/packages/kbn-ui-framework/doc_site/src/views/link/link_example.js +++ b/packages/kbn-ui-framework/doc_site/src/views/link/link_example.js @@ -26,7 +26,7 @@ import { GuideSectionTypes, } from '../../components'; -const linkHtml = require('./link.html'); +import linkHtml from './link.html'; export default props => ( diff --git a/packages/kbn-ui-framework/doc_site/src/views/local_nav/local_nav_example.js b/packages/kbn-ui-framework/doc_site/src/views/local_nav/local_nav_example.js index 945c9437f36ae..5b2a6803540d7 100644 --- a/packages/kbn-ui-framework/doc_site/src/views/local_nav/local_nav_example.js +++ b/packages/kbn-ui-framework/doc_site/src/views/local_nav/local_nav_example.js @@ -62,7 +62,7 @@ import { LocalNavWithTabs } from './local_nav_tabs'; import localNavWithTabsSource from '!!raw-loader!./local_nav_tabs'; const localNavWithTabsHtml = renderToHtml(LocalNavWithTabs); -const datePickerHtml = require('./local_nav_date_picker.html'); +import datePickerHtml from './local_nav_date_picker.html'; export default props => ( diff --git a/packages/kbn-ui-framework/doc_site/src/views/menu/menu_example.js b/packages/kbn-ui-framework/doc_site/src/views/menu/menu_example.js index 4ba5b4507b297..8e31432a1cccf 100644 --- a/packages/kbn-ui-framework/doc_site/src/views/menu/menu_example.js +++ b/packages/kbn-ui-framework/doc_site/src/views/menu/menu_example.js @@ -17,6 +17,8 @@ * under the License. */ +/* eslint-disable import/no-duplicates */ + import React from 'react'; import { renderToHtml } from '../../services'; @@ -29,11 +31,11 @@ import { } from '../../components'; import Menu from './menu'; -const menuSource = require('!!raw-loader!./menu'); +import menuSource from '!!raw-loader!./menu'; const menuHtml = renderToHtml(Menu); import MenuContained from './menu_contained'; -const menuContainedSource = require('!!raw-loader!./menu_contained'); +import menuContainedSource from '!!raw-loader!./menu_contained'; const menuContainedHtml = renderToHtml(MenuContained); export default props => ( diff --git a/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_example.js b/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_example.js index ba004a1583015..6677a7b29e528 100644 --- a/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_example.js +++ b/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_example.js @@ -27,12 +27,12 @@ import { GuideText, } from '../../components'; -const basicHtml = require('./menu_button_basic.html'); -const primaryHtml = require('./menu_button_primary.html'); -const dangerHtml = require('./menu_button_danger.html'); -const withIconHtml = require('./menu_button_with_icon.html'); -const groupHtml = require('./menu_button_group.html'); -const elementsHtml = require('./menu_button_elements.html'); +import basicHtml from './menu_button_basic.html'; +import primaryHtml from './menu_button_primary.html'; +import dangerHtml from './menu_button_danger.html'; +import withIconHtml from './menu_button_with_icon.html'; +import groupHtml from './menu_button_group.html'; +import elementsHtml from './menu_button_elements.html'; export default props => ( diff --git a/packages/kbn-ui-framework/doc_site/src/views/modal/modal_example.js b/packages/kbn-ui-framework/doc_site/src/views/modal/modal_example.js index 4135edc25a521..b21ee91ddb193 100644 --- a/packages/kbn-ui-framework/doc_site/src/views/modal/modal_example.js +++ b/packages/kbn-ui-framework/doc_site/src/views/modal/modal_example.js @@ -17,6 +17,8 @@ * under the License. */ +/* eslint-disable import/no-duplicates */ + import React from 'react'; import { renderToHtml } from '../../services'; @@ -31,11 +33,11 @@ import { } from '../../components'; import { ModalExample } from './modal'; -const modalSource = require('!!raw-loader!./modal'); +import modalSource from '!!raw-loader!./modal'; // eslint-disable-line import/default const modalHtml = renderToHtml(ModalExample); import { ConfirmModalExample } from './confirm_modal'; -const confirmModalSource = require('!!raw-loader!./confirm_modal'); +import confirmModalSource from '!!raw-loader!./confirm_modal'; // eslint-disable-line import/default const confirmModalHtml = renderToHtml(ConfirmModalExample); export default props => ( diff --git a/packages/kbn-ui-framework/doc_site/src/views/pager/pager_example.js b/packages/kbn-ui-framework/doc_site/src/views/pager/pager_example.js index 9519b5463a46a..08d30fc7c4ff4 100644 --- a/packages/kbn-ui-framework/doc_site/src/views/pager/pager_example.js +++ b/packages/kbn-ui-framework/doc_site/src/views/pager/pager_example.js @@ -17,6 +17,8 @@ * under the License. */ +/* eslint-disable import/no-duplicates */ + import React from 'react'; import { renderToHtml } from '../../services'; @@ -29,11 +31,11 @@ import { } from '../../components'; import { ToolBarPager } from './tool_bar_pager'; -const toolBarPagerSource = require('!!raw-loader!./tool_bar_pager'); +import toolBarPagerSource from '!!raw-loader!./tool_bar_pager'; // eslint-disable-line import/default const toolBarPagerHtml = renderToHtml(ToolBarPager); import { PagerButtons } from './pager_buttons'; -const pagerButtonsSource = require('!!raw-loader!./pager_buttons'); +import pagerButtonsSource from '!!raw-loader!./pager_buttons'; // eslint-disable-line import/default const pagerButtonsHtml = renderToHtml(PagerButtons); export default props => ( diff --git a/packages/kbn-ui-framework/doc_site/src/views/panel/panel_example.js b/packages/kbn-ui-framework/doc_site/src/views/panel/panel_example.js index b5e4ae5b07ff6..98c9a4ca3cea9 100644 --- a/packages/kbn-ui-framework/doc_site/src/views/panel/panel_example.js +++ b/packages/kbn-ui-framework/doc_site/src/views/panel/panel_example.js @@ -27,9 +27,9 @@ import { GuideText, } from '../../components'; -const panelHtml = require('./panel.html'); -const panelWithToolBarHtml = require('./panel_with_toolbar.html'); -const panelWithHeaderSectionsHtml = require('./panel_with_header_sections.html'); +import panelHtml from './panel.html'; +import panelWithToolBarHtml from './panel_with_toolbar.html'; +import panelWithHeaderSectionsHtml from './panel_with_header_sections.html'; export default props => ( diff --git a/packages/kbn-ui-framework/doc_site/src/views/panel_simple/panel_simple_example.js b/packages/kbn-ui-framework/doc_site/src/views/panel_simple/panel_simple_example.js index fd0102a23eb74..3f7ffa2eec089 100644 --- a/packages/kbn-ui-framework/doc_site/src/views/panel_simple/panel_simple_example.js +++ b/packages/kbn-ui-framework/doc_site/src/views/panel_simple/panel_simple_example.js @@ -17,6 +17,8 @@ * under the License. */ +/* eslint-disable import/no-duplicates */ + import React from 'react'; import { Link } from 'react-router'; @@ -33,7 +35,7 @@ import { } from '../../components'; import PanelSimple from './panel_simple'; -const panelSimpleSource = require('!!raw-loader!./panel_simple'); +import panelSimpleSource from '!!raw-loader!./panel_simple'; const panelSimpleHtml = renderToHtml(PanelSimple); export default props => ( diff --git a/packages/kbn-ui-framework/doc_site/src/views/popover/popover_example.js b/packages/kbn-ui-framework/doc_site/src/views/popover/popover_example.js index 7b7553d0f7850..90317ad5c56cd 100644 --- a/packages/kbn-ui-framework/doc_site/src/views/popover/popover_example.js +++ b/packages/kbn-ui-framework/doc_site/src/views/popover/popover_example.js @@ -17,6 +17,8 @@ * under the License. */ +/* eslint-disable import/no-duplicates */ + import React from 'react'; import { renderToHtml } from '../../services'; @@ -31,23 +33,23 @@ import { } from '../../components'; import Popover from './popover'; -const popoverSource = require('!!raw-loader!./popover'); +import popoverSource from '!!raw-loader!./popover'; const popoverHtml = renderToHtml(Popover); import TrapFocus from './trap_focus'; -const trapFocusSource = require('!!raw-loader!./trap_focus'); +import trapFocusSource from '!!raw-loader!./trap_focus'; const trapFocusHtml = renderToHtml(TrapFocus); import PopoverAnchorPosition from './popover_anchor_position'; -const popoverAnchorPositionSource = require('!!raw-loader!./popover_anchor_position'); +import popoverAnchorPositionSource from '!!raw-loader!./popover_anchor_position'; const popoverAnchorPositionHtml = renderToHtml(PopoverAnchorPosition); import PopoverPanelClassName from './popover_panel_class_name'; -const popoverPanelClassNameSource = require('!!raw-loader!./popover_panel_class_name'); +import popoverPanelClassNameSource from '!!raw-loader!./popover_panel_class_name'; const popoverPanelClassNameHtml = renderToHtml(PopoverPanelClassName); import PopoverWithTitle from './popover_with_title'; -const popoverWithTitleSource = require('!!raw-loader!./popover_with_title'); +import popoverWithTitleSource from '!!raw-loader!./popover_with_title'; const popoverWithTitleHtml = renderToHtml(PopoverWithTitle); export default props => ( diff --git a/packages/kbn-ui-framework/doc_site/src/views/status_text/status_text_example.js b/packages/kbn-ui-framework/doc_site/src/views/status_text/status_text_example.js index 9bafdf9ea40c9..dcff295164134 100644 --- a/packages/kbn-ui-framework/doc_site/src/views/status_text/status_text_example.js +++ b/packages/kbn-ui-framework/doc_site/src/views/status_text/status_text_example.js @@ -26,11 +26,11 @@ import { GuideSectionTypes, } from '../../components'; -const html = require('./status_text.html'); -const infoHtml = require('./status_text_info.html'); -const successHtml = require('./status_text_success.html'); -const warningHtml = require('./status_text_warning.html'); -const errorHtml = require('./status_text_error.html'); +import html from './status_text.html'; +import infoHtml from './status_text_info.html'; +import successHtml from './status_text_success.html'; +import warningHtml from './status_text_warning.html'; +import errorHtml from './status_text_error.html'; export default props => ( diff --git a/packages/kbn-ui-framework/doc_site/src/views/table/table_example.js b/packages/kbn-ui-framework/doc_site/src/views/table/table_example.js index 221ba988441c4..c8c834216d335 100644 --- a/packages/kbn-ui-framework/doc_site/src/views/table/table_example.js +++ b/packages/kbn-ui-framework/doc_site/src/views/table/table_example.js @@ -17,6 +17,8 @@ * under the License. */ +/* eslint-disable import/no-duplicates */ + import React from 'react'; import { renderToHtml } from '../../services'; @@ -29,31 +31,31 @@ import { } from '../../components'; import { Table } from './table'; -const tableSource = require('!!raw-loader!./table'); +import tableSource from '!!raw-loader!./table'; // eslint-disable-line import/default const tableHtml = renderToHtml(Table); import { TableWithMenuButtons } from './table_with_menu_buttons'; -const tableWithMenuButtonsSource = require('!!raw-loader!./table_with_menu_buttons'); +import tableWithMenuButtonsSource from '!!raw-loader!./table_with_menu_buttons'; // eslint-disable-line import/default const tableWithMenuButtonsHtml = renderToHtml(TableWithMenuButtons); import { FluidTable } from './fluid_table'; -const fluidTableSource = require('!!raw-loader!./fluid_table'); +import fluidTableSource from '!!raw-loader!./fluid_table'; // eslint-disable-line import/default const fluidTableHtml = renderToHtml(FluidTable); import { ListingTable } from './listing_table'; -const listingTableSource = require('!!raw-loader!./listing_table'); +import listingTableSource from '!!raw-loader!./listing_table'; // eslint-disable-line import/default const listingTableHtml = renderToHtml(ListingTable); import { ListingTableWithEmptyPrompt } from './listing_table_with_empty_prompt'; -const listingTableWithEmptyPromptSource = require('!!raw-loader!./listing_table_with_empty_prompt'); +import listingTableWithEmptyPromptSource from '!!raw-loader!./listing_table_with_empty_prompt'; // eslint-disable-line import/default const listingTableWithEmptyPromptHtml = renderToHtml(ListingTableWithEmptyPrompt); import { ListingTableWithNoItems } from './listing_table_with_no_items'; -const listingTableWithNoItemsSource = require('!!raw-loader!./listing_table_with_no_items'); +import listingTableWithNoItemsSource from '!!raw-loader!./listing_table_with_no_items'; // eslint-disable-line import/default const listingTableWithNoItemsHtml = renderToHtml(ListingTableWithNoItems); import { ListingTableLoadingItems } from './listing_table_loading_items'; -const listingTableLoadingItemsSource = require('!!raw-loader!./listing_table_loading_items'); +import listingTableLoadingItemsSource from '!!raw-loader!./listing_table_loading_items'; // eslint-disable-line import/default const listingTableLoadingItemsHtml = renderToHtml(ListingTableLoadingItems); export default props => ( diff --git a/packages/kbn-ui-framework/doc_site/src/views/tabs/tabs_example.js b/packages/kbn-ui-framework/doc_site/src/views/tabs/tabs_example.js index 540e8e1a6d2ac..a8777531f39d2 100644 --- a/packages/kbn-ui-framework/doc_site/src/views/tabs/tabs_example.js +++ b/packages/kbn-ui-framework/doc_site/src/views/tabs/tabs_example.js @@ -17,6 +17,8 @@ * under the License. */ +/* eslint-disable import/no-duplicates */ + import React from 'react'; import { renderToHtml } from '../../services'; @@ -30,7 +32,7 @@ import { } from '../../components'; import Tabs from './tabs'; -const tabsSource = require('!!raw-loader!./tabs'); +import tabsSource from '!!raw-loader!./tabs'; const tabsHtml = renderToHtml(Tabs); export default props => ( diff --git a/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_button_example.js b/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_button_example.js index a69e8c3d21b87..6fd3a816e4fc4 100644 --- a/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_button_example.js +++ b/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_button_example.js @@ -31,11 +31,11 @@ import { Link, } from 'react-router'; -const toggleButtonHtml = require('./toggle_button.html'); -const toggleButtonJs = require('raw-loader!./toggle_button.js'); -const toggleButtonDisabledHtml = require('./toggle_button_disabled.html'); -const togglePanelHtml = require('./toggle_panel.html'); -const togglePanelJs = require('raw-loader!./toggle_panel.js'); +import toggleButtonHtml from './toggle_button.html'; +import toggleButtonJs from 'raw-loader!./toggle_button.js'; +import toggleButtonDisabledHtml from './toggle_button_disabled.html'; +import togglePanelHtml from './toggle_panel.html'; +import togglePanelJs from 'raw-loader!./toggle_panel.js'; export default props => ( diff --git a/packages/kbn-ui-framework/doc_site/src/views/tool_bar/tool_bar_example.js b/packages/kbn-ui-framework/doc_site/src/views/tool_bar/tool_bar_example.js index fa3a797472f85..e755274e8c31c 100644 --- a/packages/kbn-ui-framework/doc_site/src/views/tool_bar/tool_bar_example.js +++ b/packages/kbn-ui-framework/doc_site/src/views/tool_bar/tool_bar_example.js @@ -17,6 +17,8 @@ * under the License. */ +/* eslint-disable import/no-duplicates */ + import React from 'react'; import { renderToHtml } from '../../services'; @@ -29,11 +31,11 @@ import { } from '../../components'; import { ToolBar } from './tool_bar'; -const toolBarSource = require('!!raw-loader!./tool_bar'); +import toolBarSource from '!!raw-loader!./tool_bar'; // eslint-disable-line import/default const toolBarHtml = renderToHtml(ToolBar); import { ToolBarFooter } from './tool_bar_footer'; -const toolBarFooterSource = require('!!raw-loader!./tool_bar_footer'); +import toolBarFooterSource from '!!raw-loader!./tool_bar_footer'; // eslint-disable-line import/default const toolBarFooterHtml = renderToHtml(ToolBarFooter); export default props => ( diff --git a/packages/kbn-ui-framework/doc_site/src/views/typography/typography_example.js b/packages/kbn-ui-framework/doc_site/src/views/typography/typography_example.js index 17d7051908e7e..1e11257f1f228 100644 --- a/packages/kbn-ui-framework/doc_site/src/views/typography/typography_example.js +++ b/packages/kbn-ui-framework/doc_site/src/views/typography/typography_example.js @@ -28,12 +28,12 @@ import { GuideText, } from '../../components'; -const titleHtml = require('./title.html'); -const subTitleHtml = require('./sub_title.html'); -const textTitleHtml = require('./text_title.html'); -const textHtml = require('./text.html'); -const subTextHtml = require('./sub_text.html'); -const subduedHtml = require('./subdued_type.html'); +import titleHtml from './title.html'; +import subTitleHtml from './sub_title.html'; +import textTitleHtml from './text_title.html'; +import textHtml from './text.html'; +import subTextHtml from './sub_text.html'; +import subduedHtml from './subdued_type.html'; export default props => ( diff --git a/packages/kbn-ui-framework/doc_site/src/views/vertical_rhythm/vertical_rhythm_example.js b/packages/kbn-ui-framework/doc_site/src/views/vertical_rhythm/vertical_rhythm_example.js index d8a559f4d4fe0..d2b3a2fa50854 100644 --- a/packages/kbn-ui-framework/doc_site/src/views/vertical_rhythm/vertical_rhythm_example.js +++ b/packages/kbn-ui-framework/doc_site/src/views/vertical_rhythm/vertical_rhythm_example.js @@ -27,10 +27,10 @@ import { GuideText, } from '../../components'; -const verticalRhythmHtml = require('./vertical_rhythm.html'); -const verticalRhythmSmallHtml = require('./vertical_rhythm_small.html'); -const verticalRhythmAsWrapperHtml = require('./vertical_rhythm_as_wrapper.html'); -const verticalRhythmOnComponentHtml = require('./vertical_rhythm_on_component.html'); +import verticalRhythmHtml from './vertical_rhythm.html'; +import verticalRhythmSmallHtml from './vertical_rhythm_small.html'; +import verticalRhythmAsWrapperHtml from './vertical_rhythm_as_wrapper.html'; +import verticalRhythmOnComponentHtml from './vertical_rhythm_on_component.html'; export default props => ( diff --git a/packages/kbn-ui-framework/doc_site/src/views/view/view_sandbox.js b/packages/kbn-ui-framework/doc_site/src/views/view/view_sandbox.js index cf313e6c72646..6cfd0ec296cfa 100644 --- a/packages/kbn-ui-framework/doc_site/src/views/view/view_sandbox.js +++ b/packages/kbn-ui-framework/doc_site/src/views/view/view_sandbox.js @@ -26,7 +26,7 @@ import { GuideSectionTypes, } from '../../components'; -const html = require('./view_sandbox.html'); +import html from './view_sandbox.html'; export default props => ( diff --git a/packages/kbn-ui-framework/generator-kui/documentation/templates/documentation_page.js b/packages/kbn-ui-framework/generator-kui/documentation/templates/documentation_page.js index 4dc6a47884b2e..df45099bb9c64 100644 --- a/packages/kbn-ui-framework/generator-kui/documentation/templates/documentation_page.js +++ b/packages/kbn-ui-framework/generator-kui/documentation/templates/documentation_page.js @@ -1,3 +1,5 @@ +/* eslint-disable import/no-duplicates */ + import React from 'react'; import { renderToHtml } from '../../services'; @@ -12,7 +14,7 @@ import { } from '../../components'; import <%= componentExampleName %> from './<%= fileName %>'; -const <%= componentExamplePrefix %>Source = require('!!raw-loader!./<%= fileName %>'); +import <%= componentExamplePrefix %>Source from '!!raw-loader!./<%= fileName %>'; // eslint-disable-line import/default const <%= componentExamplePrefix %>Html = renderToHtml(<%= componentExampleName %>); export default props => ( diff --git a/packages/kbn-ui-framework/generator-kui/documentation/templates/documentation_sandbox.js b/packages/kbn-ui-framework/generator-kui/documentation/templates/documentation_sandbox.js index 3edfa10665c65..6dd661601b891 100644 --- a/packages/kbn-ui-framework/generator-kui/documentation/templates/documentation_sandbox.js +++ b/packages/kbn-ui-framework/generator-kui/documentation/templates/documentation_sandbox.js @@ -7,7 +7,7 @@ import { GuideSectionTypes, } from '../../components'; -const html = require('./<%= fileName %>_sandbox.html'); +import html from './<%= fileName %>_sandbox.html'; export default props => ( diff --git a/packages/kbn-ui-framework/package.json b/packages/kbn-ui-framework/package.json index 4d7f60e52af50..cb9683620e0b4 100644 --- a/packages/kbn-ui-framework/package.json +++ b/packages/kbn-ui-framework/package.json @@ -15,14 +15,14 @@ } }, "dependencies": { - "classnames": "2.2.5", + "classnames": "2.2.6", "focus-trap-react": "^3.1.1", "lodash": "npm:@elastic/lodash@3.10.1-kibana1", "prop-types": "15.5.8", "react": "^16.2.0", "react-ace": "^5.9.0", "react-color": "^2.13.8", - "tabbable": "1.1.0", + "tabbable": "1.1.3", "uuid": "3.0.1" }, "peerDependencies": { @@ -30,23 +30,23 @@ "enzyme-adapter-react-16": "^1.9.1" }, "devDependencies": { - "@babel/core": "^7.3.4", - "@babel/polyfill": "^7.2.5", - "@elastic/eui": "0.0.23", + "@babel/core": "7.4.5", + "@babel/polyfill": "7.4.4", + "@elastic/eui": "0.0.55", "@kbn/babel-preset": "1.0.0", - "autoprefixer": "6.5.4", - "babel-loader": "^8.0.5", - "brace": "0.10.0", + "autoprefixer": "6.7.7", + "babel-loader": "8.0.6", + "brace": "0.11.1", "chalk": "^2.4.1", - "chokidar": "1.6.0", - "css-loader": "^1.0.0", + "chokidar": "3.0.0", + "css-loader": "^2.0.0", "expose-loader": "^0.7.5", - "file-loader": "^2.0.0", - "grunt": "1.0.3", + "file-loader": "^4.0.0", + "grunt": "1.0.4", "grunt-babel": "^8.0.0", "grunt-contrib-clean": "^1.1.0", "grunt-contrib-copy": "^1.0.0", - "highlight.js": "9.0.0", + "highlight.js": "9.15.8", "html": "1.0.0", "html-loader": "^0.5.5", "imports-loader": "^0.8.0", @@ -56,7 +56,7 @@ "node-sass": "^4.9.4", "postcss": "^7.0.5", "postcss-loader": "^3.0.0", - "raw-loader": "^0.5.1", + "raw-loader": "^3.0.0", "react-dom": "^16.2.0", "react-redux": "^5.0.6", "react-router": "^3.2.0", diff --git a/renovate.json5 b/renovate.json5 new file mode 100644 index 0000000000000..aa1f6cfbcc161 --- /dev/null +++ b/renovate.json5 @@ -0,0 +1,863 @@ +/** + * PLEASE DO NOT MODIFY + * + * This file is automatically generated by running `node scripts/build_renovate_config` + * + */ +{ + extends: [ + 'config:base', + ], + includePaths: [ + 'package.json', + 'x-pack/package.json', + 'x-pack/plugins/*/package.json', + 'packages/*/package.json', + 'test/plugin_functional/plugins/*/package.json', + 'test/interpreter_functional/plugins/*/package.json', + ], + baseBranches: [ + 'master', + ], + labels: [ + 'release_note:skip', + 'renovate', + 'v8.0.0', + 'v7.3.0', + ], + major: { + labels: [ + 'release_note:skip', + 'renovate', + 'v8.0.0', + 'v7.3.0', + 'renovate:major', + ], + }, + masterIssue: true, + masterIssueApproval: true, + rangeStrategy: 'replace', + npm: { + lockFileMaintenance: { + enabled: false, + }, + packageRules: [ + { + groupSlug: 'eslint', + groupName: 'eslint related packages', + packagePatterns: [ + '(\\b|_)eslint(\\b|_)', + ], + }, + { + groupSlug: 'babel', + groupName: 'babel related packages', + packagePatterns: [ + '(\\b|_)babel(\\b|_)', + ], + packageNames: [ + 'core-js', + '@types/core-js', + ], + }, + { + groupSlug: 'jest', + groupName: 'jest related packages', + packagePatterns: [ + '(\\b|_)jest(\\b|_)', + ], + }, + { + groupSlug: 'mocha', + groupName: 'mocha related packages', + packagePatterns: [ + '(\\b|_)mocha(\\b|_)', + ], + }, + { + groupSlug: 'karma', + groupName: 'karma related packages', + packagePatterns: [ + '(\\b|_)karma(\\b|_)', + ], + }, + { + groupSlug: 'gulp', + groupName: 'gulp related packages', + packagePatterns: [ + '(\\b|_)gulp(\\b|_)', + ], + }, + { + groupSlug: 'grunt', + groupName: 'grunt related packages', + packagePatterns: [ + '(\\b|_)grunt(\\b|_)', + ], + }, + { + groupSlug: 'angular', + groupName: 'angular related packages', + packagePatterns: [ + '(\\b|_)angular(\\b|_)', + ], + }, + { + groupSlug: 'd3', + groupName: 'd3 related packages', + packagePatterns: [ + '(\\b|_)d3(\\b|_)', + ], + }, + { + groupSlug: 'react', + groupName: 'react related packages', + packagePatterns: [ + '(\\b|_)react(\\b|_)', + '(\\b|_)redux(\\b|_)', + '(\\b|_)enzyme(\\b|_)', + ], + packageNames: [ + 'ngreact', + '@types/ngreact', + 'recompose', + '@types/recompose', + 'prop-types', + '@types/prop-types', + 'typescript-fsa-reducers', + '@types/typescript-fsa-reducers', + 'reselect', + '@types/reselect', + ], + }, + { + groupSlug: 'graphql', + groupName: 'graphql related packages', + packagePatterns: [ + '(\\b|_)graphql(\\b|_)', + ], + }, + { + groupSlug: 'webpack', + groupName: 'webpack related packages', + packagePatterns: [ + '(\\b|_)webpack(\\b|_)', + '(\\b|_)loader(\\b|_)', + ], + packageNames: [ + 'mini-css-extract-plugin', + '@types/mini-css-extract-plugin', + 'chokidar', + '@types/chokidar', + ], + }, + { + groupSlug: 'language server', + groupName: 'language server related packages', + packageNames: [ + 'vscode-jsonrpc', + '@types/vscode-jsonrpc', + 'vscode-languageserver', + '@types/vscode-languageserver', + 'vscode-languageserver-types', + '@types/vscode-languageserver-types', + ], + }, + { + groupSlug: 'hapi', + groupName: 'hapi related packages', + packagePatterns: [ + '(\\b|_)hapi(\\b|_)', + ], + packageNames: [ + 'hapi', + '@types/hapi', + 'joi', + '@types/joi', + 'boom', + '@types/boom', + 'hoek', + '@types/hoek', + 'h2o2', + '@types/h2o2', + '@elastic/good', + '@types/elastic__good', + 'good-squeeze', + '@types/good-squeeze', + 'inert', + '@types/inert', + ], + }, + { + groupSlug: 'dragselect', + groupName: 'dragselect related packages', + packageNames: [ + 'dragselect', + '@types/dragselect', + ], + labels: [ + 'release_note:skip', + 'renovate', + 'v8.0.0', + 'v7.3.0', + ':ml', + ], + }, + { + groupSlug: 'api-documenter', + groupName: 'api-documenter related packages', + packageNames: [ + '@microsoft/api-documenter', + '@types/microsoft__api-documenter', + '@microsoft/api-extractor', + '@types/microsoft__api-extractor', + ], + enabled: false, + }, + { + groupSlug: 'json-stable-stringify', + groupName: 'json-stable-stringify related packages', + packageNames: [ + 'json-stable-stringify', + '@types/json-stable-stringify', + ], + }, + { + groupSlug: 'lodash.clonedeep', + groupName: 'lodash.clonedeep related packages', + packageNames: [ + 'lodash.clonedeep', + '@types/lodash.clonedeep', + ], + }, + { + groupSlug: 'yauzl', + groupName: 'yauzl related packages', + packageNames: [ + 'yauzl', + '@types/yauzl', + ], + }, + { + groupSlug: 'bluebird', + groupName: 'bluebird related packages', + packageNames: [ + 'bluebird', + '@types/bluebird', + ], + }, + { + groupSlug: 'chance', + groupName: 'chance related packages', + packageNames: [ + 'chance', + '@types/chance', + ], + }, + { + groupSlug: 'cheerio', + groupName: 'cheerio related packages', + packageNames: [ + 'cheerio', + '@types/cheerio', + ], + }, + { + groupSlug: 'chromedriver', + groupName: 'chromedriver related packages', + packageNames: [ + 'chromedriver', + '@types/chromedriver', + ], + }, + { + groupSlug: 'classnames', + groupName: 'classnames related packages', + packageNames: [ + 'classnames', + '@types/classnames', + ], + }, + { + groupSlug: 'dedent', + groupName: 'dedent related packages', + packageNames: [ + 'dedent', + '@types/dedent', + ], + }, + { + groupSlug: 'delete-empty', + groupName: 'delete-empty related packages', + packageNames: [ + 'delete-empty', + '@types/delete-empty', + ], + }, + { + groupSlug: 'elasticsearch', + groupName: 'elasticsearch related packages', + packageNames: [ + 'elasticsearch', + '@types/elasticsearch', + ], + }, + { + groupSlug: 'execa', + groupName: 'execa related packages', + packageNames: [ + 'execa', + '@types/execa', + ], + }, + { + groupSlug: 'fetch-mock', + groupName: 'fetch-mock related packages', + packageNames: [ + 'fetch-mock', + '@types/fetch-mock', + ], + }, + { + groupSlug: 'getopts', + groupName: 'getopts related packages', + packageNames: [ + 'getopts', + '@types/getopts', + ], + }, + { + groupSlug: 'glob', + groupName: 'glob related packages', + packageNames: [ + 'glob', + '@types/glob', + ], + }, + { + groupSlug: 'globby', + groupName: 'globby related packages', + packageNames: [ + 'globby', + '@types/globby', + ], + }, + { + groupSlug: 'has-ansi', + groupName: 'has-ansi related packages', + packageNames: [ + 'has-ansi', + '@types/has-ansi', + ], + }, + { + groupSlug: 'humps', + groupName: 'humps related packages', + packageNames: [ + 'humps', + '@types/humps', + ], + }, + { + groupSlug: 'jquery', + groupName: 'jquery related packages', + packageNames: [ + 'jquery', + '@types/jquery', + ], + }, + { + groupSlug: 'js-yaml', + groupName: 'js-yaml related packages', + packageNames: [ + 'js-yaml', + '@types/js-yaml', + ], + }, + { + groupSlug: 'json5', + groupName: 'json5 related packages', + packageNames: [ + 'json5', + '@types/json5', + ], + }, + { + groupSlug: 'license-checker', + groupName: 'license-checker related packages', + packageNames: [ + 'license-checker', + '@types/license-checker', + ], + }, + { + groupSlug: 'listr', + groupName: 'listr related packages', + packageNames: [ + 'listr', + '@types/listr', + ], + }, + { + groupSlug: 'lodash', + groupName: 'lodash related packages', + packageNames: [ + 'lodash', + '@types/lodash', + ], + }, + { + groupSlug: 'lru-cache', + groupName: 'lru-cache related packages', + packageNames: [ + 'lru-cache', + '@types/lru-cache', + ], + }, + { + groupSlug: 'markdown-it', + groupName: 'markdown-it related packages', + packageNames: [ + 'markdown-it', + '@types/markdown-it', + ], + }, + { + groupSlug: 'minimatch', + groupName: 'minimatch related packages', + packageNames: [ + 'minimatch', + '@types/minimatch', + ], + }, + { + groupSlug: 'moment-timezone', + groupName: 'moment-timezone related packages', + packageNames: [ + 'moment-timezone', + '@types/moment-timezone', + ], + }, + { + groupSlug: 'mustache', + groupName: 'mustache related packages', + packageNames: [ + 'mustache', + '@types/mustache', + ], + }, + { + groupSlug: 'node', + groupName: 'node related packages', + packageNames: [ + 'node', + '@types/node', + ], + }, + { + groupSlug: 'opn', + groupName: 'opn related packages', + packageNames: [ + 'opn', + '@types/opn', + ], + }, + { + groupSlug: 'podium', + groupName: 'podium related packages', + packageNames: [ + 'podium', + '@types/podium', + ], + }, + { + groupSlug: 'puppeteer-core', + groupName: 'puppeteer-core related packages', + packageNames: [ + 'puppeteer-core', + '@types/puppeteer-core', + ], + }, + { + groupSlug: 'request', + groupName: 'request related packages', + packageNames: [ + 'request', + '@types/request', + ], + }, + { + groupSlug: 'rimraf', + groupName: 'rimraf related packages', + packageNames: [ + 'rimraf', + '@types/rimraf', + ], + }, + { + groupSlug: 'selenium-webdriver', + groupName: 'selenium-webdriver related packages', + packageNames: [ + 'selenium-webdriver', + '@types/selenium-webdriver', + ], + }, + { + groupSlug: 'semver', + groupName: 'semver related packages', + packageNames: [ + 'semver', + '@types/semver', + ], + }, + { + groupSlug: 'sinon', + groupName: 'sinon related packages', + packageNames: [ + 'sinon', + '@types/sinon', + ], + }, + { + groupSlug: 'strip-ansi', + groupName: 'strip-ansi related packages', + packageNames: [ + 'strip-ansi', + '@types/strip-ansi', + ], + }, + { + groupSlug: 'styled-components', + groupName: 'styled-components related packages', + packageNames: [ + 'styled-components', + '@types/styled-components', + ], + }, + { + groupSlug: 'supertest', + groupName: 'supertest related packages', + packageNames: [ + 'supertest', + '@types/supertest', + ], + }, + { + groupSlug: 'type-detect', + groupName: 'type-detect related packages', + packageNames: [ + 'type-detect', + '@types/type-detect', + ], + }, + { + groupSlug: 'uuid', + groupName: 'uuid related packages', + packageNames: [ + 'uuid', + '@types/uuid', + ], + }, + { + groupSlug: 'zen-observable', + groupName: 'zen-observable related packages', + packageNames: [ + 'zen-observable', + '@types/zen-observable', + ], + }, + { + groupSlug: 'base64-js', + groupName: 'base64-js related packages', + packageNames: [ + 'base64-js', + '@types/base64-js', + ], + }, + { + groupSlug: 'chroma-js', + groupName: 'chroma-js related packages', + packageNames: [ + 'chroma-js', + '@types/chroma-js', + ], + }, + { + groupSlug: 'color', + groupName: 'color related packages', + packageNames: [ + 'color', + '@types/color', + ], + }, + { + groupSlug: 'file-saver', + groupName: 'file-saver related packages', + packageNames: [ + 'file-saver', + '@types/file-saver', + ], + }, + { + groupSlug: 'git-url-parse', + groupName: 'git-url-parse related packages', + packageNames: [ + 'git-url-parse', + '@types/git-url-parse', + ], + }, + { + groupSlug: 'history', + groupName: 'history related packages', + packageNames: [ + 'history', + '@types/history', + ], + }, + { + groupSlug: 'jsonwebtoken', + groupName: 'jsonwebtoken related packages', + packageNames: [ + 'jsonwebtoken', + '@types/jsonwebtoken', + ], + }, + { + groupSlug: 'memoize-one', + groupName: 'memoize-one related packages', + packageNames: [ + 'memoize-one', + '@types/memoize-one', + ], + }, + { + groupSlug: 'mime', + groupName: 'mime related packages', + packageNames: [ + 'mime', + '@types/mime', + ], + }, + { + groupSlug: 'mkdirp', + groupName: 'mkdirp related packages', + packageNames: [ + 'mkdirp', + '@types/mkdirp', + ], + }, + { + groupSlug: 'nock', + groupName: 'nock related packages', + packageNames: [ + 'nock', + '@types/nock', + ], + }, + { + groupSlug: 'node-fetch', + groupName: 'node-fetch related packages', + packageNames: [ + 'node-fetch', + '@types/node-fetch', + ], + }, + { + groupSlug: 'object-hash', + groupName: 'object-hash related packages', + packageNames: [ + 'object-hash', + '@types/object-hash', + ], + }, + { + groupSlug: 'papaparse', + groupName: 'papaparse related packages', + packageNames: [ + 'papaparse', + '@types/papaparse', + ], + }, + { + groupSlug: 'pngjs', + groupName: 'pngjs related packages', + packageNames: [ + 'pngjs', + '@types/pngjs', + ], + }, + { + groupSlug: 'proper-lockfile', + groupName: 'proper-lockfile related packages', + packageNames: [ + 'proper-lockfile', + '@types/proper-lockfile', + ], + }, + { + groupSlug: 'reduce-reducers', + groupName: 'reduce-reducers related packages', + packageNames: [ + 'reduce-reducers', + '@types/reduce-reducers', + ], + }, + { + groupSlug: '@storybook/addon-actions', + groupName: '@storybook/addon-actions related packages', + packageNames: [ + '@storybook/addon-actions', + '@types/storybook__addon-actions', + ], + }, + { + groupSlug: '@storybook/addon-info', + groupName: '@storybook/addon-info related packages', + packageNames: [ + '@storybook/addon-info', + '@types/storybook__addon-info', + ], + }, + { + groupSlug: '@storybook/addon-knobs', + groupName: '@storybook/addon-knobs related packages', + packageNames: [ + '@storybook/addon-knobs', + '@types/storybook__addon-knobs', + ], + }, + { + groupSlug: 'tar-fs', + groupName: 'tar-fs related packages', + packageNames: [ + 'tar-fs', + '@types/tar-fs', + ], + }, + { + groupSlug: 'tinycolor2', + groupName: 'tinycolor2 related packages', + packageNames: [ + 'tinycolor2', + '@types/tinycolor2', + ], + }, + { + groupSlug: 'intl-relativeformat', + groupName: 'intl-relativeformat related packages', + packageNames: [ + 'intl-relativeformat', + '@types/intl-relativeformat', + ], + }, + { + groupSlug: 'cmd-shim', + groupName: 'cmd-shim related packages', + packageNames: [ + 'cmd-shim', + '@types/cmd-shim', + ], + }, + { + groupSlug: 'cpy', + groupName: 'cpy related packages', + packageNames: [ + 'cpy', + '@types/cpy', + ], + }, + { + groupSlug: 'indent-string', + groupName: 'indent-string related packages', + packageNames: [ + 'indent-string', + '@types/indent-string', + ], + }, + { + groupSlug: 'lodash.clonedeepwith', + groupName: 'lodash.clonedeepwith related packages', + packageNames: [ + 'lodash.clonedeepwith', + '@types/lodash.clonedeepwith', + ], + }, + { + groupSlug: 'log-symbols', + groupName: 'log-symbols related packages', + packageNames: [ + 'log-symbols', + '@types/log-symbols', + ], + }, + { + groupSlug: 'ncp', + groupName: 'ncp related packages', + packageNames: [ + 'ncp', + '@types/ncp', + ], + }, + { + groupSlug: 'ora', + groupName: 'ora related packages', + packageNames: [ + 'ora', + '@types/ora', + ], + }, + { + groupSlug: 'read-pkg', + groupName: 'read-pkg related packages', + packageNames: [ + 'read-pkg', + '@types/read-pkg', + ], + }, + { + groupSlug: 'strong-log-transformer', + groupName: 'strong-log-transformer related packages', + packageNames: [ + 'strong-log-transformer', + '@types/strong-log-transformer', + ], + }, + { + groupSlug: 'tempy', + groupName: 'tempy related packages', + packageNames: [ + 'tempy', + '@types/tempy', + ], + }, + { + groupSlug: 'wrap-ansi', + groupName: 'wrap-ansi related packages', + packageNames: [ + 'wrap-ansi', + '@types/wrap-ansi', + ], + }, + { + groupSlug: 'write-pkg', + groupName: 'write-pkg related packages', + packageNames: [ + 'write-pkg', + '@types/write-pkg', + ], + }, + { + packagePatterns: [ + '^@kbn/.*', + ], + enabled: false, + }, + ], + }, + prConcurrentLimit: 6, + vulnerabilityAlerts: { + enabled: false, + }, + rebaseStalePrs: false, + semanticCommits: false, +} diff --git a/scripts/build_renovate_config.js b/scripts/build_renovate_config.js new file mode 100644 index 0000000000000..b9171c44f4a8a --- /dev/null +++ b/scripts/build_renovate_config.js @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +require('../src/setup_node_env'); +require('../src/dev/renovate/run_build_renovate_config_cli'); diff --git a/scripts/check_licenses.js b/scripts/check_licenses.js new file mode 100644 index 0000000000000..238bcc966102d --- /dev/null +++ b/scripts/check_licenses.js @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +require('../src/setup_node_env'); +require('../src/dev/license_checker/run_check_licenses_cli'); diff --git a/scripts/update_prs.js b/scripts/update_prs.js new file mode 100644 index 0000000000000..32ca032962c98 --- /dev/null +++ b/scripts/update_prs.js @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +require('../src/setup_node_env'); +require('../src/dev/prs/run_update_prs_cli'); diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index 9a339f3bb6c8f..2741f8594e16b 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -184,19 +184,20 @@ export default function (program) { .option('--open', 'Open a browser window to the base url after the server is started') .option('--ssl', 'Run the dev server using HTTPS') .option('--no-base-path', 'Don\'t put a proxy in front of the dev server, which adds a random basePath') - .option('--no-watch', 'Prevents automatic restarts of the server in --dev mode'); + .option('--no-watch', 'Prevents automatic restarts of the server in --dev mode') + .option('--no-dev-config', 'Prevents loading the kibana.dev.yml file in --dev mode'); } command .action(async function (opts) { - if (opts.dev) { + if (opts.dev && opts.devConfig !== false) { try { const kbnDevConfig = fromRoot('config/kibana.dev.yml'); if (statSync(kbnDevConfig).isFile()) { opts.config.push(kbnDevConfig); } } catch (err) { - // ignore, kibana.dev.yml does not exist + // ignore, kibana.dev.yml does not exist } } diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 4fc13b1cc8fd1..87aa9464de252 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -20,9 +20,13 @@ * [Frequently asked questions](#frequently-asked-questions) * [Is migrating a plugin an all-or-nothing thing?](#is-migrating-a-plugin-an-all-or-nothing-thing) * [Do plugins need to be converted to TypeScript?](#do-plugins-need-to-be-converted-to-typescript) - * [How is static code shared between plugins?](#how-is-static-code-shared-between-plugins) + * [Can static code be shared between plugins?](#can-static-code-be-shared-between-plugins) + * [How can I avoid passing Core services deeply within my UI component tree?](#how-can-i-avoid-passing-core-services-deeply-within-my-ui-component-tree) * [How is "common" code shared on both the client and server?](#how-is-common-code-shared-on-both-the-client-and-server) * [When does code go into a plugin, core, or packages?](#when-does-code-go-into-a-plugin-core-or-packages) +* [How to](#how-to) + * [Configure plugin](#configure-plugin) + * [Mock core services in tests](#mock-core-services-in-tests) Make no mistake, it is going to take a lot of work to move certain plugins to the new platform. Our target is to migrate the entire repo over to the new platform throughout 7.x and to remove the legacy plugin system no later than 8.0, and this is only possible if teams start on the effort now. @@ -80,7 +84,7 @@ export function plugin(initializerContext: PluginInitializerContext) { **[3] `public/plugin.ts`** is the client-side plugin definition itself. Technically speaking it does not need to be a class or even a separate file from the entry point, but _all plugins at Elastic_ should be consistent in this way. ```ts -import { PluginInitializerContext, CoreSetup, PluginStop } from '../../../core/public'; +import { PluginInitializerContext, CoreSetup, CoreStart } from '../../../core/public'; export class Plugin { constructor(initializerContext: PluginInitializerContext) { @@ -90,7 +94,11 @@ export class Plugin { // called when plugin is setting up } - public stop(core: PluginStop) { + public start(core: CoreStart) { + // called after all plugins are set up + } + + public stop() { // called when plugin is torn down, aka window.onbeforeunload } } @@ -110,7 +118,7 @@ export function plugin(initializerContext: PluginInitializerContext) { **[5] `server/plugin.ts`** is the server-side plugin definition. The _shape_ of this plugin is the same as it's client-side counter-part: ```ts -import { PluginInitializerContext, CoreSetup, CoreStop } from '../../../core/server'; +import { PluginInitializerContext, CoreSetup, CoreStart } from '../../../core/server'; export class Plugin { constructor(initializerContext: PluginInitializerContext) { @@ -120,7 +128,11 @@ export class Plugin { // called when plugin is setting up during Kibana's startup sequence } - public stop(core: CoreStop) { + public start(core: CoreStart) { + // called after all plugins are set up + } + + public stop() { // called when plugin is torn down during Kibana's shutdown sequence } } @@ -132,9 +144,17 @@ The platform does not impose any technical restrictions on how the internals of The various independent domains that make up `core` are represented by a series of services, and many of those services expose public interfaces that are provided to _all_ plugins. Services expose different features at different parts of their _lifecycle_. We describe the lifecycle of core services and plugins with specifically-named functions on the service definition. -In the new platform, there are two lifecycle functions today: `setup` and `stop`. The `setup` functions are invoked sequentially while Kibana is starting up on the server or when it is being loaded in the browser. The `stop` functions are invoked sequentially while Kibana is gracefully shutting down on the server or when the browser tab or window is being closed. +In the new platform, there are three lifecycle functions today: `setup`, `start`, and `stop`. The `setup` functions are invoked sequentially while Kibana is setting up on the server or when it is being loaded in the browser. The `start` functions are invoked sequentially after setup has completed for all plugins. The `stop` functions are invoked sequentially while Kibana is gracefully shutting down on the server or when the browser tab or window is being closed. + +The table below explains how each lifecycle event relates to the state of Kibana. + +| lifecycle event | server | browser | +|-----------------|-------------------------------------------|-----------------------------------------------------| +| *setup* | bootstrapping and configuring routes | loading plugin bundles and configuring applications | +| *start* | server is now serving traffic | browser is now showing UI to the user | +| *stop* | server has received a request to shutdown | user is navigating away from Kibana | -There is no equivalent behavior to `stop` in legacy plugins, so this guide primarily focuses on migrating functionality into `setup`. +There is no equivalent behavior to `start` or `stop` in legacy plugins, so this guide primarily focuses on migrating functionality into `setup`. The lifecycle-specific contracts exposed by core services are always passed as the first argument to the equivalent lifecycle function in a plugin. For example, the core `UiSettings` service exposes a function `get` to all plugin `setup` functions. To use this function to retrieve a specific UI setting, a plugin just accesses it off of the first argument: @@ -156,15 +176,15 @@ Core services that expose functionality to plugins always have their `setup` fun ### Integrating with other plugins -Plugins can expose public interfaces for other plugins to consume. Like `core`, those interfaces are bound to the lifecycle functions `setup` and/or `stop`. +Plugins can expose public interfaces for other plugins to consume. Like `core`, those interfaces are bound to the lifecycle functions `setup` and/or `start`. -Anything returned from `setup` or `stop` will act as the interface, and while not a technical requirement, all Elastic plugins should expose types for that interface as well. +Anything returned from `setup` or `start` will act as the interface, and while not a technical requirement, all Elastic plugins should expose types for that interface as well. 3rd party plugins wishing to allow other plugins to integrate with it are also highly encouraged to expose types for their plugin interfaces. **foobar plugin.ts:** ```ts export type FoobarPluginSetup = ReturnType; -export type FoobarPluginStop = ReturnType; +export type FoobarPluginStart = ReturnType; export class Plugin { public setup() { @@ -175,7 +195,7 @@ export class Plugin { }; } - public stop() { + public start() { return { getBar() { return 'bar'; @@ -200,20 +220,20 @@ Unlike core, capabilities exposed by plugins are _not_ automatically injected in } ``` -With that specified in the plugin manifest, the appropriate interfaces are then available via the second argument of `setup` and/or `stop`: +With that specified in the plugin manifest, the appropriate interfaces are then available via the second argument of `setup` and/or `start`: **demo plugin.ts:** ```ts -import { CoreSetup, PluginStop } from '../../../core/server'; +import { CoreSetup, CoreStart } from '../../../core/server'; import { FoobarPluginSetup, FoobarPluginStop } from '../../foobar/server'; interface DemoSetupPlugins { - foobar: FoobarPluginSetup + foobar: FoobarPluginSetup; } -interface DemoStopPlugins { - foobar: FoobarPluginStop +interface DemoStartPlugins { + foobar: FoobarPluginStart; } export class Plugin { @@ -223,11 +243,13 @@ export class Plugin { foobar.getBar(); // throws because getBar does not exist } - public stop(core: PluginStop, plugins: DemoStopPlugins) { + public start(core: CoreStart, plugins: DemoStartPlugins) { const { foobar } = plugins; foobar.getFoo(); // throws because getFoo does not exist foobar.getBar(); // 'bar' } + + public stop() {}, } ``` @@ -241,7 +263,7 @@ This means that there are unique sets of challenges for migrating to the new pla The general shape/architecture of legacy server-side code is similar to the new platform architecture in one important way: most legacy server-side plugins define an `init` function where the bulk of their business logic begins, and they access both "core" and "plugin-provided" functionality through the arguments given to `init`. Rarely does legacy server-side code share stateful services via import statements. -While not exactly the same, legacy plugin `init` functions behave similarly today as new platform `setup` functions. There is no corresponding legacy concept of `stop`, however. +While not exactly the same, legacy plugin `init` functions behave similarly today as new platform `setup` functions. `KbnServer` also exposes an `afterPluginsInit` method which behaves similarly to `start`. There is no corresponding legacy concept of `stop`, however. Despite their similarities, server-side plugins pose a formidable challenge: legacy core and plugin functionality is retrieved from either the hapi.js `server` or `request` god objects. Worse, these objects are often passed deeply throughout entire plugins, which directly couples business logic with hapi. And the worst of it all is, these objects are mutable at any time. @@ -651,27 +673,141 @@ No. That said, the migration process will require a lot of refactoring, and Type At the very least, any plugin exposing an extension point should do so with first-class type support so downstream plugins that _are_ using TypeScript can depend on those types. -### How is static code shared between plugins? +### Can static code be shared between plugins? + +**tl;dr** Yes, but it should be limited to pure functional code that does not depend on outside state from the platform or a plugin. + +#### Background + +> Don't care why, just want to know how? Skip to the ["how" section below](#how-to-decide-what-code-can-be-statically-imported). + +Legacy Kibana has never run as a single page application. Each plugin has it's own entry point and gets "ownership" of every module it imports when it is loaded into the browser. This has allowed stateful modules to work without breaking other plugins because each time the user navigates to a new plugin, the browser reloads with a different entry bundle, clearing the state of the previous plugin. + +Because of this "feature" many undesirable things developed in the legacy platform: +- We had to invent an unconventional and fragile way of allowing plugins to integrate and communicate with one another, `uiExports`. +- It has never mattered if shared modules in `ui/public` were stateful or cleaned up after themselves, so many of them behave like global singletons. These modules could never work in single-page application because of this state. +- We've had to ship Webpack with Kibana in production so plugins could be disabled or installed and still have access to all the "platform" features of `ui/public` modules and all the `uiExports` would be present for any enabled plugins. +- We've had to require that 3rd-party plugin developers release a new version of their plugin for each and every version of Kibana because these shared modules have no stable API and are coupled tightly both to their consumers and the Kibana platform. + +The New Platform's primary goal is to make developing Kibana plugins easier, both for developers at Elastic and in the community. The approach we've chosen is to enable plugins to integrate and communicate _at runtime_ rather than at build time. By wiring services and plugins up at runtime, we can ship stable APIs that do not have to be compiled into every plugin and instead live inside a solid core that each plugin gets connected to when it executes. + +This applies to APIs that plugins expose as well. In the new platform, plugins can communicate through an explicit interface rather than importing all the code from one another and having to recompile Webpack bundles when a plugin is disabled or a new plugin is installed. + +You've probably noticed that this is not the typical way a JavaScript developer works. We're used to importing code at the top of files (and for some use-cases this is still fine). However, we're not building a typical JavaScript application, we're building an application that is installed into a dynamic system (the Kibana Platform). + +#### What goes wrong if I do share modules with state? + +One goal of a stable Kibana core API is to allow Kibana instances to run plugins with varying minor versions, e.g. Kibana 8.4.0 running PluginX 8.0.1 and PluginY 8.2.5. This will be made possible by building each plugin into an “immutable bundle” that can be installed into Kibana. You can think of an immutable bundle as code that doesn't share any imported dependencies with any other bundles, that is all it's dependencies are bundled together. + +This method of building and installing plugins comes with side effects which are important to be aware of when developing a plugin. + + +- **Any code you export to other plugins will get copied into their bundles.** If a plugin is built for 8.1 and is running on Kibana 8.2, any modules it imported that changed will not be updated in that plugin. +- **When a plugin is disabled, other plugins can still import its static exports.** This can make code difficult to reason about and result in poor user experience. For example, users generally expect that all of a plugin’s features will be disabled when the plugin is disabled. If another plugin imports a disabled plugin’s feature and exposes it to the user, then users will be confused about whether that plugin really is disabled or not. +- **Plugins cannot share state by importing each others modules.** Sharing state via imports does not work because exported modules will be copied into plugins that import them. Let’s say your plugin exports a module that’s imported by other plugins. If your plugin populates state into this module, a natural expectation would be that the other plugins now have access to this state. However, because those plugins have copies of the exported module, this assumption will be incorrect. + +#### How to decide what code can be statically imported + +The general rule of thumb here is: any module that is not purely functional should not be shared statically, and instead should be exposed at runtime via the plugin's `setup` and/or `start` contracts. + +Ask yourself these questions when deciding to share code through static exports or plugin contracts: +- Is its behavior dependent on any state populated from my plugin? +- If a plugin uses an old copy (from an older version of Kibana) of this module, will it still break? + +If you answered yes to any of the above questions, you probably have an impure module that cannot be shared across plugins. Another way to think about this: if someone literally copied and pasted your exported module into their plugin, would it break if: +- Your original module changed in a future version and the copy was the old version; or +- If your plugin doesn’t have access to the copied version in the other plugin (because it doesn't know about it). + +If your module were to break for either of these reasons, it should not be exported statically. This can be more easily illustrated by examples of what can and cannot be exported statically. + +Examples of code that could be shared statically: +- Constants. Strings and numbers that do not ever change (even between Kibana versions) + - If constants do change between Kibana versions, then they should only be exported statically if the old value would not _break_ if it is still used. For instance, exporting a constant like `VALID_INDEX_NAME_CHARACTERS` would be fine, but exporting a constant like `API_BASE_PATH` would not because if this changed, old bundles using the previous value would break. +- React components that do not depend on module state. + - Make sure these components are not dependent on or pre-wired to Core services. In many of these cases you can export a HOC that takes the Core service and returns a component wired up to that particular service instance. + - These components do not need to be "pure" in the sense that they do not use React state or React hooks, they just cannot rely on state inside the module or any modules it imports. +- Pure computation functions, for example lodash-like functions like `mapValues`. + +Examples of code that could **not** be shared statically and how to fix it: +- A function that calls a Core service, but does not take that service as a parameter. + - If the function does not take a client as an argument, it must have an instance of the client in its internal state, populated by your plugin. This would not work across plugin boundaries because your plugin would not be able to call `setClient` in the copy of this module in other plugins: + ```js + let esClient; + export const setClient = (client) => esClient = client; + export const query = (params) => esClient.search(params); + ``` + - This could be fixed by requiring the calling code to provide the client: + ```js + export const query = (esClient, params) => esClient.search(params); + ``` +- A function that allows other plugins to register values that get pushed into an array defined internally to the module. + - The values registered would only be visible to the plugin that imported it. Each plugin would essentially have their own registry of visTypes that is not visible to any other plugins. + ```js + const visTypes = []; + export const registerVisType = (visType) => visTypes.push(visType); + export const getVisTypes = () => visTypes; + ``` + - For state that does need to be shared across plugins, you will need to expose methods in your plugin's `setup` and `start` contracts. + ```js + class MyPlugin { + constructor() { this.visTypes = [] } + setup() { + return { + registerVisType: (visType) => this.visTypes.push(visType) + } + } + + start() { + return { + getVisTypes: () => this.visTypes + } + } + } + ``` + +In any case, you will also need to carefully consider backward compatibility (BWC). Whatever you choose to export will need to work for the entire major version cycle (eg. Kibana 8.0-8.9), regardless of which version of the export a plugin has bundled and which minor version of Kibana they're using. Breaking changes to static exports are only allowed in major versions. However, during the 7.x cycle, all of these APIs are considered "experimental" and can be broken at any time. We will not consider these APIs stable until 8.0 at the earliest. + +#### Concrete Example -Plugins are strongly discouraged from sharing static code for other plugins to import. There will be times when it is necessary, so it will remain possible, but it has serious drawbacks that won't necessarily be clear at development time. +Ok, you've decided you want to export static code from your plugin, how do you do it? The New Platform only considers values exported from `my_plugin/public` and `my_plugin/server` to be stable. The linter will only let you import statically from these top-level modules. In the future, our tooling will enforce that these APIs do not break between minor versions. All code shared among plugins should be exported in these modules like so: + +```ts +// my_plugin/public/index.ts +export { MyPureComponent } from './components'; + +// regular plugin export used by core to initialize your plugin +export const plugin = ...; +``` + +These can then be imported using relative paths from other plugins: + +```ts +// my_other_plugin/public/components/my_app.ts +import { MyPureComponent } from '../my_plugin/public'; +``` -1. When a plugin is uninstalled, its code is removed from the filesystem, so all imports referencing it will break. This will result in Kibana failing to start or load, and there is no way to recover beyond installing the missing plugin or disabling the plugin with the broken import. -2. When a plugin is disabled, its static exports will still be importable by any other plugin. This can result in undesirable effects where it _appears_ like a plugin is enabled when it is not. In the worst case, it can result in an unexpected user experience where features that should have been disabled are not. -3. Code that is statically imported will be _copied_ into the plugin that imported it. This will bloat your plugin's client-side bundles and its footprint on the server's file system. Often today client-side imports expose a global singleton, and due to this copying behavior that will no longer work. +If you have code that should be available to other plugins on both the client and server, you can have a common directory. _See [How is "common" code shared on both the client and server?](#how-is-common-code-shared-on-both-the-client-and-server)_ -If you must share code statically, regardless of whether static code is on the server or in the browser, it can be imported via relative paths. +### How can I avoid passing Core services deeply within my UI component tree? -For some background, this has long been problematic in Kibana for two reasons: +There are some Core services that are purely presentational, for example `core.overlays.openModal()` or `core.application.createLink()` where UI code does need access to these deeply within your application. However, passing these services down as props throughout your application leads to lots of boilerplate. To avoid this, you have three options: +1. Use an abstraction layer, like Redux, to decouple your UI code from core (**this is the highly preferred option**); or + - [redux-thunk](https://github.com/reduxjs/redux-thunk#injecting-a-custom-argument) and [redux-saga](https://redux-saga.js.org/docs/api/#createsagamiddlewareoptions) already have ways to do this. +1. Use React Context to provide these services to large parts of your React tree; or +1. Create a high-order-component that injects core into a React component; or + - This would be a stateful module that holds a reference to Core, but provides it as props to components with a `withCore(MyComponent)` interface. This can make testing components simpler. (Note: this module cannot be shared across plugin boundaries, see above). +1. Create a global singleton module that gets imported into each module that needs it. (Note: this module cannot be shared across plugin boundaries, see above). [Example](https://gist.github.com/epixa/06c8eeabd99da3c7545ab295e49acdc3). -* Plugin directories were configurable, so there was no reliably relative path for imports across plugins and from core. This has since been addressed and all plugins in the Kibana repo have reliable locations relative to the Kibana root. -* The `x-pack` directory moved into `node_modules` at build time, so a relative import from `x-pack` to `src` that worked during development would break once a Kibana distribution was built. This is still a problem today, but the fix is in flight via issue [#32722](https://github.com/elastic/kibana/pull/32722). +If you find that you need many different Core services throughout your application, this may be a code smell and could lead to pain down the road. For instance, if you need access to an HTTP Client or SavedObjectsClient in many places in your React tree, it's likely that a data layer abstraction (like Redux) could make developing your plugin much simpler (see option 1). -Any code not exported via the index of either the `server` or `public` directories should never be imported outside that plugin as it should be considered unstable and subject to change at any time. +Without such an abstraction, you will need to mock out Core services throughout your test suite and will couple your UI code very tightly to Core. However, if you can contain all of your integration points with Core to Redux middleware and/or reducers, you only need to mock Core services once, and benefit from being able to change those integrations with Core in one place rather than many. This will become incredibly handy when Core APIs have breaking changes. ### How is "common" code shared on both the client and server? There is no formal notion of "common" code that can safely be imported from either client-side or server-side code. However, if a plugin author wishes to maintain a set of code in their plugin in a single place and then expose it to both server-side and client-side code, they can do so by exporting in the index files for both the `server` and `public` directories. +Plugins should not ever import code from deeply inside another plugin (eg. `my_plugin/public/components`) or from other top-level directories (eg. `my_plugin/common/constants`) as these are not checked for breaking changes and are considered unstable and subject to change at any time. You can have other top-level directories like `my_plugin/common`, but our tooling will not treat these as a stable API and linter rules will prevent importing from these directories _from outside the plugin_. + The benefit of this approach is that the details of where code lives and whether it is accessible in multiple runtimes is an implementation detail of the plugin itself. A plugin consumer that is writing client-side code only ever needs to concern themselves with the client-side contracts being exposed, and the same can be said for server-side contracts on the server. A plugin author that decides some set of code should diverge from having a single "common" definition can now safely change the implementation details without impacting downstream consumers. @@ -687,3 +823,59 @@ There is essentially no code that _can't_ exist in a plugin. When in doubt, put After plugins, core is where most of the rest of the code in Kibana will exist. Functionality that's critical to the reliable execution of the Kibana process belongs in core. Services that will widely be used by nearly every non-trivial plugin in any Kibana install belong in core. Functionality that is too specialized to specific use cases should not be in core, so while something like generic saved objects is a core concern, index patterns are not. The packages directory should have the least amount of code in Kibana. Just because some piece of code is not stateful doesn't mean it should go into packages. The packages directory exists to aid us in our quest to centralize as many of our owned dependencies in this single monorepo, so it's the logical place to put things like Kibana specific forks of node modules or vendor dependencies. + + +## How to + +### Configure plugin +Kibana provides ConfigService if a plugin developer may want to support adjustable runtime behavior for their plugins. Access to Kibana config in New platform has been subject to significant refactoring. +In order to have access to a config, plugin *should*: +- Declare plugin specific "configPath" (will fallback to plugin "id" if not specified) in `kibana.json` file. +- Export schema validation for config from plugin's main file. Schema is mandatory. If a plugin reads from the config without schema declaration, ConfigService will throw an error. +```typescript +// my_plugin/server/index.ts +import { schema, TypeOf } from '@kbn/config-schema'; +export const plugin = ... +export const config = { + schema: schema.object(...), +}; +export type MyPluginConfigType = TypeOf; +``` +- Read config value exposed via initializerContext. No config path is required. +```typescript +class MyPlugin { + constructor(initializerContext: PluginInitializerContext) { + this.config$ = initializerContext.config.create(); + // or if config is optional: + this.config$ = initializerContext.config.createIfExists(); + } +``` + +### Mock core services in tests +Core services already provide mocks to simplify testing and make sure plugins always rely on valid public contracts. +```typescript +// my_plugin/server/plugin.test.ts +Import { configServiceMock } from 'src/core/server/mocks.ts' + +const configService = configServiceMock.create(); +configService.atPath.mockReturnValue(config$); +… +const plugin = new MyPlugin({ configService }, …) +``` +However it's not mandatory, we strongly recommended to export your plugin mocks as well, in order for dependent plugins to use them in tests. Your plugin mocks should be exported from the root level of the plugin. Plugin mocks should consist of mocks for *public API only*: setup/start/stop contracts. Mocks aren't necessary for pure functions as other plugins can call original implementation in tests. +```typescript +// my_plugin/server/mocks.ts +const createSetupContractMock = () => { + const startContract: jest.Mocked= { + isValid: jest.fn(); + } + // here we already type check as TS infers to the correct type declared above + startContract.isValid.mockReturnValue(true); + return startContract; +} + +export const myPluginMocks = { + createSetup: createSetupContractMock, + createStart: ... +} +``` diff --git a/src/core/public/application/application_service.test.tsx b/src/core/public/application/application_service.test.tsx index bd58bcfc73452..af100ab5f5f5b 100644 --- a/src/core/public/application/application_service.test.tsx +++ b/src/core/public/application/application_service.test.tsx @@ -17,7 +17,6 @@ * under the License. */ -import { basePathServiceMock } from '../base_path/base_path_service.mock'; import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; import { MockCapabilitiesService } from './application_service.test.mocks'; import { ApplicationService } from './application_service'; @@ -29,9 +28,7 @@ describe('#start()', () => { setup.registerApp({ id: 'app1' } as any); setup.registerLegacyApp({ id: 'app2' } as any); const injectedMetadata = injectedMetadataServiceMock.createStartContract(); - const basePath = basePathServiceMock.createStartContract(); - expect((await service.start({ basePath, injectedMetadata })).availableApps) - .toMatchInlineSnapshot(` + expect((await service.start({ injectedMetadata })).availableApps).toMatchInlineSnapshot(` Array [ Object { "id": "app1", @@ -48,11 +45,9 @@ Array [ const setup = service.setup(); setup.registerApp({ id: 'app1' } as any); const injectedMetadata = injectedMetadataServiceMock.createStartContract(); - const basePath = basePathServiceMock.createStartContract(); - await service.start({ basePath, injectedMetadata }); + await service.start({ injectedMetadata }); expect(MockCapabilitiesService.start).toHaveBeenCalledWith({ apps: [{ id: 'app1' }], - basePath, injectedMetadata, }); }); @@ -62,11 +57,9 @@ Array [ const setup = service.setup(); setup.registerLegacyApp({ id: 'legacyApp1' } as any); const injectedMetadata = injectedMetadataServiceMock.createStartContract(); - const basePath = basePathServiceMock.createStartContract(); - await service.start({ basePath, injectedMetadata }); + await service.start({ injectedMetadata }); expect(MockCapabilitiesService.start).toHaveBeenCalledWith({ apps: [{ id: 'legacyApp1' }], - basePath, injectedMetadata, }); }); diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index d195984552e66..e7f18cf7bfcdd 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -20,7 +20,6 @@ import { Observable, BehaviorSubject } from 'rxjs'; import { CapabilitiesStart, CapabilitiesService, Capabilities } from './capabilities'; import { InjectedMetadataStart } from '../injected_metadata'; -import { BasePathStart } from '../base_path'; interface BaseApp { id: string; @@ -106,7 +105,6 @@ export interface ApplicationStart { } interface StartDeps { - basePath: BasePathStart; injectedMetadata: InjectedMetadataStart; } @@ -130,14 +128,13 @@ export class ApplicationService { }; } - public async start({ basePath, injectedMetadata }: StartDeps): Promise { + public async start({ injectedMetadata }: StartDeps): Promise { this.apps$.complete(); this.legacyApps$.complete(); const apps = [...this.apps$.value, ...this.legacyApps$.value]; const { capabilities, availableApps } = await this.capabilities.start({ apps, - basePath, injectedMetadata, }); diff --git a/src/core/public/application/capabilities/capabilities_service.mock.ts b/src/core/public/application/capabilities/capabilities_service.mock.ts index 0c6f284bf7f3a..e152c1d9ee638 100644 --- a/src/core/public/application/capabilities/capabilities_service.mock.ts +++ b/src/core/public/application/capabilities/capabilities_service.mock.ts @@ -17,7 +17,7 @@ * under the License. */ import { CapabilitiesService, CapabilitiesStart } from './capabilities_service'; -import { deepFreeze } from '../../utils/deep_freeze'; +import { deepFreeze } from '../../../utils/'; import { MixedApp } from '../application_service'; const createStartContractMock = ( diff --git a/src/core/public/application/capabilities/capabilities_service.test.ts b/src/core/public/application/capabilities/capabilities_service.test.ts index 6ec1b26a4ed4a..d7d3c4c7ef72b 100644 --- a/src/core/public/application/capabilities/capabilities_service.test.ts +++ b/src/core/public/application/capabilities/capabilities_service.test.ts @@ -17,88 +17,30 @@ * under the License. */ -// @ts-ignore -import fetchMock from 'fetch-mock/es5/client'; - import { InjectedMetadataService } from '../../injected_metadata'; import { CapabilitiesService } from './capabilities_service'; -import { basePathServiceMock } from '../../base_path/base_path_service.mock'; describe('#start', () => { - const basePath = basePathServiceMock.createStartContract(); - basePath.addToPath.mockImplementation(str => str); const injectedMetadata = new InjectedMetadataService({ injectedMetadata: { - vars: { - uiCapabilities: { - foo: { feature: true }, - bar: { feature: true }, + version: 'kibanaVersion', + capabilities: { + catalogue: {}, + management: {}, + navLinks: { + app1: true, + app2: false, }, + foo: { feature: true }, + bar: { feature: true }, }, } as any, }).start(); const apps = [{ id: 'app1' }, { id: 'app2', capabilities: { app2: { feature: true } } }] as any; - beforeEach(() => { - fetchMock.post('/api/capabilities', (url: string, options: any) => ({ - body: options.body, - status: 200, - })); - }); - - afterEach(() => { - fetchMock.restore(); - }); - - it('calls backend API with merged capabilities', async () => { - const service = new CapabilitiesService(); - await service.start({ apps, basePath, injectedMetadata }); - expect(fetchMock.calls()).toMatchInlineSnapshot(` -Array [ - Array [ - "/api/capabilities", - Object { - "body": "{\\"capabilities\\":{\\"navLinks\\":{\\"app2\\":true,\\"app1\\":true},\\"management\\":{},\\"catalogue\\":{},\\"app2\\":{\\"feature\\":true}}}", - "credentials": "same-origin", - "headers": Object { - "kbn-xsrf": "xxx", - }, - "method": "POST", - }, - ], -] -`); - }); - - it('returns capabilities from backend', async () => { - const service = new CapabilitiesService(); - expect((await service.start({ apps, basePath, injectedMetadata })).capabilities) - .toMatchInlineSnapshot(` -Object { - "app2": Object { - "feature": true, - }, - "catalogue": Object {}, - "management": Object {}, - "navLinks": Object { - "app1": true, - "app2": true, - }, -} -`); - }); - it('filters available apps based on returned navLinks', async () => { - fetchMock.post( - '/api/capabilities', - (url: string, options: any) => ({ - body: JSON.stringify({ capabilities: { navLinks: { app1: true, app2: false } } }), - status: 200, - }), - { overwriteRoutes: true } - ); const service = new CapabilitiesService(); - expect((await service.start({ apps, basePath, injectedMetadata })).availableApps).toEqual([ + expect((await service.start({ apps, injectedMetadata })).availableApps).toEqual([ { id: 'app1' }, ]); }); @@ -107,7 +49,6 @@ Object { const service = new CapabilitiesService(); const { capabilities } = await service.start({ apps, - basePath, injectedMetadata, }); diff --git a/src/core/public/application/capabilities/capabilities_service.tsx b/src/core/public/application/capabilities/capabilities_service.tsx index 13d9c6d50c0dd..86fd1c74af907 100644 --- a/src/core/public/application/capabilities/capabilities_service.tsx +++ b/src/core/public/application/capabilities/capabilities_service.tsx @@ -17,15 +17,12 @@ * under the License. */ -import { deepFreeze, RecursiveReadonly } from '../../utils/deep_freeze'; +import { deepFreeze, RecursiveReadonly } from '../../../utils'; import { MixedApp } from '../application_service'; -import { mergeCapabilities } from './merge_capabilities'; import { InjectedMetadataStart } from '../../injected_metadata'; -import { BasePathStart } from '../../base_path'; interface StartDeps { apps: ReadonlyArray; - basePath: BasePathStart; injectedMetadata: InjectedMetadataStart; } @@ -75,39 +72,8 @@ export interface CapabilitiesStart { * Service that is responsible for UI Capabilities. */ export class CapabilitiesService { - public async start({ apps, basePath, injectedMetadata }: StartDeps): Promise { - const mergedCapabilities = mergeCapabilities( - // Custom capabilites for new platform apps - ...apps.filter(app => app.capabilities).map(app => app.capabilities!), - // Generate navLink capabilities for all apps - ...apps.map(app => ({ navLinks: { [app.id]: true } })) - ); - - // NOTE: should replace `fetch` with browser HTTP service once it exists - const res = await fetch(basePath.addToPath('/api/capabilities'), { - method: 'POST', - body: JSON.stringify({ capabilities: mergedCapabilities }), - headers: { - 'kbn-xsrf': 'xxx', - }, - credentials: 'same-origin', - }); - - if (res.status === 401) { - return { - availableApps: [], - capabilities: deepFreeze({ - navLinks: {}, - management: {}, - catalogue: {}, - }), - }; - } else if (res.status !== 200) { - throw new Error(`Capabilities check failed.`); - } - - const body = await res.json(); - const capabilities = deepFreeze(body.capabilities as Capabilities); + public async start({ apps, injectedMetadata }: StartDeps): Promise { + const capabilities = deepFreeze(injectedMetadata.getCapabilities()); const availableApps = apps.filter(app => capabilities.navLinks[app.id]); return { diff --git a/src/core/public/base_path/base_path_service.mock.ts b/src/core/public/base_path/base_path_service.mock.ts deleted file mode 100644 index 964f8962c6abd..0000000000000 --- a/src/core/public/base_path/base_path_service.mock.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { BasePathService, BasePathSetup } from './base_path_service'; - -const createSetupContractMock = () => { - const setupContract: jest.Mocked = { - get: jest.fn(), - addToPath: jest.fn(), - removeFromPath: jest.fn(), - }; - setupContract.get.mockReturnValue('get'); - setupContract.addToPath.mockReturnValue('addToPath'); - setupContract.removeFromPath.mockReturnValue('removeFromPath'); - return setupContract; -}; - -const createStartContractMock = createSetupContractMock; - -type BasePathServiceContract = PublicMethodsOf; -const createMock = () => { - const mocked: jest.Mocked = { - setup: jest.fn(), - start: jest.fn(), - }; - mocked.setup.mockReturnValue(createSetupContractMock()); - mocked.start.mockReturnValue(createStartContractMock()); - return mocked; -}; - -export const basePathServiceMock = { - create: createMock, - createSetupContract: createSetupContractMock, - createStartContract: createStartContractMock, -}; diff --git a/src/core/public/base_path/base_path_service.test.ts b/src/core/public/base_path/base_path_service.test.ts deleted file mode 100644 index b99aa8b7b714e..0000000000000 --- a/src/core/public/base_path/base_path_service.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; -import { BasePathService } from './base_path_service'; - -function setupService(options: any = {}) { - const injectedBasePath: string = - options.injectedBasePath === undefined ? '/foo/bar' : options.injectedBasePath; - - const service = new BasePathService(); - - const injectedMetadata = injectedMetadataServiceMock.createSetupContract(); - injectedMetadata.getBasePath.mockReturnValue(injectedBasePath); - - const setupContract = service.setup({ - injectedMetadata, - }); - - return { - service, - setupContract, - injectedBasePath, - }; -} - -describe('setup.get()', () => { - it('returns an empty string if no basePath is injected', () => { - const { setupContract } = setupService({ injectedBasePath: null }); - expect(setupContract.get()).toBe(''); - }); - - it('returns the injected basePath', () => { - const { setupContract } = setupService(); - expect(setupContract.get()).toBe('/foo/bar'); - }); -}); - -describe('setup.addToPath()', () => { - it('adds the base path to the path if it is relative and starts with a slash', () => { - const { setupContract } = setupService(); - expect(setupContract.addToPath('/a/b')).toBe('/foo/bar/a/b'); - }); - - it('leaves the query string and hash of path unchanged', () => { - const { setupContract } = setupService(); - expect(setupContract.addToPath('/a/b?x=y#c/d/e')).toBe('/foo/bar/a/b?x=y#c/d/e'); - }); - - it('returns the path unchanged if it does not start with a slash', () => { - const { setupContract } = setupService(); - expect(setupContract.addToPath('a/b')).toBe('a/b'); - }); - - it('returns the path unchanged it it has a hostname', () => { - const { setupContract } = setupService(); - expect(setupContract.addToPath('http://localhost:5601/a/b')).toBe('http://localhost:5601/a/b'); - }); -}); - -describe('setup.removeFromPath()', () => { - it('removes the basePath if relative path starts with it', () => { - const { setupContract } = setupService(); - expect(setupContract.removeFromPath('/foo/bar/a/b')).toBe('/a/b'); - }); - - it('leaves query string and hash intact', () => { - const { setupContract } = setupService(); - expect(setupContract.removeFromPath('/foo/bar/a/b?c=y#1234')).toBe('/a/b?c=y#1234'); - }); - - it('ignores urls with hostnames', () => { - const { setupContract } = setupService(); - expect(setupContract.removeFromPath('http://localhost:5601/foo/bar/a/b')).toBe( - 'http://localhost:5601/foo/bar/a/b' - ); - }); - - it('returns slash if path is just basePath', () => { - const { setupContract } = setupService(); - expect(setupContract.removeFromPath('/foo/bar')).toBe('/'); - }); - - it('returns full path if basePath is not its own segment', () => { - const { setupContract } = setupService(); - expect(setupContract.removeFromPath('/foo/barhop')).toBe('/foo/barhop'); - }); -}); diff --git a/src/core/public/base_path/base_path_service.ts b/src/core/public/base_path/base_path_service.ts deleted file mode 100644 index 8b7ff8ba74c55..0000000000000 --- a/src/core/public/base_path/base_path_service.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -/* eslint-disable max-classes-per-file */ - -import { InjectedMetadataSetup, InjectedMetadataStart } from '../injected_metadata'; -import { modifyUrl } from '../utils'; - -/** - * Provides access to the 'server.basePath' configuration option in kibana.yml - * - * @public - */ -export interface BasePathSetup { - /** - * Get the basePath as defined by the server - * - * @returns The basePath as defined by the server - */ - get(): string; - - /** - * Add the current basePath to a path string. - * - * @param path - A relative url including the leading `/`, otherwise it will be returned without modification - */ - addToPath(path: string): string; - - /** - * Removes basePath from the given path if the path starts with it - * - * @param path - A relative url that starts with the basePath, which will be stripped - */ - removeFromPath(path: string): string; -} - -/** - * Provides access to the 'server.basePath' configuration option in kibana.yml - * - * @public - */ -export type BasePathStart = BasePathSetup; - -interface SetupDeps { - injectedMetadata: InjectedMetadataSetup; -} - -interface StartDeps { - injectedMetadata: InjectedMetadataStart; -} - -/** @internal */ -export class BasePathService { - public setup({ injectedMetadata }: SetupDeps) { - const basePath = injectedMetadata.getBasePath() || ''; - - const basePathSetup: BasePathSetup = { - get: () => basePath, - addToPath: path => { - return modifyUrl(path, parts => { - if (!parts.hostname && parts.pathname && parts.pathname.startsWith('/')) { - parts.pathname = `${basePath}${parts.pathname}`; - } - }); - }, - removeFromPath(path: string): string { - if (!basePath) { - return path; - } - - if (path === basePath) { - return '/'; - } - - if (path.startsWith(basePath + '/')) { - return path.slice(basePath.length); - } - - return path; - }, - }; - - return basePathSetup; - } - - public start({ injectedMetadata }: StartDeps) { - return this.setup({ injectedMetadata }); - } -} diff --git a/src/core/public/base_path/index.ts b/src/core/public/base_path/index.ts deleted file mode 100644 index 8b44050b16939..0000000000000 --- a/src/core/public/base_path/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { BasePathService, BasePathSetup, BasePathStart } from './base_path_service'; diff --git a/src/core/public/chrome/chrome_service.ts b/src/core/public/chrome/chrome_service.ts index 37146b05f9206..74a2e6cff1dc1 100644 --- a/src/core/public/chrome/chrome_service.ts +++ b/src/core/public/chrome/chrome_service.ts @@ -27,7 +27,7 @@ import { InjectedMetadataSetup } from '../injected_metadata'; import { NotificationsSetup } from '../notifications'; import { NavLinksService } from './nav_links/nav_links_service'; import { ApplicationStart } from '../application'; -import { BasePathStart } from '../base_path'; +import { HttpStart } from '../http'; const IS_COLLAPSED_KEY = 'core.chrome.isCollapsed'; @@ -70,7 +70,7 @@ interface SetupDeps { interface StartDeps { application: ApplicationStart; - basePath: BasePathStart; + http: HttpStart; } /** @internal */ @@ -230,9 +230,9 @@ export class ChromeService { }; } - public start({ application, basePath }: StartDeps) { + public start({ application, http }: StartDeps) { return { - navLinks: this.navLinks.start({ application, basePath }), + navLinks: this.navLinks.start({ application, http }), }; } diff --git a/src/core/public/chrome/nav_links/nav_links_service.test.ts b/src/core/public/chrome/nav_links/nav_links_service.test.ts index 9acc1bc132da8..d6ccdf84c0f2c 100644 --- a/src/core/public/chrome/nav_links/nav_links_service.test.ts +++ b/src/core/public/chrome/nav_links/nav_links_service.test.ts @@ -28,8 +28,8 @@ const mockAppService = { ], } as any; -const mockBasePath = { - addToPath: (url: string) => `wow${url}`, +const mockHttp = { + prependBasePath: (url: string) => `wow${url}`, } as any; describe('NavLinksService', () => { @@ -38,7 +38,7 @@ describe('NavLinksService', () => { beforeEach(() => { service = new NavLinksService(); - start = service.start({ application: mockAppService, basePath: mockBasePath }); + start = service.start({ application: mockAppService, http: mockHttp }); }); describe('#getNavLinks$()', () => { diff --git a/src/core/public/chrome/nav_links/nav_links_service.ts b/src/core/public/chrome/nav_links/nav_links_service.ts index 156d8e2d01573..e8ea30ff6f61d 100644 --- a/src/core/public/chrome/nav_links/nav_links_service.ts +++ b/src/core/public/chrome/nav_links/nav_links_service.ts @@ -22,17 +22,17 @@ import { BehaviorSubject, ReplaySubject } from 'rxjs'; import { map, takeUntil } from 'rxjs/operators'; import { NavLinkWrapper, NavLinkUpdateableFields } from './nav_link'; import { ApplicationStart } from '../../application'; -import { BasePathStart } from '../../base_path'; +import { HttpStart } from '../../http'; interface StartDeps { application: ApplicationStart; - basePath: BasePathStart; + http: HttpStart; } export class NavLinksService { private readonly stop$ = new ReplaySubject(1); - public start({ application, basePath }: StartDeps) { + public start({ application, http }: StartDeps) { const navLinks$ = new BehaviorSubject>( new Map( application.availableApps.map( @@ -42,7 +42,7 @@ export class NavLinksService { new NavLinkWrapper({ ...app, // Either rootRoute or appUrl must be defined. - baseUrl: relativeToAbsolute(basePath.addToPath((app.rootRoute || app.appUrl)!)), + baseUrl: relativeToAbsolute(http.prependBasePath((app.rootRoute || app.appUrl)!)), }), ] as [string, NavLinkWrapper] ) diff --git a/src/core/public/core_system.test.mocks.ts b/src/core/public/core_system.test.mocks.ts index f33d9ff2ec17f..4567785be8c32 100644 --- a/src/core/public/core_system.test.mocks.ts +++ b/src/core/public/core_system.test.mocks.ts @@ -17,7 +17,6 @@ * under the License. */ -import { basePathServiceMock } from './base_path/base_path_service.mock'; import { applicationServiceMock } from './application/application_service.mock'; import { chromeServiceMock } from './chrome/chrome_service.mock'; import { fatalErrorsServiceMock } from './fatal_errors/fatal_errors_service.mock'; @@ -74,12 +73,6 @@ jest.doMock('./http', () => ({ HttpService: HttpServiceConstructor, })); -export const MockBasePathService = basePathServiceMock.create(); -export const BasePathServiceConstructor = jest.fn().mockImplementation(() => MockBasePathService); -jest.doMock('./base_path', () => ({ - BasePathService: BasePathServiceConstructor, -})); - export const MockUiSettingsService = uiSettingsServiceMock.create(); export const UiSettingsServiceConstructor = jest .fn() diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts index b0d37f0103df0..0973c9a05030b 100644 --- a/src/core/public/core_system.test.ts +++ b/src/core/public/core_system.test.ts @@ -18,14 +18,12 @@ */ import { - BasePathServiceConstructor, ChromeServiceConstructor, FatalErrorsServiceConstructor, HttpServiceConstructor, I18nServiceConstructor, InjectedMetadataServiceConstructor, LegacyPlatformServiceConstructor, - MockBasePathService, MockChromeService, MockFatalErrorsService, MockHttpService, @@ -78,7 +76,6 @@ describe('constructor', () => { expect(FatalErrorsServiceConstructor).toHaveBeenCalledTimes(1); expect(NotificationServiceConstructor).toHaveBeenCalledTimes(1); expect(HttpServiceConstructor).toHaveBeenCalledTimes(1); - expect(BasePathServiceConstructor).toHaveBeenCalledTimes(1); expect(UiSettingsServiceConstructor).toHaveBeenCalledTimes(1); expect(ChromeServiceConstructor).toHaveBeenCalledTimes(1); expect(OverlayServiceConstructor).toHaveBeenCalledTimes(1); @@ -169,11 +166,6 @@ describe('#setup()', () => { expect(MockHttpService.setup).toHaveBeenCalledTimes(1); }); - it('calls basePath#setup()', async () => { - await setupCore(); - expect(MockBasePathService.setup).toHaveBeenCalledTimes(1); - }); - it('calls uiSettings#setup()', async () => { await setupCore(); expect(MockUiSettingsService.setup).toHaveBeenCalledTimes(1); @@ -243,6 +235,7 @@ describe('#start()', () => { expect(MockNotificationsService.start).toHaveBeenCalledTimes(1); expect(MockNotificationsService.start).toHaveBeenCalledWith({ i18n: expect.any(Object), + overlays: expect.any(Object), targetDomElement: expect.any(HTMLElement), }); }); diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 4002eafba7b03..84ff227f11c7e 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -19,8 +19,7 @@ import './core.css'; -import { CoreSetup, CoreStart } from '.'; -import { BasePathService } from './base_path'; +import { InternalCoreSetup, InternalCoreStart } from '.'; import { ChromeService } from './chrome'; import { FatalErrorsService, FatalErrorsSetup } from './fatal_errors'; import { HttpService } from './http'; @@ -32,6 +31,7 @@ import { OverlayService } from './overlays'; import { PluginsService } from './plugins'; import { UiSettingsService } from './ui_settings'; import { ApplicationService } from './application'; +import { mapToObject } from '../utils/'; interface Params { rootDomElement: HTMLElement; @@ -60,7 +60,6 @@ export class CoreSystem { private readonly notifications: NotificationsService; private readonly http: HttpService; private readonly uiSettings: UiSettingsService; - private readonly basePath: BasePathService; private readonly chrome: ChromeService; private readonly i18n: I18nService; private readonly overlay: OverlayService; @@ -94,7 +93,6 @@ export class CoreSystem { this.notifications = new NotificationsService(); this.http = new HttpService(); - this.basePath = new BasePathService(); this.uiSettings = new UiSettingsService(); this.overlay = new OverlayService(); this.application = new ApplicationService(); @@ -116,27 +114,14 @@ export class CoreSystem { const i18n = this.i18n.setup(); const injectedMetadata = this.injectedMetadata.setup(); this.fatalErrorsSetup = this.fatalErrors.setup({ injectedMetadata, i18n }); - const basePath = this.basePath.setup({ injectedMetadata }); - const http = this.http.setup({ - basePath, - injectedMetadata, - fatalErrors: this.fatalErrorsSetup, - }); - const uiSettings = this.uiSettings.setup({ - http, - injectedMetadata, - basePath, - }); - const notifications = this.notifications.setup({ uiSettings }); + const http = this.http.setup({ injectedMetadata, fatalErrors: this.fatalErrorsSetup }); + const uiSettings = this.uiSettings.setup({ http, injectedMetadata }); + const notifications = this.notifications.setup({ uiSettings, i18n }); const application = this.application.setup(); - const chrome = this.chrome.setup({ - injectedMetadata, - notifications, - }); + const chrome = this.chrome.setup({ injectedMetadata, notifications }); - const core: CoreSetup = { + const core: InternalCoreSetup = { application, - basePath, chrome, fatalErrors: this.fatalErrorsSetup, http, @@ -147,8 +132,8 @@ export class CoreSystem { }; // Services that do not expose contracts at setup - await this.plugins.setup(core); - await this.legacyPlatform.setup({ core }); + const plugins = await this.plugins.setup(core); + await this.legacyPlatform.setup({ core, plugins: mapToObject(plugins.contracts) }); return { fatalErrors: this.fatalErrorsSetup }; } catch (error) { @@ -165,11 +150,10 @@ export class CoreSystem { public async start() { try { const injectedMetadata = await this.injectedMetadata.start(); - const basePath = await this.basePath.start({ injectedMetadata }); - const http = await this.http.start(); + const http = await this.http.start({ injectedMetadata, fatalErrors: this.fatalErrorsSetup }); const i18n = await this.i18n.start(); - const application = await this.application.start({ basePath, injectedMetadata }); - const chrome = await this.chrome.start({ application, basePath }); + const application = await this.application.start({ injectedMetadata }); + const chrome = await this.chrome.start({ application, http }); const notificationsTargetDomElement = document.createElement('div'); const overlayTargetDomElement = document.createElement('div'); @@ -182,15 +166,15 @@ export class CoreSystem { this.rootDomElement.appendChild(legacyPlatformTargetDomElement); this.rootDomElement.appendChild(overlayTargetDomElement); + const overlays = this.overlay.start({ i18n, targetDomElement: overlayTargetDomElement }); const notifications = await this.notifications.start({ i18n, + overlays, targetDomElement: notificationsTargetDomElement, }); - const overlays = this.overlay.start({ i18n, targetDomElement: overlayTargetDomElement }); - const core: CoreStart = { + const core: InternalCoreStart = { application, - basePath, chrome, http, i18n, @@ -199,8 +183,12 @@ export class CoreSystem { overlays, }; - await this.plugins.start(core); - await this.legacyPlatform.start({ core, targetDomElement: legacyPlatformTargetDomElement }); + const plugins = await this.plugins.start(core); + await this.legacyPlatform.start({ + core, + plugins: mapToObject(plugins.contracts), + targetDomElement: legacyPlatformTargetDomElement, + }); } catch (error) { if (this.fatalErrorsSetup) { this.fatalErrorsSetup.add(error); diff --git a/src/core/public/fatal_errors/fatal_errors_service.tsx b/src/core/public/fatal_errors/fatal_errors_service.tsx index 92c7633cfb9f1..4a2c5cbd88419 100644 --- a/src/core/public/fatal_errors/fatal_errors_service.tsx +++ b/src/core/public/fatal_errors/fatal_errors_service.tsx @@ -23,7 +23,7 @@ import * as Rx from 'rxjs'; import { first, tap } from 'rxjs/operators'; import { I18nSetup } from '../i18n'; -import { InjectedMetadataSetup } from '../'; +import { InjectedMetadataSetup } from '../injected_metadata'; import { FatalErrorsScreen } from './fatal_errors_screen'; import { FatalErrorInfo, getErrorInfo } from './get_error_info'; diff --git a/src/core/public/http/fetch.ts b/src/core/public/http/fetch.ts deleted file mode 100644 index 9bfc13820cb55..0000000000000 --- a/src/core/public/http/fetch.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { merge } from 'lodash'; -import { format } from 'url'; - -import { HttpFetchOptions, HttpBody, Deps } from './types'; -import { HttpFetchError } from './http_fetch_error'; - -const JSON_CONTENT = /^(application\/(json|x-javascript)|text\/(x-)?javascript|x-json)(;.*)?$/; -const NDJSON_CONTENT = /^(application\/ndjson)(;.*)?$/; - -export const setup = ({ basePath, injectedMetadata }: Deps) => { - async function fetch(path: string, options: HttpFetchOptions = {}): Promise { - const { query, prependBasePath, ...fetchOptions } = merge( - { - method: 'GET', - credentials: 'same-origin', - prependBasePath: true, - headers: { - 'kbn-version': injectedMetadata.getKibanaVersion(), - 'Content-Type': 'application/json', - }, - }, - options - ); - const url = format({ - pathname: prependBasePath ? basePath.addToPath(path) : path, - query, - }); - - if ( - options.headers && - 'Content-Type' in options.headers && - options.headers['Content-Type'] === undefined - ) { - delete fetchOptions.headers['Content-Type']; - } - - let response; - let body = null; - - try { - response = await window.fetch(url, fetchOptions as RequestInit); - } catch (err) { - throw new HttpFetchError(err.message); - } - - const contentType = response.headers.get('Content-Type') || ''; - - try { - if (NDJSON_CONTENT.test(contentType)) { - body = await response.blob(); - } else if (JSON_CONTENT.test(contentType)) { - body = await response.json(); - } else { - body = await response.text(); - } - } catch (err) { - throw new HttpFetchError(err.message, response, body); - } - - if (!response.ok) { - throw new HttpFetchError(response.statusText, response, body); - } - - return body; - } - - function shorthand(method: string) { - return (path: string, options: HttpFetchOptions = {}) => fetch(path, { ...options, method }); - } - - return { fetch, shorthand }; -}; diff --git a/src/core/public/http/http_intercept_controller.ts b/src/core/public/http/http_intercept_controller.ts new file mode 100644 index 0000000000000..65585e6ff7e25 --- /dev/null +++ b/src/core/public/http/http_intercept_controller.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export class HttpInterceptController { + private _halted = false; + + get halted() { + return this._halted; + } + + halt() { + this._halted = true; + } +} diff --git a/src/core/public/http/http_intercept_halt_error.ts b/src/core/public/http/http_intercept_halt_error.ts new file mode 100644 index 0000000000000..856a912f63c82 --- /dev/null +++ b/src/core/public/http/http_intercept_halt_error.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export class HttpInterceptHaltError extends Error { + constructor() { + super('HTTP Intercept Halt'); + + // captureStackTrace is only available in the V8 engine, so any browser using + // a different JS engine won't have access to this method. + if (Error.captureStackTrace) { + Error.captureStackTrace(this, HttpInterceptHaltError); + } + } +} diff --git a/src/core/public/http/http_service.mock.ts b/src/core/public/http/http_service.mock.ts index 7893d250f0812..3f5611ba7f7df 100644 --- a/src/core/public/http/http_service.mock.ts +++ b/src/core/public/http/http_service.mock.ts @@ -17,9 +17,10 @@ * under the License. */ -import { HttpService, HttpSetup, HttpStart } from './http_service'; +import { HttpService } from './http_service'; +import { HttpSetup, HttpStart } from './types'; -const createSetupContractMock = (): jest.Mocked => ({ +const createServiceMock = () => ({ fetch: jest.fn(), get: jest.fn(), head: jest.fn(), @@ -28,10 +29,18 @@ const createSetupContractMock = (): jest.Mocked => ({ patch: jest.fn(), delete: jest.fn(), options: jest.fn(), + getBasePath: jest.fn(), + prependBasePath: jest.fn(), + removeBasePath: jest.fn(), addLoadingCount: jest.fn(), getLoadingCount$: jest.fn(), + stop: jest.fn(), + intercept: jest.fn(), + removeAllInterceptors: jest.fn(), }); -const createStartContractMock = (): jest.Mocked => undefined; + +const createSetupContractMock = (): jest.Mocked => createServiceMock(); +const createStartContractMock = (): jest.Mocked => createServiceMock(); const createMock = (): jest.Mocked> => ({ setup: jest.fn().mockReturnValue(createSetupContractMock()), start: jest.fn().mockReturnValue(createStartContractMock()), diff --git a/src/core/public/http/http_service.test.ts b/src/core/public/http/http_service.test.ts index 51f4af44d313d..d40152157ec93 100644 --- a/src/core/public/http/http_service.test.ts +++ b/src/core/public/http/http_service.test.ts @@ -21,34 +21,97 @@ import * as Rx from 'rxjs'; import { toArray } from 'rxjs/operators'; // @ts-ignore import fetchMock from 'fetch-mock/es5/client'; - -import { BasePathService } from '../base_path/base_path_service'; -import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.mock'; -import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; -import { HttpService } from './http_service'; import { readFileSync } from 'fs'; import { join } from 'path'; +import { setup, SetupTap } from '../../../test_utils/public/http_test_setup'; -function setupService() { - const httpService = new HttpService(); - const fatalErrors = fatalErrorsServiceMock.createSetupContract(); - const injectedMetadata = injectedMetadataServiceMock.createSetupContract(); +const setupFakeBasePath: SetupTap = injectedMetadata => { + injectedMetadata.getBasePath.mockReturnValue('/foo/bar'); +}; - injectedMetadata.getBasePath.mockReturnValueOnce('http://localhost/myBase'); +describe('getBasePath', () => { + it('returns an empty string if no basePath is injected', () => { + const { http } = setup(injectedMetadata => { + injectedMetadata.getBasePath.mockReturnValue(''); + }); - const basePath = new BasePathService().setup({ injectedMetadata }); - const http = httpService.setup({ basePath, fatalErrors, injectedMetadata }); + expect(http.getBasePath()).toBe(''); + }); - return { httpService, fatalErrors, http }; -} + it('returns the injected basePath', () => { + const { http } = setup(setupFakeBasePath); + + expect(http.getBasePath()).toBe('/foo/bar'); + }); +}); + +describe('prependBasePath', () => { + it('adds the base path to the path if it is relative and starts with a slash', () => { + const { http } = setup(setupFakeBasePath); + + expect(http.prependBasePath('/a/b')).toBe('/foo/bar/a/b'); + }); -describe('http requests', async () => { + it('leaves the query string and hash of path unchanged', () => { + const { http } = setup(setupFakeBasePath); + + expect(http.prependBasePath('/a/b?x=y#c/d/e')).toBe('/foo/bar/a/b?x=y#c/d/e'); + }); + + it('returns the path unchanged if it does not start with a slash', () => { + const { http } = setup(setupFakeBasePath); + + expect(http.prependBasePath('a/b')).toBe('a/b'); + }); + + it('returns the path unchanged it it has a hostname', () => { + const { http } = setup(setupFakeBasePath); + + expect(http.prependBasePath('http://localhost:5601/a/b')).toBe('http://localhost:5601/a/b'); + }); +}); + +describe('removeBasePath', () => { + it('removes the basePath if relative path starts with it', () => { + const { http } = setup(setupFakeBasePath); + + expect(http.removeBasePath('/foo/bar/a/b')).toBe('/a/b'); + }); + + it('leaves query string and hash intact', () => { + const { http } = setup(setupFakeBasePath); + + expect(http.removeBasePath('/foo/bar/a/b?c=y#1234')).toBe('/a/b?c=y#1234'); + }); + + it('ignores urls with hostnames', () => { + const { http } = setup(setupFakeBasePath); + + expect(http.removeBasePath('http://localhost:5601/foo/bar/a/b')).toBe( + 'http://localhost:5601/foo/bar/a/b' + ); + }); + + it('returns slash if path is just basePath', () => { + const { http } = setup(setupFakeBasePath); + + expect(http.removeBasePath('/foo/bar')).toBe('/'); + }); + + it('returns full path if basePath is not its own segment', () => { + const { http } = setup(setupFakeBasePath); + + expect(http.removeBasePath('/foo/barhop')).toBe('/foo/barhop'); + }); +}); + +describe('http requests', () => { afterEach(() => { fetchMock.restore(); }); it('should use supplied request method', async () => { - const { http } = setupService(); + const { http } = setup(); fetchMock.post('*', {}); await http.fetch('/my/path', { method: 'POST' }); @@ -57,18 +120,18 @@ describe('http requests', async () => { }); it('should use supplied Content-Type', async () => { - const { http } = setupService(); + const { http } = setup(); fetchMock.get('*', {}); await http.fetch('/my/path', { headers: { 'Content-Type': 'CustomContentType' } }); expect(fetchMock.lastOptions()!.headers).toMatchObject({ - 'Content-Type': 'CustomContentType', + 'content-type': 'CustomContentType', }); }); it('should use supplied pathname and querystring', async () => { - const { http } = setupService(); + const { http } = setup(); fetchMock.get('*', {}); await http.fetch('/my/path', { query: { a: 'b' } }); @@ -77,7 +140,7 @@ describe('http requests', async () => { }); it('should use supplied headers', async () => { - const { http } = setupService(); + const { http } = setup(); fetchMock.get('*', {}); await http.fetch('/my/path', { @@ -85,14 +148,14 @@ describe('http requests', async () => { }); expect(fetchMock.lastOptions()!.headers).toEqual({ - 'Content-Type': 'application/json', + 'content-type': 'application/json', 'kbn-version': 'kibanaVersion', - myHeader: 'foo', + myheader: 'foo', }); }); it('should return response', async () => { - const { http } = setupService(); + const { http } = setup(); fetchMock.get('*', { foo: 'bar' }); @@ -102,7 +165,7 @@ describe('http requests', async () => { }); it('should prepend url with basepath by default', async () => { - const { http } = setupService(); + const { http } = setup(); fetchMock.get('*', {}); await http.fetch('/my/path'); @@ -111,7 +174,7 @@ describe('http requests', async () => { }); it('should not prepend url with basepath when disabled', async () => { - const { http } = setupService(); + const { http } = setup(); fetchMock.get('*', {}); await http.fetch('my/path', { prependBasePath: false }); @@ -120,23 +183,25 @@ describe('http requests', async () => { }); it('should make request with defaults', async () => { - const { http } = setupService(); + const { http } = setup(); fetchMock.get('*', {}); await http.fetch('/my/path'); - expect(fetchMock.lastOptions()!).toMatchObject({ + const lastCall = fetchMock.lastCall(); + + expect(lastCall!.request.credentials).toBe('same-origin'); + expect(lastCall![1]).toMatchObject({ method: 'GET', - credentials: 'same-origin', headers: { - 'Content-Type': 'application/json', + 'content-type': 'application/json', 'kbn-version': 'kibanaVersion', }, }); }); it('should reject on network error', async () => { - const { http } = setupService(); + const { http } = setup(); expect.assertions(1); fetchMock.get('*', { status: 500 }); @@ -145,7 +210,7 @@ describe('http requests', async () => { }); it('should contain error message when throwing response', async () => { - const { http } = setupService(); + const { http } = setup(); fetchMock.get('*', { status: 404, body: { foo: 'bar' } }); @@ -162,7 +227,7 @@ describe('http requests', async () => { }); it('should support get() helper', async () => { - const { http } = setupService(); + const { http } = setup(); fetchMock.get('*', {}); await http.get('/my/path', { method: 'POST' }); @@ -171,7 +236,7 @@ describe('http requests', async () => { }); it('should support head() helper', async () => { - const { http } = setupService(); + const { http } = setup(); fetchMock.head('*', {}); await http.head('/my/path', { method: 'GET' }); @@ -180,7 +245,7 @@ describe('http requests', async () => { }); it('should support post() helper', async () => { - const { http } = setupService(); + const { http } = setup(); fetchMock.post('*', {}); await http.post('/my/path', { method: 'GET', body: '{}' }); @@ -189,7 +254,7 @@ describe('http requests', async () => { }); it('should support put() helper', async () => { - const { http } = setupService(); + const { http } = setup(); fetchMock.put('*', {}); await http.put('/my/path', { method: 'GET', body: '{}' }); @@ -198,7 +263,7 @@ describe('http requests', async () => { }); it('should support patch() helper', async () => { - const { http } = setupService(); + const { http } = setup(); fetchMock.patch('*', {}); await http.patch('/my/path', { method: 'GET', body: '{}' }); @@ -207,7 +272,7 @@ describe('http requests', async () => { }); it('should support delete() helper', async () => { - const { http } = setupService(); + const { http } = setup(); fetchMock.delete('*', {}); await http.delete('/my/path', { method: 'GET' }); @@ -216,7 +281,7 @@ describe('http requests', async () => { }); it('should support options() helper', async () => { - const { http } = setupService(); + const { http } = setup(); fetchMock.mock('*', { method: 'OPTIONS' }); await http.options('/my/path', { method: 'GET' }); @@ -225,7 +290,7 @@ describe('http requests', async () => { }); it('should make requests for NDJSON content', async () => { - const { http } = setupService(); + const { http } = setup(); const content = readFileSync(join(__dirname, '_import_objects.ndjson'), { encoding: 'utf-8' }); const body = new FormData(); @@ -250,9 +315,261 @@ describe('http requests', async () => { }); }); -describe('addLoadingCount()', async () => { +describe('interception', () => { + const { http } = setup(); + + beforeEach(() => { + fetchMock.get('*', { foo: 'bar' }); + }); + + afterEach(() => { + fetchMock.restore(); + http.removeAllInterceptors(); + }); + + it('should make request and receive response', async () => { + http.intercept({}); + + const body = await http.fetch('/my/path'); + + expect(fetchMock.called()).toBe(true); + expect(body).toEqual({ foo: 'bar' }); + }); + + it('should be able to manipulate request instance', async () => { + http.intercept({ + request(request) { + request.headers.set('Content-Type', 'CustomContentType'); + }, + }); + http.intercept({ + request(request) { + return new Request('/my/route', request); + }, + }); + + const body = await http.fetch('/my/path'); + + expect(fetchMock.called()).toBe(true); + expect(body).toEqual({ foo: 'bar' }); + expect(fetchMock.lastOptions()!.headers).toMatchObject({ + 'content-type': 'CustomContentType', + }); + expect(fetchMock.lastUrl()).toBe('/my/route'); + }); + + it('should call interceptors in correct order', async () => { + const order: string[] = []; + + http.intercept({ + request() { + order.push('Request 1'); + }, + response() { + order.push('Response 1'); + }, + }); + http.intercept({ + request() { + order.push('Request 2'); + }, + response() { + order.push('Response 2'); + }, + }); + http.intercept({ + request() { + order.push('Request 3'); + }, + response() { + order.push('Response 3'); + }, + }); + + const body = await http.fetch('/my/path'); + + expect(fetchMock.called()).toBe(true); + expect(body).toEqual({ foo: 'bar' }); + expect(order).toEqual([ + 'Request 3', + 'Request 2', + 'Request 1', + 'Response 1', + 'Response 2', + 'Response 3', + ]); + }); + + it('should skip remaining interceptors when controller halts during request', async () => { + const order: string[] = []; + + http.intercept({ + request() { + order.push('Request 1'); + }, + response() { + order.push('Response 1'); + }, + }); + http.intercept({ + request(request, controller) { + controller.halt(); + order.push('Request 2'); + }, + response() { + order.push('Response 2'); + }, + }); + http.intercept({ + request() { + order.push('Request 3'); + }, + response() { + order.push('Response 3'); + }, + }); + + await expect(http.fetch('/my/wat')).rejects.toThrow(/HTTP Intercept Halt/); + expect(fetchMock.called()).toBe(false); + expect(order).toEqual(['Request 3', 'Request 2']); + }); + + it('should skip remaining interceptors when controller halts during response', async () => { + const order: string[] = []; + + http.intercept({ + request() { + order.push('Request 1'); + }, + response(response, controller) { + controller.halt(); + order.push('Response 1'); + }, + }); + http.intercept({ + request() { + order.push('Request 2'); + }, + response() { + order.push('Response 2'); + }, + }); + http.intercept({ + request() { + order.push('Request 3'); + }, + response() { + order.push('Response 3'); + }, + }); + + await expect(http.fetch('/my/wat')).rejects.toThrow(/HTTP Intercept Halt/); + expect(fetchMock.called()).toBe(true); + expect(order).toEqual(['Request 3', 'Request 2', 'Request 1', 'Response 1']); + }); + + it('should not fetch if exception occurs during request interception', async () => { + const order: string[] = []; + + http.intercept({ + request() { + order.push('Request 1'); + }, + requestError() { + order.push('RequestError 1'); + }, + response() { + order.push('Response 1'); + }, + responseError() { + order.push('ResponseError 1'); + }, + }); + http.intercept({ + request() { + order.push('Request 2'); + throw new Error('Interception Error'); + }, + response() { + order.push('Response 2'); + }, + responseError() { + order.push('ResponseError 2'); + }, + }); + http.intercept({ + request() { + order.push('Request 3'); + }, + response() { + order.push('Response 3'); + }, + responseError() { + order.push('ResponseError 3'); + }, + }); + + await expect(http.fetch('/my/wat')).rejects.toThrow(/Interception Error/); + expect(fetchMock.called()).toBe(false); + expect(order).toEqual([ + 'Request 3', + 'Request 2', + 'RequestError 1', + 'ResponseError 1', + 'ResponseError 2', + 'ResponseError 3', + ]); + }); + + it('should succeed if request throws but caught by interceptor', async () => { + const order: string[] = []; + + http.intercept({ + request() { + order.push('Request 1'); + }, + requestError({ request }) { + order.push('RequestError 1'); + return new Request('/my/route', request); + }, + response() { + order.push('Response 1'); + }, + }); + http.intercept({ + request() { + order.push('Request 2'); + throw new Error('Interception Error'); + }, + response() { + order.push('Response 2'); + }, + }); + http.intercept({ + request() { + order.push('Request 3'); + }, + response() { + order.push('Response 3'); + }, + }); + + await expect(http.fetch('/my/route')).resolves.toEqual({ foo: 'bar' }); + expect(fetchMock.called()).toBe(true); + expect(order).toEqual([ + 'Request 3', + 'Request 2', + 'RequestError 1', + 'Response 1', + 'Response 2', + 'Response 3', + ]); + }); +}); + +describe('addLoadingCount()', () => { it('subscribes to passed in sources, unsubscribes on stop', () => { - const { httpService, http } = setupService(); + const { httpService, http } = setup(); const unsubA = jest.fn(); const subA = jest.fn().mockReturnValue(unsubA); @@ -275,23 +592,23 @@ describe('addLoadingCount()', async () => { }); it('adds a fatal error if source observables emit an error', async () => { - const { http, fatalErrors } = setupService(); + const { http, fatalErrors } = setup(); http.addLoadingCount(Rx.throwError(new Error('foo bar'))); expect(fatalErrors.add.mock.calls).toMatchSnapshot(); }); it('adds a fatal error if source observable emits a negative number', async () => { - const { http, fatalErrors } = setupService(); + const { http, fatalErrors } = setup(); http.addLoadingCount(Rx.of(1, 2, 3, 4, -9)); expect(fatalErrors.add.mock.calls).toMatchSnapshot(); }); }); -describe('getLoadingCount$()', async () => { +describe('getLoadingCount$()', () => { it('emits 0 initially, the right count when sources emit their own count, and ends with zero', async () => { - const { httpService, http } = setupService(); + const { httpService, http } = setup(); const countA$ = new Rx.Subject(); const countB$ = new Rx.Subject(); @@ -318,7 +635,7 @@ describe('getLoadingCount$()', async () => { }); it('only emits when loading count changes', async () => { - const { httpService, http } = setupService(); + const { httpService, http } = setup(); const count$ = new Rx.Subject(); const promise = http diff --git a/src/core/public/http/http_service.ts b/src/core/public/http/http_service.ts index a54e6947a1980..3bff861441fca 100644 --- a/src/core/public/http/http_service.ts +++ b/src/core/public/http/http_service.ts @@ -17,84 +17,25 @@ * under the License. */ -import * as Rx from 'rxjs'; -import { - distinctUntilChanged, - endWith, - map, - pairwise, - startWith, - takeUntil, - tap, -} from 'rxjs/operators'; - -import { Deps } from './types'; -import { setup } from './fetch'; +import { HttpDeps, HttpSetup, HttpStart, HttpServiceBase } from './types'; +import { setup } from './http_setup'; /** @internal */ export class HttpService { - private readonly loadingCount$ = new Rx.BehaviorSubject(0); - private readonly stop$ = new Rx.Subject(); - - public setup(deps: Deps) { - const { fetch, shorthand } = setup(deps); - - return { - fetch, - delete: shorthand('DELETE'), - get: shorthand('GET'), - head: shorthand('HEAD'), - options: shorthand('OPTIONS'), - patch: shorthand('PATCH'), - post: shorthand('POST'), - put: shorthand('PUT'), - addLoadingCount: (count$: Rx.Observable) => { - count$ - .pipe( - distinctUntilChanged(), - - tap(count => { - if (count < 0) { - throw new Error( - 'Observables passed to loadingCount.add() must only emit positive numbers' - ); - } - }), + private service!: HttpServiceBase; - // use takeUntil() so that we can finish each stream on stop() the same way we do when they complete, - // by removing the previous count from the total - takeUntil(this.stop$), - endWith(0), - startWith(0), - pairwise(), - map(([prev, next]) => next - prev) - ) - .subscribe({ - next: delta => { - this.loadingCount$.next(this.loadingCount$.getValue() + delta); - }, - error: error => { - deps.fatalErrors.add(error); - }, - }); - }, - - getLoadingCount$: () => { - return this.loadingCount$.pipe(distinctUntilChanged()); - }, - }; + public setup(deps: HttpDeps): HttpSetup { + this.service = setup(deps.injectedMetadata, deps.fatalErrors); + return this.service; } - // eslint-disable-next-line no-unused-params - public start() {} + public start(deps: HttpDeps): HttpStart { + return this.service || this.setup(deps); + } public stop() { - this.stop$.next(); - this.loadingCount$.complete(); + if (this.service) { + this.service.stop(); + } } } - -/** @public */ -export type HttpSetup = ReturnType; -/** @public */ -export type HttpStart = ReturnType; diff --git a/src/core/public/http/http_setup.ts b/src/core/public/http/http_setup.ts new file mode 100644 index 0000000000000..c12ec4be5d3a3 --- /dev/null +++ b/src/core/public/http/http_setup.ts @@ -0,0 +1,333 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { BehaviorSubject, Observable, Subject } from 'rxjs'; +import { + distinctUntilChanged, + endWith, + map, + pairwise, + startWith, + takeUntil, + tap, +} from 'rxjs/operators'; +import { merge } from 'lodash'; +import { format } from 'url'; +import { InjectedMetadataSetup } from '../injected_metadata'; +import { FatalErrorsSetup } from '../fatal_errors'; +import { modifyUrl } from '../utils'; +import { HttpFetchOptions, HttpServiceBase, HttpInterceptor, HttpResponse } from './types'; +import { HttpInterceptController } from './http_intercept_controller'; +import { HttpFetchError } from './http_fetch_error'; +import { HttpInterceptHaltError } from './http_intercept_halt_error'; + +const JSON_CONTENT = /^(application\/(json|x-javascript)|text\/(x-)?javascript|x-json)(;.*)?$/; +const NDJSON_CONTENT = /^(application\/ndjson)(;.*)?$/; + +export const setup = ( + injectedMetadata: InjectedMetadataSetup, + fatalErrors: FatalErrorsSetup | null +): HttpServiceBase => { + const loadingCount$ = new BehaviorSubject(0); + const stop$ = new Subject(); + const interceptors = new Set(); + const kibanaVersion = injectedMetadata.getKibanaVersion(); + const basePath = injectedMetadata.getBasePath() || ''; + + function intercept(interceptor: HttpInterceptor) { + interceptors.add(interceptor); + + return () => interceptors.delete(interceptor); + } + + function removeAllInterceptors() { + interceptors.clear(); + } + + function prependBasePath(path: string): string { + return modifyUrl(path, parts => { + if (!parts.hostname && parts.pathname && parts.pathname.startsWith('/')) { + parts.pathname = `${basePath}${parts.pathname}`; + } + }); + } + + function createRequest(path: string, options?: HttpFetchOptions) { + const { query, prependBasePath: shouldPrependBasePath, ...fetchOptions } = merge( + { + method: 'GET', + credentials: 'same-origin', + prependBasePath: true, + headers: { + 'kbn-version': kibanaVersion, + 'Content-Type': 'application/json', + }, + }, + options || {} + ); + const url = format({ + pathname: shouldPrependBasePath ? prependBasePath(path) : path, + query, + }); + + if ( + options && + options.headers && + 'Content-Type' in options.headers && + options.headers['Content-Type'] === undefined + ) { + delete fetchOptions.headers['Content-Type']; + } + + return new Request(url, fetchOptions); + } + + // Request/response interceptors are called in opposite orders. + // Request hooks start from the newest interceptor and end with the oldest. + function interceptRequest( + request: Request, + controller: HttpInterceptController + ): Promise { + let next = request; + + return [...interceptors].reduceRight( + (promise, interceptor) => + promise.then( + async (current: Request) => { + if (controller.halted) { + throw new HttpInterceptHaltError(); + } + + if (!interceptor.request) { + return current; + } + + next = (await interceptor.request(current, controller)) || current; + + return next; + }, + async error => { + if (error instanceof HttpInterceptHaltError) { + throw error; + } else if (controller.halted) { + throw new HttpInterceptHaltError(); + } + + if (!interceptor.requestError) { + throw error; + } + + const nextRequest = await interceptor.requestError( + { error, request: next }, + controller + ); + + if (!nextRequest) { + throw error; + } + + next = nextRequest; + return next; + } + ), + Promise.resolve(request) + ); + } + + // Response hooks start from the oldest interceptor and end with the newest. + async function interceptResponse( + responsePromise: Promise, + controller: HttpInterceptController + ) { + let current: HttpResponse; + + const finalHttpResponse = await [...interceptors].reduce( + (promise, interceptor) => + promise.then( + async httpResponse => { + if (controller.halted) { + throw new HttpInterceptHaltError(); + } + + if (!interceptor.response) { + return httpResponse; + } + + current = (await interceptor.response(httpResponse, controller)) || httpResponse; + + return current; + }, + async error => { + if (error instanceof HttpInterceptHaltError) { + throw error; + } else if (controller.halted) { + throw new HttpInterceptHaltError(); + } + + if (!interceptor.responseError) { + throw error; + } + + const next = await interceptor.responseError({ ...current, error }, controller); + + if (!next) { + throw error; + } + + return next; + } + ), + responsePromise + ); + + return finalHttpResponse.body; + } + + async function fetcher(request: Request): Promise { + let response; + let body = null; + + try { + response = await window.fetch(request); + } catch (err) { + throw new HttpFetchError(err.message); + } + + const contentType = response.headers.get('Content-Type') || ''; + + try { + if (NDJSON_CONTENT.test(contentType)) { + body = await response.blob(); + } else if (JSON_CONTENT.test(contentType)) { + body = await response.json(); + } else { + const text = await response.text(); + + try { + body = JSON.parse(text); + } catch (err) { + body = text; + } + } + } catch (err) { + throw new HttpFetchError(err.message, response, body); + } + + if (!response.ok) { + throw new HttpFetchError(response.statusText, response, body); + } + + return { response, body, request }; + } + + function fetch(path: string, options: HttpFetchOptions = {}) { + const controller = new HttpInterceptController(); + const initialRequest = createRequest(path, options); + + return interceptResponse( + interceptRequest(initialRequest, controller).then(fetcher), + controller + ); + } + + function shorthand(method: string) { + return (path: string, options: HttpFetchOptions = {}) => fetch(path, { ...options, method }); + } + + function stop() { + stop$.next(); + loadingCount$.complete(); + } + + function getBasePath() { + return basePath; + } + + function removeBasePath(path: string): string { + if (!basePath) { + return path; + } + + if (path === basePath) { + return '/'; + } + + if (path.startsWith(`${basePath}/`)) { + return path.slice(basePath.length); + } + + return path; + } + + function addLoadingCount(count$: Observable) { + count$ + .pipe( + distinctUntilChanged(), + + tap(count => { + if (count < 0) { + throw new Error( + 'Observables passed to loadingCount.add() must only emit positive numbers' + ); + } + }), + + // use takeUntil() so that we can finish each stream on stop() the same way we do when they complete, + // by removing the previous count from the total + takeUntil(stop$), + endWith(0), + startWith(0), + pairwise(), + map(([prev, next]) => next - prev) + ) + .subscribe({ + next: delta => { + loadingCount$.next(loadingCount$.getValue() + delta); + }, + error: error => { + if (fatalErrors) { + fatalErrors.add(error); + } + }, + }); + } + + function getLoadingCount$() { + return loadingCount$.pipe(distinctUntilChanged()); + } + + return { + stop, + getBasePath, + prependBasePath, + removeBasePath, + intercept, + removeAllInterceptors, + fetch, + delete: shorthand('DELETE'), + get: shorthand('GET'), + head: shorthand('HEAD'), + options: shorthand('OPTIONS'), + patch: shorthand('PATCH'), + post: shorthand('POST'), + put: shorthand('PUT'), + addLoadingCount, + getLoadingCount$, + }; +}; diff --git a/src/core/public/http/index.ts b/src/core/public/http/index.ts index d7b2efe9b6cd0..3edbbc2e362a4 100644 --- a/src/core/public/http/index.ts +++ b/src/core/public/http/index.ts @@ -17,5 +17,8 @@ * under the License. */ -export { HttpService, HttpSetup, HttpStart } from './http_service'; +export { HttpService } from './http_service'; export { HttpFetchError } from './http_fetch_error'; +export { HttpInterceptHaltError } from './http_intercept_halt_error'; +export { HttpInterceptController } from './http_intercept_controller'; +export * from './types'; diff --git a/src/core/public/http/types.ts b/src/core/public/http/types.ts index 05f6ab502246d..9f28d03e4e5af 100644 --- a/src/core/public/http/types.ts +++ b/src/core/public/http/types.ts @@ -17,13 +17,40 @@ * under the License. */ -import { BasePathSetup } from '../base_path'; +import { Observable } from 'rxjs'; import { InjectedMetadataSetup } from '../injected_metadata'; import { FatalErrorsSetup } from '../fatal_errors'; +import { HttpInterceptController } from './http_intercept_controller'; +import { HttpFetchError } from './http_fetch_error'; +/** @public */ +export interface HttpServiceBase { + stop(): void; + getBasePath(): string; + prependBasePath(path: string): string; + removeBasePath(path: string): string; + intercept(interceptor: HttpInterceptor): () => void; + removeAllInterceptors(): void; + fetch: HttpHandler; + delete: HttpHandler; + get: HttpHandler; + head: HttpHandler; + options: HttpHandler; + patch: HttpHandler; + post: HttpHandler; + put: HttpHandler; + addLoadingCount(count$: Observable): void; + getLoadingCount$(): Observable; +} +/** @public */ +export type HttpSetup = HttpServiceBase; +/** @public */ +export type HttpStart = HttpServiceBase; +/** @public */ export interface HttpHeadersInit { [name: string]: any; } +/** @public */ export interface HttpRequestInit { body?: BodyInit | null; cache?: RequestCache; @@ -39,17 +66,56 @@ export interface HttpRequestInit { signal?: AbortSignal | null; window?: any; } -export interface Deps { - basePath: BasePathSetup; +/** @public */ +export interface HttpDeps { injectedMetadata: InjectedMetadataSetup; - fatalErrors: FatalErrorsSetup; + fatalErrors: FatalErrorsSetup | null; } +/** @public */ export interface HttpFetchQuery { [key: string]: string | number | boolean | undefined; } +/** @public */ export interface HttpFetchOptions extends HttpRequestInit { query?: HttpFetchQuery; prependBasePath?: boolean; headers?: HttpHeadersInit; } -export type HttpBody = BodyInit | null; +/** @public */ +export type HttpHandler = (path: string, options?: HttpFetchOptions) => Promise; +/** @public */ +export type HttpBody = BodyInit | null | any; +/** @public */ +export interface HttpResponse { + request: Request; + response?: Response; + body?: HttpBody; +} +/** @public */ +export interface HttpErrorResponse extends HttpResponse { + error: Error | HttpFetchError; +} +/** @public */ +export interface HttpErrorRequest { + request?: Request; + error: Error; +} +/** @public */ +export interface HttpInterceptor { + request?( + request: Request, + controller: HttpInterceptController + ): Promise | Request | void; + requestError?( + httpErrorRequest: HttpErrorRequest, + controller: HttpInterceptController + ): Promise | Request | void; + response?( + httpResponse: HttpResponse, + controller: HttpInterceptController + ): Promise | HttpResponse | void; + responseError?( + httpErrorResponse: HttpErrorResponse, + controller: HttpInterceptController + ): Promise | HttpResponse | void; +} diff --git a/src/core/public/index.ts b/src/core/public/index.ts index c99ff82fa5600..4e50955af8676 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -17,7 +17,24 @@ * under the License. */ -import { BasePathSetup, BasePathStart } from './base_path'; +/** + * The Kibana Core APIs for client-side plugins. + * + * A plugin's `public/index` file must contain a named import, `plugin`, that + * implements {@link PluginInitializer} which returns an object that implements + * {@link Plugin}. + * + * The plugin integrates with the core system via lifecycle events: `setup`, + * `start`, and `stop`. In each lifecycle method, the plugin will receive the + * corresponding core services available (either {@link CoreSetup} or + * {@link CoreStart}) and any interfaces returned by dependency plugins' + * lifecycle method. Anything returned by the plugin's lifecycle method will be + * exposed to downstream dependencies when their corresponding lifecycle methods + * are invoked. + * + * @packageDocumentation + */ + import { ChromeBadge, ChromeBrand, @@ -28,36 +45,27 @@ import { ChromeStart, } from './chrome'; import { FatalErrorsSetup, FatalErrorInfo } from './fatal_errors'; -import { HttpSetup, HttpStart } from './http'; +import { HttpServiceBase, HttpSetup, HttpStart, HttpInterceptor } from './http'; import { I18nSetup, I18nStart } from './i18n'; +import { InjectedMetadataSetup, InjectedMetadataStart, LegacyNavLink } from './injected_metadata'; import { - InjectedMetadataParams, - InjectedMetadataSetup, - InjectedMetadataStart, - LegacyNavLink, -} from './injected_metadata'; -import { + ErrorToastOptions, NotificationsSetup, + NotificationsStart, Toast, ToastInput, ToastsApi, - NotificationsStart, } from './notifications'; import { OverlayRef, OverlayStart } from './overlays'; -import { - Plugin, - PluginInitializer, - PluginInitializerContext, - PluginSetupContext, - PluginStartContext, -} from './plugins'; +import { Plugin, PluginInitializer, PluginInitializerContext } from './plugins'; import { UiSettingsClient, UiSettingsSetup, UiSettingsState } from './ui_settings'; import { ApplicationSetup, Capabilities, ApplicationStart } from './application'; export { CoreContext, CoreSystem } from './core_system'; +export { RecursiveReadonly } from '../utils'; /** - * Core services exposed to the setup lifecycle + * Core services exposed to the `Plugin` setup lifecycle * * @public * @@ -66,28 +74,22 @@ export { CoreContext, CoreSystem } from './core_system'; * https://github.com/Microsoft/web-build-tools/issues/1237 */ export interface CoreSetup { - /** {@link ApplicationSetup} */ - application: ApplicationSetup; - /** {@link I18nSetup} */ - i18n: I18nSetup; - /** {@link InjectedMetadataSetup} */ - injectedMetadata: InjectedMetadataSetup; + /** {@link ChromeSetup} */ + chrome: ChromeSetup; /** {@link FatalErrorsSetup} */ fatalErrors: FatalErrorsSetup; - /** {@link NotificationsSetup} */ - notifications: NotificationsSetup; /** {@link HttpSetup} */ http: HttpSetup; - /** {@link BasePathSetup} */ - basePath: BasePathSetup; + /** {@link I18nSetup} */ + i18n: I18nSetup; + /** {@link NotificationsSetup} */ + notifications: NotificationsSetup; /** {@link UiSettingsSetup} */ uiSettings: UiSettingsSetup; - /** {@link ChromeSetup} */ - chrome: ChromeSetup; } /** - * Core services exposed to the start lifecycle + * Core services exposed to the `Plugin` start lifecycle * * @public * @@ -97,30 +99,39 @@ export interface CoreSetup { */ export interface CoreStart { /** {@link ApplicationStart} */ - application: ApplicationStart; - /** {@link BasePathStart} */ - basePath: BasePathStart; + application: Pick; /** {@link ChromeStart} */ chrome: ChromeStart; /** {@link HttpStart} */ http: HttpStart; /** {@link I18nStart} */ i18n: I18nStart; - /** {@link InjectedMetadataStart} */ - injectedMetadata: InjectedMetadataStart; /** {@link NotificationsStart} */ notifications: NotificationsStart; /** {@link OverlayStart} */ overlays: OverlayStart; } +/** @internal */ +export interface InternalCoreSetup extends CoreSetup { + application: ApplicationSetup; + injectedMetadata: InjectedMetadataSetup; +} + +/** @internal */ +export interface InternalCoreStart extends CoreStart { + application: ApplicationStart; + injectedMetadata: InjectedMetadataStart; +} + export { ApplicationSetup, ApplicationStart, - BasePathSetup, - BasePathStart, + HttpServiceBase, HttpSetup, HttpStart, + HttpInterceptor, + ErrorToastOptions, FatalErrorsSetup, FatalErrorInfo, Capabilities, @@ -133,15 +144,10 @@ export { ChromeNavLink, I18nSetup, I18nStart, - InjectedMetadataSetup, - InjectedMetadataStart, - InjectedMetadataParams, LegacyNavLink, Plugin, PluginInitializer, PluginInitializerContext, - PluginSetupContext, - PluginStartContext, NotificationsSetup, NotificationsStart, OverlayRef, diff --git a/src/core/public/injected_metadata/injected_metadata_service.mock.ts b/src/core/public/injected_metadata/injected_metadata_service.mock.ts index 39c40cb8a4bfd..6949936bc9a31 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.mock.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.mock.ts @@ -22,6 +22,7 @@ const createSetupContractMock = () => { const setupContract: jest.Mocked = { getBasePath: jest.fn(), getKibanaVersion: jest.fn(), + getCapabilities: jest.fn(), getCspConfig: jest.fn(), getLegacyMetadata: jest.fn(), getPlugins: jest.fn(), @@ -29,6 +30,7 @@ const createSetupContractMock = () => { getInjectedVars: jest.fn(), getKibanaBuildNumber: jest.fn(), }; + setupContract.getCapabilities.mockReturnValue({} as any); setupContract.getCspConfig.mockReturnValue({ warnLegacyBrowsers: true }); setupContract.getKibanaVersion.mockReturnValue('kibanaVersion'); setupContract.getLegacyMetadata.mockReturnValue({ diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index de7ad1595ed36..419dc9f498a2c 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -20,7 +20,8 @@ import { get } from 'lodash'; import { DiscoveredPlugin, PluginName } from '../../server'; import { UiSettingsState } from '../ui_settings'; -import { deepFreeze } from '../utils/deep_freeze'; +import { deepFreeze } from '../../utils/'; +import { Capabilities } from '..'; /** @public */ export interface LegacyNavLink { @@ -48,6 +49,7 @@ export interface InjectedMetadataParams { id: PluginName; plugin: DiscoveredPlugin; }>; + capabilities: Capabilities; legacyMetadata: { app: unknown; translations: unknown; @@ -97,6 +99,10 @@ export class InjectedMetadataService { return this.state.version; }, + getCapabilities: () => { + return this.state.capabilities; + }, + getCspConfig: () => { return this.state.csp; }, @@ -127,12 +133,13 @@ export class InjectedMetadataService { /** * Provides access to the metadata injected by the server into the page * - * @public + * @internal */ export interface InjectedMetadataSetup { getBasePath: () => string; getKibanaBuildNumber: () => number; getKibanaVersion: () => string; + getCapabilities: () => Capabilities; getCspConfig: () => { warnLegacyBrowsers: boolean; }; @@ -166,5 +173,5 @@ export interface InjectedMetadataSetup { }; } -/** @public */ +/** @internal */ export type InjectedMetadataStart = InjectedMetadataSetup; diff --git a/src/core/public/legacy/__snapshots__/legacy_service.test.ts.snap b/src/core/public/legacy/__snapshots__/legacy_service.test.ts.snap index e76bcf7725d17..97629fdd1add5 100644 --- a/src/core/public/legacy/__snapshots__/legacy_service.test.ts.snap +++ b/src/core/public/legacy/__snapshots__/legacy_service.test.ts.snap @@ -1,84 +1,18 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`#setup() load order useLegacyTestHarness = false loads ui/modules before ui/chrome, and both before legacy files 1`] = ` -Array [ - "ui/metadata", - "ui/i18n", - "ui/notify/fatal_error", - "ui/notify/toasts", - "ui/chrome/api/loading_count", - "ui/chrome/api/base_path", - "ui/chrome/api/ui_settings", - "ui/chrome/api/injected_vars", - "ui/chrome/api/controls", - "ui/chrome/api/help_extension", - "ui/chrome/api/theme", - "ui/chrome/api/breadcrumbs", - "ui/chrome/services/global_nav_state", - "ui/chrome", - "legacy files", -] -`; - -exports[`#setup() load order useLegacyTestHarness = true loads ui/modules before ui/test_harness, and both before legacy files 1`] = ` -Array [ - "ui/metadata", - "ui/i18n", - "ui/notify/fatal_error", - "ui/notify/toasts", - "ui/chrome/api/loading_count", - "ui/chrome/api/base_path", - "ui/chrome/api/ui_settings", - "ui/chrome/api/injected_vars", - "ui/chrome/api/controls", - "ui/chrome/api/help_extension", - "ui/chrome/api/theme", - "ui/chrome/api/breadcrumbs", - "ui/chrome/services/global_nav_state", - "ui/test_harness", - "legacy files", -] -`; - exports[`#start() load order useLegacyTestHarness = false loads ui/modules before ui/chrome, and both before legacy files 1`] = ` Array [ - "ui/metadata", - "ui/i18n", - "ui/notify/fatal_error", - "ui/notify/toasts", - "ui/chrome/api/loading_count", - "ui/chrome/api/base_path", - "ui/chrome/api/ui_settings", - "ui/chrome/api/injected_vars", - "ui/chrome/api/controls", - "ui/chrome/api/help_extension", - "ui/chrome/api/theme", - "ui/chrome/api/breadcrumbs", - "ui/chrome/services/global_nav_state", + "ui/new_platform", "ui/chrome", "legacy files", - "ui/capabilities", ] `; exports[`#start() load order useLegacyTestHarness = true loads ui/modules before ui/test_harness, and both before legacy files 1`] = ` Array [ - "ui/metadata", - "ui/i18n", - "ui/notify/fatal_error", - "ui/notify/toasts", - "ui/chrome/api/loading_count", - "ui/chrome/api/base_path", - "ui/chrome/api/ui_settings", - "ui/chrome/api/injected_vars", - "ui/chrome/api/controls", - "ui/chrome/api/help_extension", - "ui/chrome/api/theme", - "ui/chrome/api/breadcrumbs", - "ui/chrome/services/global_nav_state", + "ui/new_platform", "ui/test_harness", "legacy files", - "ui/capabilities", ] `; diff --git a/src/core/public/legacy/legacy_service.test.ts b/src/core/public/legacy/legacy_service.test.ts index e2cdcfe71b091..01b2781311086 100644 --- a/src/core/public/legacy/legacy_service.test.ts +++ b/src/core/public/legacy/legacy_service.test.ts @@ -21,11 +21,13 @@ import angular from 'angular'; const mockLoadOrder: string[] = []; -const mockUiMetadataInit = jest.fn(); -jest.mock('ui/metadata', () => { - mockLoadOrder.push('ui/metadata'); +const mockUiNewPlatformSetup = jest.fn(); +const mockUiNewPlatformStart = jest.fn(); +jest.mock('ui/new_platform', () => { + mockLoadOrder.push('ui/new_platform'); return { - __newPlatformSetup__: mockUiMetadataInit, + __setup__: mockUiNewPlatformSetup, + __start__: mockUiNewPlatformStart, }; }); @@ -45,111 +47,6 @@ jest.mock('ui/test_harness', () => { }; }); -const mockI18nContextInit = jest.fn(); -jest.mock('ui/i18n', () => { - mockLoadOrder.push('ui/i18n'); - return { - __newPlatformSetup__: mockI18nContextInit, - }; -}); - -const mockUICapabilitiesInit = jest.fn(); -jest.mock('ui/capabilities', () => { - mockLoadOrder.push('ui/capabilities'); - return { - __newPlatformStart__: mockUICapabilitiesInit, - }; -}); - -const mockFatalErrorInit = jest.fn(); -jest.mock('ui/notify/fatal_error', () => { - mockLoadOrder.push('ui/notify/fatal_error'); - return { - __newPlatformSetup__: mockFatalErrorInit, - }; -}); - -const mockNotifyToastsInit = jest.fn(); -jest.mock('ui/notify/toasts', () => { - mockLoadOrder.push('ui/notify/toasts'); - return { - __newPlatformSetup__: mockNotifyToastsInit, - }; -}); - -const mockHttpInit = jest.fn(); -jest.mock('ui/chrome/api/loading_count', () => { - mockLoadOrder.push('ui/chrome/api/loading_count'); - return { - __newPlatformSetup__: mockHttpInit, - }; -}); - -const mockBasePathInit = jest.fn(); -jest.mock('ui/chrome/api/base_path', () => { - mockLoadOrder.push('ui/chrome/api/base_path'); - return { - __newPlatformSetup__: mockBasePathInit, - }; -}); - -const mockUiSettingsInit = jest.fn(); -jest.mock('ui/chrome/api/ui_settings', () => { - mockLoadOrder.push('ui/chrome/api/ui_settings'); - return { - __newPlatformSetup__: mockUiSettingsInit, - }; -}); - -const mockInjectedVarsInit = jest.fn(); -jest.mock('ui/chrome/api/injected_vars', () => { - mockLoadOrder.push('ui/chrome/api/injected_vars'); - return { - __newPlatformSetup__: mockInjectedVarsInit, - }; -}); - -const mockChromeControlsInit = jest.fn(); -jest.mock('ui/chrome/api/controls', () => { - mockLoadOrder.push('ui/chrome/api/controls'); - return { - __newPlatformSetup__: mockChromeControlsInit, - }; -}); - -const mockChromeHelpExtensionInit = jest.fn(); -jest.mock('ui/chrome/api/help_extension', () => { - mockLoadOrder.push('ui/chrome/api/help_extension'); - return { - __newPlatformSetup__: mockChromeHelpExtensionInit, - }; -}); - -const mockChromeThemeInit = jest.fn(); -jest.mock('ui/chrome/api/theme', () => { - mockLoadOrder.push('ui/chrome/api/theme'); - return { - __newPlatformSetup__: mockChromeThemeInit, - }; -}); - -const mockChromeBreadcrumbsInit = jest.fn(); -jest.mock('ui/chrome/api/breadcrumbs', () => { - mockLoadOrder.push('ui/chrome/api/breadcrumbs'); - return { - __newPlatformSetup__: mockChromeBreadcrumbsInit, - }; -}); - -const mockGlobalNavStateInit = jest.fn(); -jest.mock('ui/chrome/services/global_nav_state', () => { - mockLoadOrder.push('ui/chrome/services/global_nav_state'); - return { - __newPlatformSetup__: mockGlobalNavStateInit, - }; -}); - -import { basePathServiceMock } from '../base_path/base_path_service.mock'; import { chromeServiceMock } from '../chrome/chrome_service.mock'; import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.mock'; import { httpServiceMock } from '../http/http_service.mock'; @@ -162,7 +59,6 @@ import { LegacyPlatformService } from './legacy_service'; import { applicationServiceMock } from '../application/application_service.mock'; const applicationSetup = applicationServiceMock.createSetupContract(); -const basePathSetup = basePathServiceMock.createSetupContract(); const chromeSetup = chromeServiceMock.createSetupContract(); const fatalErrorsSetup = fatalErrorsServiceMock.createSetupContract(); const httpSetup = httpServiceMock.createSetupContract(); @@ -185,17 +81,16 @@ const defaultSetupDeps = { injectedMetadata: injectedMetadataSetup, notifications: notificationsSetup, http: httpSetup, - basePath: basePathSetup, uiSettings: uiSettingsSetup, chrome: chromeSetup, }, + plugins: {}, }; const applicationStart = applicationServiceMock.createStartContract(); -const basePathStart = basePathServiceMock.createStartContract(); +const httpStart = httpServiceMock.createStartContract(); const chromeStart = chromeServiceMock.createStartContract(); const i18nStart = i18nServiceMock.createStartContract(); -const httpStart = httpServiceMock.createStartContract(); const injectedMetadataStart = injectedMetadataServiceMock.createStartContract(); const notificationsStart = notificationServiceMock.createStartContract(); const overlayStart = overlayServiceMock.createStartContract(); @@ -203,15 +98,15 @@ const overlayStart = overlayServiceMock.createStartContract(); const defaultStartDeps = { core: { application: applicationStart, - basePath: basePathStart, + http: httpStart, chrome: chromeStart, i18n: i18nStart, - http: httpStart, injectedMetadata: injectedMetadataStart, notifications: notificationsStart, overlays: overlayStart, }, targetDomElement: document.createElement('div'), + plugins: {}, }; afterEach(() => { @@ -222,187 +117,21 @@ afterEach(() => { describe('#setup()', () => { describe('default', () => { - it('passes legacy metadata from injectedVars to ui/metadata', () => { - const legacyMetadata = { nav: [], isLegacyMetadata: true }; - injectedMetadataSetup.getLegacyMetadata.mockReturnValueOnce(legacyMetadata as any); - - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - }); - - legacyPlatform.setup(defaultSetupDeps); - - expect(mockUiMetadataInit).toHaveBeenCalledTimes(1); - expect(mockUiMetadataInit).toHaveBeenCalledWith(legacyMetadata); - }); - - it('passes i18n.Context to ui/i18n', () => { - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - }); - - legacyPlatform.setup(defaultSetupDeps); - - expect(mockI18nContextInit).toHaveBeenCalledTimes(1); - expect(mockI18nContextInit).toHaveBeenCalledWith(i18nSetup.Context); - }); - - it('passes fatalErrors service to ui/notify/fatal_errors', () => { - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - }); - - legacyPlatform.setup(defaultSetupDeps); - - expect(mockFatalErrorInit).toHaveBeenCalledTimes(1); - expect(mockFatalErrorInit).toHaveBeenCalledWith(fatalErrorsSetup); - }); - - it('passes toasts service to ui/notify/toasts', () => { - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - }); - - legacyPlatform.setup(defaultSetupDeps); - - expect(mockNotifyToastsInit).toHaveBeenCalledTimes(1); - expect(mockNotifyToastsInit).toHaveBeenCalledWith(notificationsSetup.toasts); - }); - - it('passes http service to ui/chrome/api/loading_count', () => { - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - }); - - legacyPlatform.setup(defaultSetupDeps); - - expect(mockHttpInit).toHaveBeenCalledTimes(1); - expect(mockHttpInit).toHaveBeenCalledWith(httpSetup); - }); - - it('passes basePath service to ui/chrome/api/base_path', () => { - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - }); - - legacyPlatform.setup(defaultSetupDeps); - - expect(mockBasePathInit).toHaveBeenCalledTimes(1); - expect(mockBasePathInit).toHaveBeenCalledWith(basePathSetup); - }); - - it('passes basePath service to ui/chrome/api/ui_settings', () => { - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - }); - - legacyPlatform.setup(defaultSetupDeps); - - expect(mockUiSettingsInit).toHaveBeenCalledTimes(1); - expect(mockUiSettingsInit).toHaveBeenCalledWith(uiSettingsSetup); - }); - - it('passes injectedMetadata service to ui/chrome/api/injected_vars', () => { - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - }); - - legacyPlatform.setup(defaultSetupDeps); - - expect(mockInjectedVarsInit).toHaveBeenCalledTimes(1); - expect(mockInjectedVarsInit).toHaveBeenCalledWith(injectedMetadataSetup); - }); - - it('passes chrome service to ui/chrome/api/controls', () => { - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - }); - - legacyPlatform.setup(defaultSetupDeps); - - expect(mockChromeControlsInit).toHaveBeenCalledTimes(1); - expect(mockChromeControlsInit).toHaveBeenCalledWith(chromeSetup); - }); - - it('passes chrome service to ui/chrome/api/help_extension', () => { - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - }); - - legacyPlatform.setup(defaultSetupDeps); - - expect(mockChromeHelpExtensionInit).toHaveBeenCalledTimes(1); - expect(mockChromeHelpExtensionInit).toHaveBeenCalledWith(chromeSetup); - }); - - it('passes chrome service to ui/chrome/api/theme', () => { - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - }); - - legacyPlatform.setup(defaultSetupDeps); - - expect(mockChromeThemeInit).toHaveBeenCalledTimes(1); - expect(mockChromeThemeInit).toHaveBeenCalledWith(chromeSetup); - }); - - it('passes chrome service to ui/chrome/api/breadcrumbs', () => { + it('initializes ui/new_platform with core APIs', () => { const legacyPlatform = new LegacyPlatformService({ ...defaultParams, }); legacyPlatform.setup(defaultSetupDeps); - expect(mockChromeBreadcrumbsInit).toHaveBeenCalledTimes(1); - expect(mockChromeBreadcrumbsInit).toHaveBeenCalledWith(chromeSetup); - }); - - it('passes chrome service to ui/chrome/api/global_nav_state', () => { - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - }); - - legacyPlatform.setup(defaultSetupDeps); - - expect(mockGlobalNavStateInit).toHaveBeenCalledTimes(1); - expect(mockGlobalNavStateInit).toHaveBeenCalledWith(chromeSetup); - }); - }); - - describe('load order', () => { - describe('useLegacyTestHarness = false', () => { - it('loads ui/modules before ui/chrome, and both before legacy files', () => { - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - }); - - expect(mockLoadOrder).toEqual([]); - - legacyPlatform.setup(defaultSetupDeps); - - expect(mockLoadOrder).toMatchSnapshot(); - }); - }); - - describe('useLegacyTestHarness = true', () => { - it('loads ui/modules before ui/test_harness, and both before legacy files', () => { - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - useLegacyTestHarness: true, - }); - - expect(mockLoadOrder).toEqual([]); - - legacyPlatform.setup(defaultSetupDeps); - - expect(mockLoadOrder).toMatchSnapshot(); - }); + expect(mockUiNewPlatformSetup).toHaveBeenCalledTimes(1); + expect(mockUiNewPlatformSetup).toHaveBeenCalledWith(defaultSetupDeps.core, {}); }); }); }); describe('#start()', () => { - it('passes uiCapabilities to ui/capabilities', () => { + it('initializes ui/new_platform with core APIs', () => { const legacyPlatform = new LegacyPlatformService({ ...defaultParams, }); @@ -410,8 +139,8 @@ describe('#start()', () => { legacyPlatform.setup(defaultSetupDeps); legacyPlatform.start(defaultStartDeps); - expect(mockUICapabilitiesInit).toHaveBeenCalledTimes(1); - expect(mockUICapabilitiesInit).toHaveBeenCalledWith(applicationStart.capabilities); + expect(mockUiNewPlatformStart).toHaveBeenCalledTimes(1); + expect(mockUiNewPlatformStart).toHaveBeenCalledWith(defaultStartDeps.core, {}); }); describe('useLegacyTestHarness = false', () => { diff --git a/src/core/public/legacy/legacy_service.ts b/src/core/public/legacy/legacy_service.ts index c9e90376e9766..7d852773ad03f 100644 --- a/src/core/public/legacy/legacy_service.ts +++ b/src/core/public/legacy/legacy_service.ts @@ -18,7 +18,7 @@ */ import angular from 'angular'; -import { CoreSetup, CoreStart } from '../'; +import { InternalCoreSetup, InternalCoreStart } from '../'; /** @internal */ export interface LegacyPlatformParams { @@ -27,11 +27,13 @@ export interface LegacyPlatformParams { } interface SetupDeps { - core: CoreSetup; + core: InternalCoreSetup; + plugins: Record; } interface StartDeps { - core: CoreStart; + core: InternalCoreStart; + plugins: Record; targetDomElement: HTMLElement; } @@ -52,39 +54,13 @@ export class LegacyPlatformService { constructor(private readonly params: LegacyPlatformParams) {} - public setup({ core }: SetupDeps) { - const { - application, - i18n, - injectedMetadata, - fatalErrors, - notifications, - http, - basePath, - uiSettings, - chrome, - } = core; + public setup({ core, plugins }: SetupDeps) { // Inject parts of the new platform into parts of the legacy platform // so that legacy APIs/modules can mimic their new platform counterparts - require('ui/new_platform').__newPlatformSetup__(core); - require('ui/metadata').__newPlatformSetup__(injectedMetadata.getLegacyMetadata()); - require('ui/i18n').__newPlatformSetup__(i18n.Context); - require('ui/notify/fatal_error').__newPlatformSetup__(fatalErrors); - require('ui/kfetch').__newPlatformSetup__(http); - require('ui/notify/toasts').__newPlatformSetup__(notifications.toasts); - require('ui/chrome/api/loading_count').__newPlatformSetup__(http); - require('ui/chrome/api/base_path').__newPlatformSetup__(basePath); - require('ui/chrome/api/ui_settings').__newPlatformSetup__(uiSettings); - require('ui/chrome/api/injected_vars').__newPlatformSetup__(injectedMetadata); - require('ui/chrome/api/controls').__newPlatformSetup__(chrome); - require('ui/chrome/api/help_extension').__newPlatformSetup__(chrome); - require('ui/chrome/api/theme').__newPlatformSetup__(chrome); - require('ui/chrome/api/badge').__newPlatformSetup__(chrome); - require('ui/chrome/api/breadcrumbs').__newPlatformSetup__(chrome); - require('ui/chrome/services/global_nav_state').__newPlatformSetup__(chrome); - - injectedMetadata.getLegacyMetadata().nav.forEach((navLink: any) => - application.registerLegacyApp({ + require('ui/new_platform').__setup__(core, plugins); + + core.injectedMetadata.getLegacyMetadata().nav.forEach((navLink: any) => + core.application.registerLegacyApp({ id: navLink.id, order: navLink.order, title: navLink.title, @@ -95,6 +71,12 @@ export class LegacyPlatformService { linkToLastSubUrl: navLink.linkToLastSubUrl, }) ); + } + + public start({ core, targetDomElement, plugins }: StartDeps) { + // Inject parts of the new platform into parts of the legacy platform + // so that legacy APIs/modules can mimic their new platform counterparts + require('ui/new_platform').__start__(core, plugins); // Load the bootstrap module before loading the legacy platform files so that // the bootstrap module can modify the environment a bit first @@ -102,18 +84,13 @@ export class LegacyPlatformService { // require the files that will tie into the legacy platform this.params.requireLegacyFiles(); - } - public start({ core, targetDomElement }: StartDeps) { if (!this.bootstrapModule) { throw new Error('Bootstrap module must be loaded before `start`'); } this.targetDomElement = targetDomElement; - require('ui/new_platform').__newPlatformStart__(core); - require('ui/capabilities').__newPlatformStart__(core.application.capabilities); - this.bootstrapModule.bootstrap(this.targetDomElement); } @@ -125,7 +102,7 @@ export class LegacyPlatformService { const angularRoot = angular.element(this.targetDomElement); const injector$ = angularRoot.injector(); - // if we haven't gotten to the point of bootstraping + // if we haven't gotten to the point of bootstrapping // angular, injector$ won't be defined if (!injector$) { return; diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index e58bb2f0efb3a..cb09b1d0b6be5 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -17,7 +17,6 @@ * under the License. */ -export { basePathServiceMock } from './base_path/base_path_service.mock'; export { chromeServiceMock } from './chrome/chrome_service.mock'; export { fatalErrorsServiceMock } from './fatal_errors/fatal_errors_service.mock'; export { httpServiceMock } from './http/http_service.mock'; diff --git a/src/core/public/notifications/index.ts b/src/core/public/notifications/index.ts index 9800b5154d0bc..e85db3772f019 100644 --- a/src/core/public/notifications/index.ts +++ b/src/core/public/notifications/index.ts @@ -17,7 +17,7 @@ * under the License. */ -export { Toast, ToastInput, ToastsApi } from './toasts'; +export { ErrorToastOptions, Toast, ToastInput, ToastsApi } from './toasts'; export { NotificationsService, NotificationsSetup, diff --git a/src/core/public/notifications/notifications_service.ts b/src/core/public/notifications/notifications_service.ts index d68d848c1d88a..d8667bca97964 100644 --- a/src/core/public/notifications/notifications_service.ts +++ b/src/core/public/notifications/notifications_service.ts @@ -20,17 +20,19 @@ import { i18n } from '@kbn/i18n'; import { Subscription } from 'rxjs'; -import { I18nStart } from '../i18n'; -import { ToastsService } from './toasts'; -import { ToastsApi } from './toasts/toasts_api'; +import { I18nStart, I18nSetup } from '../i18n'; +import { ToastsService, ToastsSetup, ToastsStart } from './toasts'; import { UiSettingsSetup } from '../ui_settings'; +import { OverlayStart } from '../overlays'; interface SetupDeps { + i18n: I18nSetup; uiSettings: UiSettingsSetup; } interface StartDeps { i18n: I18nStart; + overlays: OverlayStart; targetDomElement: HTMLElement; } @@ -44,8 +46,8 @@ export class NotificationsService { this.toasts = new ToastsService(); } - public setup({ uiSettings }: SetupDeps): NotificationsSetup { - const notificationSetup = { toasts: this.toasts.setup() }; + public setup({ i18n: i18nSetup, uiSettings }: SetupDeps): NotificationsSetup { + const notificationSetup = { toasts: this.toasts.setup({ i18n: i18nSetup, uiSettings }) }; this.uiSettingsErrorSubscription = uiSettings.getUpdateErrors$().subscribe(error => { notificationSetup.toasts.addDanger({ @@ -59,13 +61,13 @@ export class NotificationsService { return notificationSetup; } - public start({ i18n: i18nDep, targetDomElement }: StartDeps): NotificationsStart { + public start({ i18n: i18nDep, overlays, targetDomElement }: StartDeps): NotificationsStart { this.targetDomElement = targetDomElement; const toastsContainer = document.createElement('div'); targetDomElement.appendChild(toastsContainer); return { - toasts: this.toasts.start({ i18n: i18nDep, targetDomElement: toastsContainer }), + toasts: this.toasts.start({ i18n: i18nDep, overlays, targetDomElement: toastsContainer }), }; } @@ -84,8 +86,10 @@ export class NotificationsService { /** @public */ export interface NotificationsSetup { - toasts: ToastsApi; + toasts: ToastsSetup; } /** @public */ -export type NotificationsStart = NotificationsSetup; +export interface NotificationsStart { + toasts: ToastsStart; +} diff --git a/src/core/public/notifications/toasts/__snapshots__/error_toast.test.tsx.snap b/src/core/public/notifications/toasts/__snapshots__/error_toast.test.tsx.snap new file mode 100644 index 0000000000000..22af4cebcf387 --- /dev/null +++ b/src/core/public/notifications/toasts/__snapshots__/error_toast.test.tsx.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders matching snapshot 1`] = ` + +

+ This is the toast message +

+
+ + + +
+ +`; diff --git a/src/core/public/notifications/toasts/__snapshots__/global_toast_list.test.tsx.snap b/src/core/public/notifications/toasts/__snapshots__/global_toast_list.test.tsx.snap index afa2f7b911979..29b289592b2ef 100644 --- a/src/core/public/notifications/toasts/__snapshots__/global_toast_list.test.tsx.snap +++ b/src/core/public/notifications/toasts/__snapshots__/global_toast_list.test.tsx.snap @@ -2,8 +2,9 @@ exports[`renders matching snapshot 1`] = ` `; diff --git a/src/core/public/notifications/toasts/error_toast.test.tsx b/src/core/public/notifications/toasts/error_toast.test.tsx new file mode 100644 index 0000000000000..aa128f39e447d --- /dev/null +++ b/src/core/public/notifications/toasts/error_toast.test.tsx @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { shallow } from 'enzyme'; +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; + +import { ErrorToast } from './error_toast'; + +import { i18nServiceMock } from '../../i18n/i18n_service.mock'; + +interface ErrorToastProps { + error?: Error; + title?: string; + toastMessage?: string; +} + +let openModal: jest.Mock; + +beforeEach(() => (openModal = jest.fn())); + +function render(props: ErrorToastProps = {}) { + return ( + + ); +} + +it('renders matching snapshot', () => { + expect(shallow(render())).toMatchSnapshot(); +}); + +it('should open a modal when clicking button', () => { + const wrapper = mountWithIntl(render()); + expect(openModal).not.toHaveBeenCalled(); + wrapper.find('button').simulate('click'); + expect(openModal).toHaveBeenCalled(); +}); + +afterAll(() => { + // Cleanup document.body to cleanup any modals which might be left over from tests. + document.body.innerHTML = ''; +}); diff --git a/src/core/public/notifications/toasts/error_toast.tsx b/src/core/public/notifications/toasts/error_toast.tsx new file mode 100644 index 0000000000000..ca249e75fd92c --- /dev/null +++ b/src/core/public/notifications/toasts/error_toast.tsx @@ -0,0 +1,106 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { + EuiButton, + EuiCallOut, + EuiCodeBlock, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, +} from '@elastic/eui'; +import { EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { I18nSetup } from '../../i18n'; +import { OverlayStart } from '../../overlays'; + +interface ErrorToastProps { + title: string; + error: Error; + toastMessage: string; + i18nContext: I18nSetup['Context']; + openModal: OverlayStart['openModal']; +} + +/** + * This should instead be replaced by the overlay service once it's available. + * This does not use React portals so that if the parent toast times out, this modal + * does not disappear. NOTE: this should use a global modal in the overlay service + * in the future. + */ +function showErrorDialog({ + title, + error, + i18nContext: I18nContext, + openModal, +}: Pick) { + const modal = openModal( + + + {title} + + + + {error.stack && ( + + + + {error.stack} + + + )} + + + modal.close()} fill> + + + + + ); +} + +export function ErrorToast({ + title, + error, + toastMessage, + i18nContext, + openModal, +}: ErrorToastProps) { + return ( + +

{toastMessage}

+
+ showErrorDialog({ title, error, openModal, i18nContext })} + > + + +
+
+ ); +} diff --git a/src/core/public/notifications/toasts/global_toast_list.tsx b/src/core/public/notifications/toasts/global_toast_list.tsx index 4832b700d1da9..1b3717f55d45b 100644 --- a/src/core/public/notifications/toasts/global_toast_list.tsx +++ b/src/core/public/notifications/toasts/global_toast_list.tsx @@ -53,9 +53,15 @@ export class GlobalToastList extends React.Component { public render() { return ( ); } diff --git a/src/core/public/notifications/toasts/index.ts b/src/core/public/notifications/toasts/index.ts index 8615bfd792e19..2279f44a55090 100644 --- a/src/core/public/notifications/toasts/index.ts +++ b/src/core/public/notifications/toasts/index.ts @@ -17,6 +17,6 @@ * under the License. */ -export { ToastsService } from './toasts_service'; -export { ToastsApi, ToastInput } from './toasts_api'; +export { ToastsService, ToastsSetup, ToastsStart } from './toasts_service'; +export { ErrorToastOptions, ToastsApi, ToastInput } from './toasts_api'; export { Toast } from '@elastic/eui'; diff --git a/src/core/public/notifications/toasts/toasts_api.test.ts b/src/core/public/notifications/toasts/toasts_api.test.ts index d57b84b876c13..d934af932d400 100644 --- a/src/core/public/notifications/toasts/toasts_api.test.ts +++ b/src/core/public/notifications/toasts/toasts_api.test.ts @@ -21,6 +21,9 @@ import { take } from 'rxjs/operators'; import { ToastsApi } from './toasts_api'; +import { uiSettingsServiceMock } from '../../ui_settings/ui_settings_service.mock'; +import { i18nServiceMock } from '../../i18n/i18n_service.mock'; + async function getCurrentToasts(toasts: ToastsApi) { return await toasts .get$() @@ -28,9 +31,33 @@ async function getCurrentToasts(toasts: ToastsApi) { .toPromise(); } +function uiSettingsMock() { + const mock = uiSettingsServiceMock.createSetupContract(); + (mock.get as jest.Mock).mockImplementation(() => (config: string) => { + switch (config) { + case 'notifications:lifetime:info': + return 5000; + case 'notifications:lifetime:warning': + return 10000; + case 'notification:lifetime:error': + return 30000; + default: + throw new Error(`Accessing ${config} is not supported in the mock.`); + } + }); + return mock; +} + +function toastDeps() { + return { + uiSettings: uiSettingsMock(), + i18n: i18nServiceMock.createSetupContract(), + }; +} + describe('#get$()', () => { it('returns observable that emits NEW toast list when something added or removed', () => { - const toasts = new ToastsApi(); + const toasts = new ToastsApi(toastDeps()); const onToasts = jest.fn(); toasts.get$().subscribe(onToasts); @@ -57,7 +84,7 @@ describe('#get$()', () => { }); it('does not emit a new toast list when unknown toast is passed to remove()', () => { - const toasts = new ToastsApi(); + const toasts = new ToastsApi(toastDeps()); const onToasts = jest.fn(); toasts.get$().subscribe(onToasts); @@ -71,14 +98,14 @@ describe('#get$()', () => { describe('#add()', () => { it('returns toast objects with auto assigned id', () => { - const toasts = new ToastsApi(); + const toasts = new ToastsApi(toastDeps()); const toast = toasts.add({ title: 'foo' }); expect(toast).toHaveProperty('id'); expect(toast).toHaveProperty('title', 'foo'); }); it('adds the toast to toasts list', async () => { - const toasts = new ToastsApi(); + const toasts = new ToastsApi(toastDeps()); const toast = toasts.add({}); const currentToasts = await getCurrentToasts(toasts); @@ -87,27 +114,27 @@ describe('#add()', () => { }); it('increments the toast ID for each additional toast', () => { - const toasts = new ToastsApi(); + const toasts = new ToastsApi(toastDeps()); expect(toasts.add({})).toHaveProperty('id', '0'); expect(toasts.add({})).toHaveProperty('id', '1'); expect(toasts.add({})).toHaveProperty('id', '2'); }); it('accepts a string, uses it as the title', async () => { - const toasts = new ToastsApi(); + const toasts = new ToastsApi(toastDeps()); expect(toasts.add('foo')).toHaveProperty('title', 'foo'); }); }); describe('#remove()', () => { it('removes a toast', async () => { - const toasts = new ToastsApi(); + const toasts = new ToastsApi(toastDeps()); toasts.remove(toasts.add('Test')); expect(await getCurrentToasts(toasts)).toHaveLength(0); }); it('ignores unknown toast', async () => { - const toasts = new ToastsApi(); + const toasts = new ToastsApi(toastDeps()); toasts.add('Test'); toasts.remove({ id: 'foo' }); @@ -118,12 +145,12 @@ describe('#remove()', () => { describe('#addSuccess()', () => { it('adds a success toast', async () => { - const toasts = new ToastsApi(); + const toasts = new ToastsApi(toastDeps()); expect(toasts.addSuccess({})).toHaveProperty('color', 'success'); }); it('returns the created toast', async () => { - const toasts = new ToastsApi(); + const toasts = new ToastsApi(toastDeps()); const toast = toasts.addSuccess({}); const currentToasts = await getCurrentToasts(toasts); expect(currentToasts[0]).toBe(toast); @@ -132,12 +159,12 @@ describe('#addSuccess()', () => { describe('#addWarning()', () => { it('adds a warning toast', async () => { - const toasts = new ToastsApi(); + const toasts = new ToastsApi(toastDeps()); expect(toasts.addWarning({})).toHaveProperty('color', 'warning'); }); it('returns the created toast', async () => { - const toasts = new ToastsApi(); + const toasts = new ToastsApi(toastDeps()); const toast = toasts.addWarning({}); const currentToasts = await getCurrentToasts(toasts); expect(currentToasts[0]).toBe(toast); @@ -146,14 +173,30 @@ describe('#addWarning()', () => { describe('#addDanger()', () => { it('adds a danger toast', async () => { - const toasts = new ToastsApi(); + const toasts = new ToastsApi(toastDeps()); expect(toasts.addDanger({})).toHaveProperty('color', 'danger'); }); it('returns the created toast', async () => { - const toasts = new ToastsApi(); + const toasts = new ToastsApi(toastDeps()); const toast = toasts.addDanger({}); const currentToasts = await getCurrentToasts(toasts); expect(currentToasts[0]).toBe(toast); }); }); + +describe('#addError', () => { + it('adds an error toast', async () => { + const toasts = new ToastsApi(toastDeps()); + const toast = toasts.addError(new Error('unexpected error'), { title: 'Something went wrong' }); + expect(toast).toHaveProperty('color', 'danger'); + expect(toast).toHaveProperty('title', 'Something went wrong'); + }); + + it('returns the created toast', async () => { + const toasts = new ToastsApi(toastDeps()); + const toast = toasts.addError(new Error('unexpected error'), { title: 'Something went wrong' }); + const currentToasts = await getCurrentToasts(toasts); + expect(currentToasts[0]).toBe(toast); + }); +}); diff --git a/src/core/public/notifications/toasts/toasts_api.tsx b/src/core/public/notifications/toasts/toasts_api.tsx index d833c8586def5..6f430d7765c28 100644 --- a/src/core/public/notifications/toasts/toasts_api.tsx +++ b/src/core/public/notifications/toasts/toasts_api.tsx @@ -18,10 +18,32 @@ */ import { Toast } from '@elastic/eui'; +import React from 'react'; import * as Rx from 'rxjs'; +import { ErrorToast } from './error_toast'; +import { UiSettingsSetup } from '../../ui_settings'; +import { I18nSetup } from '../../i18n'; +import { OverlayStart } from '../../overlays'; + +type ToastInputFields = Pick>; + /** @public */ -export type ToastInput = string | Pick>; +export type ToastInput = string | ToastInputFields | Promise; + +export interface ErrorToastOptions { + /** + * The title of the toast and the dialog when expanding the message. + */ + title: string; + /** + * The message to be shown in the toast. If this is not specified the error's + * message will be shown in the toast instead. Overwriting that message can + * be used to provide more user-friendly toasts. If you specify this, the error + * message will still be shown in the detailed error modal. + */ + toastMessage?: string; +} const normalizeToast = (toastOrTitle: ToastInput) => { if (typeof toastOrTitle === 'string') { @@ -37,6 +59,19 @@ const normalizeToast = (toastOrTitle: ToastInput) => { export class ToastsApi { private toasts$ = new Rx.BehaviorSubject([]); private idCounter = 0; + private uiSettings: UiSettingsSetup; + private i18n: I18nSetup; + + private overlays?: OverlayStart; + + constructor(deps: { uiSettings: UiSettingsSetup; i18n: I18nSetup }) { + this.uiSettings = deps.uiSettings; + this.i18n = deps.i18n; + } + + public registerOverlays(overlays: OverlayStart) { + this.overlays = overlays; + } public get$() { return this.toasts$.asObservable(); @@ -45,6 +80,7 @@ export class ToastsApi { public add(toastOrTitle: ToastInput) { const toast: Toast = { id: String(this.idCounter++), + toastLifeTimeMs: this.uiSettings.get('notifications:lifetime:info'), ...normalizeToast(toastOrTitle), }; @@ -73,6 +109,7 @@ export class ToastsApi { return this.add({ color: 'warning', iconType: 'help', + toastLifeTimeMs: this.uiSettings.get('notifications:lifetime:warning'), ...normalizeToast(toastOrTitle), }); } @@ -81,7 +118,39 @@ export class ToastsApi { return this.add({ color: 'danger', iconType: 'alert', + toastLifeTimeMs: this.uiSettings.get('notifications:lifetime:warning'), ...normalizeToast(toastOrTitle), }); } + + public addError(error: Error, options: ErrorToastOptions) { + const message = options.toastMessage || error.message; + return this.add({ + color: 'danger', + iconType: 'alert', + title: options.title, + toastLifeTimeMs: this.uiSettings.get('notifications:lifetime:error'), + text: ( + + ), + }); + } + + private openModal( + ...args: Parameters + ): ReturnType { + if (!this.overlays) { + // This case should never happen because no rendering should be occurring + // before the ToastService is started. + throw new Error(`Modal opened before ToastService was started.`); + } + + return this.overlays.openModal(...args); + } } diff --git a/src/core/public/notifications/toasts/toasts_service.mock.ts b/src/core/public/notifications/toasts/toasts_service.mock.ts index e6a7d225f9243..06a30f458c162 100644 --- a/src/core/public/notifications/toasts/toasts_service.mock.ts +++ b/src/core/public/notifications/toasts/toasts_service.mock.ts @@ -16,16 +16,19 @@ * specific language governing permissions and limitations * under the License. */ -import { ToastsApi } from './toasts_api'; + +import { Observable } from 'rxjs'; +import { ToastsSetup } from './toasts_service'; const createToastsApiMock = () => { - const api: jest.Mocked> = { - get$: jest.fn(), + const api: jest.Mocked> = { + get$: jest.fn(() => new Observable()), add: jest.fn(), remove: jest.fn(), addSuccess: jest.fn(), addWarning: jest.fn(), addDanger: jest.fn(), + addError: jest.fn(), }; return api; }; diff --git a/src/core/public/notifications/toasts/toasts_service.test.tsx b/src/core/public/notifications/toasts/toasts_service.test.tsx index bc1683aa2c490..b5e89a88bd17e 100644 --- a/src/core/public/notifications/toasts/toasts_service.test.tsx +++ b/src/core/public/notifications/toasts/toasts_service.test.tsx @@ -21,6 +21,8 @@ import { mockReactDomRender, mockReactDomUnmount } from './toasts_service.test.m import { ToastsService } from './toasts_service'; import { ToastsApi } from './toasts_api'; +import { overlayServiceMock } from '../../overlays/overlay_service.mock'; +import { uiSettingsServiceMock } from '../../ui_settings/ui_settings_service.mock'; const mockI18n: any = { Context: function I18nContext() { @@ -28,11 +30,15 @@ const mockI18n: any = { }, }; +const mockOverlays = overlayServiceMock.createStartContract(); + describe('#setup()', () => { it('returns a ToastsApi', () => { const toasts = new ToastsService(); - expect(toasts.setup()).toBeInstanceOf(ToastsApi); + expect( + toasts.setup({ i18n: mockI18n, uiSettings: uiSettingsServiceMock.createSetupContract() }) + ).toBeInstanceOf(ToastsApi); }); }); @@ -43,8 +49,8 @@ describe('#start()', () => { const toasts = new ToastsService(); expect(mockReactDomRender).not.toHaveBeenCalled(); - toasts.setup(); - toasts.start({ i18n: mockI18n, targetDomElement }); + toasts.setup({ i18n: mockI18n, uiSettings: uiSettingsServiceMock.createSetupContract() }); + toasts.start({ i18n: mockI18n, targetDomElement, overlays: mockOverlays }); expect(mockReactDomRender.mock.calls).toMatchSnapshot(); }); @@ -52,8 +58,12 @@ describe('#start()', () => { const targetDomElement = document.createElement('div'); const toasts = new ToastsService(); - toasts.setup(); - expect(toasts.start({ i18n: mockI18n, targetDomElement })).toBeInstanceOf(ToastsApi); + expect( + toasts.setup({ i18n: mockI18n, uiSettings: uiSettingsServiceMock.createSetupContract() }) + ).toBeInstanceOf(ToastsApi); + expect( + toasts.start({ i18n: mockI18n, targetDomElement, overlays: mockOverlays }) + ).toBeInstanceOf(ToastsApi); }); }); @@ -63,8 +73,8 @@ describe('#stop()', () => { targetDomElement.setAttribute('test', 'target-dom-element'); const toasts = new ToastsService(); - toasts.setup(); - toasts.start({ i18n: mockI18n, targetDomElement }); + toasts.setup({ i18n: mockI18n, uiSettings: uiSettingsServiceMock.createSetupContract() }); + toasts.start({ i18n: mockI18n, targetDomElement, overlays: mockOverlays }); expect(mockReactDomUnmount).not.toHaveBeenCalled(); toasts.stop(); @@ -82,8 +92,8 @@ describe('#stop()', () => { const targetDomElement = document.createElement('div'); const toasts = new ToastsService(); - toasts.setup(); - toasts.start({ i18n: mockI18n, targetDomElement }); + toasts.setup({ i18n: mockI18n, uiSettings: uiSettingsServiceMock.createSetupContract() }); + toasts.start({ i18n: mockI18n, targetDomElement, overlays: mockOverlays }); toasts.stop(); expect(targetDomElement.childNodes).toHaveLength(0); }); diff --git a/src/core/public/notifications/toasts/toasts_service.tsx b/src/core/public/notifications/toasts/toasts_service.tsx index 9d7f12ec67c38..37d911357a51b 100644 --- a/src/core/public/notifications/toasts/toasts_service.tsx +++ b/src/core/public/notifications/toasts/toasts_service.tsx @@ -21,25 +21,40 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { Toast } from '@elastic/eui'; -import { I18nSetup } from '../../i18n'; +import { I18nSetup, I18nStart } from '../../i18n'; +import { UiSettingsSetup } from '../../ui_settings'; import { GlobalToastList } from './global_toast_list'; import { ToastsApi } from './toasts_api'; +import { OverlayStart } from '../../overlays'; -interface StartDeps { +interface SetupDeps { i18n: I18nSetup; + uiSettings: UiSettingsSetup; +} + +interface StartDeps { + i18n: I18nStart; + overlays: OverlayStart; targetDomElement: HTMLElement; } +/** @public */ +export type ToastsSetup = Pick>; + +/** @public */ +export type ToastsStart = ToastsSetup; + export class ToastsService { private api?: ToastsApi; private targetDomElement?: HTMLElement; - public setup() { - this.api = new ToastsApi(); + public setup({ i18n, uiSettings }: SetupDeps) { + this.api = new ToastsApi({ i18n, uiSettings }); return this.api!; } - public start({ i18n, targetDomElement }: StartDeps) { + public start({ i18n, overlays, targetDomElement }: StartDeps) { + this.api!.registerOverlays(overlays); this.targetDomElement = targetDomElement; render( diff --git a/src/core/public/overlays/overlay_service.ts b/src/core/public/overlays/overlay_service.ts index 961e45f365676..0f9dec0b311c2 100644 --- a/src/core/public/overlays/overlay_service.ts +++ b/src/core/public/overlays/overlay_service.ts @@ -17,6 +17,8 @@ * under the License. */ +import React from 'react'; + import { FlyoutService } from './flyout'; import { ModalService } from './modal'; import { I18nStart } from '../i18n'; diff --git a/src/core/public/plugins/index.ts b/src/core/public/plugins/index.ts index 3e1e1434c4259..fc16b6b004565 100644 --- a/src/core/public/plugins/index.ts +++ b/src/core/public/plugins/index.ts @@ -19,4 +19,4 @@ export * from './plugins_service'; export { Plugin, PluginInitializer } from './plugin'; -export { PluginInitializerContext, PluginSetupContext, PluginStartContext } from './plugin_context'; +export { PluginInitializerContext } from './plugin_context'; diff --git a/src/core/public/plugins/plugin.ts b/src/core/public/plugins/plugin.ts index a57d74bb26a9b..5beba9dbafd2c 100644 --- a/src/core/public/plugins/plugin.ts +++ b/src/core/public/plugins/plugin.ts @@ -18,8 +18,9 @@ */ import { DiscoveredPlugin, PluginName } from '../../server'; -import { PluginInitializerContext, PluginSetupContext, PluginStartContext } from './plugin_context'; +import { PluginInitializerContext } from './plugin_context'; import { loadPluginBundle } from './plugin_loader'; +import { CoreStart, CoreSetup } from '..'; /** * The interface that should be returned by a `PluginInitializer`. @@ -32,8 +33,8 @@ export interface Plugin< TPluginsSetup extends Record = {}, TPluginsStart extends Record = {} > { - setup: (core: PluginSetupContext, plugins: TPluginsSetup) => TSetup | Promise; - start: (core: PluginStartContext, plugins: TPluginsStart) => TStart | Promise; + setup: (core: CoreSetup, plugins: TPluginsSetup) => TSetup | Promise; + start: (core: CoreStart, plugins: TPluginsStart) => TStart | Promise; stop?: () => void; } @@ -98,7 +99,7 @@ export class PluginWrapper< * @param plugins The dictionary where the key is the dependency name and the value * is the contract returned by the dependency's `setup` function. */ - public async setup(setupContext: PluginSetupContext, plugins: TPluginsSetup) { + public async setup(setupContext: CoreSetup, plugins: TPluginsSetup) { this.instance = await this.createPluginInstance(); return await this.instance.setup(setupContext, plugins); @@ -111,7 +112,7 @@ export class PluginWrapper< * @param plugins The dictionary where the key is the dependency name and the value * is the contract returned by the dependency's `start` function. */ - public async start(startContext: PluginStartContext, plugins: TPluginsStart) { + public async start(startContext: CoreStart, plugins: TPluginsStart) { if (this.instance === undefined) { throw new Error(`Plugin "${this.name}" can't be started since it isn't set up.`); } diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index dd74dadb36a1c..4d1809e1e2ca4 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -18,18 +18,10 @@ */ import { DiscoveredPlugin } from '../../server'; -import { BasePathSetup, BasePathStart } from '../base_path'; -import { ChromeSetup, ChromeStart } from '../chrome'; import { CoreContext } from '../core_system'; -import { FatalErrorsSetup } from '../fatal_errors'; -import { I18nSetup, I18nStart } from '../i18n'; -import { NotificationsSetup, NotificationsStart } from '../notifications'; -import { UiSettingsSetup } from '../ui_settings'; import { PluginWrapper } from './plugin'; import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service'; -import { OverlayStart } from '../overlays'; -import { ApplicationStart } from '../application'; -import { HttpSetup, HttpStart } from '../http'; +import { CoreSetup, CoreStart } from '../'; /** * The available core services passed to a `PluginInitializer` @@ -39,36 +31,6 @@ import { HttpSetup, HttpStart } from '../http'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface PluginInitializerContext {} -/** - * The available core services passed to a plugin's `Plugin#setup` method. - * - * @public - */ -export interface PluginSetupContext { - basePath: BasePathSetup; - chrome: ChromeSetup; - fatalErrors: FatalErrorsSetup; - http: HttpSetup; - i18n: I18nSetup; - notifications: NotificationsSetup; - uiSettings: UiSettingsSetup; -} - -/** - * The available core services passed to a plugin's `Plugin#start` method. - * - * @public - */ -export interface PluginStartContext { - application: Pick; - chrome: ChromeStart; - basePath: BasePathStart; - http: HttpStart; - i18n: I18nStart; - notifications: NotificationsStart; - overlays: OverlayStart; -} - /** * Provides a plugin-specific context passed to the plugin's construtor. This is currently * empty but should provide static services in the future, such as config and logging. @@ -98,10 +60,9 @@ export function createPluginSetupContext -): PluginSetupContext { +): CoreSetup { return { http: deps.http, - basePath: deps.basePath, chrome: deps.chrome, fatalErrors: deps.fatalErrors, i18n: deps.i18n, @@ -124,14 +85,13 @@ export function createPluginStartContext -): PluginStartContext { +): CoreStart { return { application: { capabilities: deps.application.capabilities, }, - chrome: deps.chrome, - basePath: deps.basePath, http: deps.http, + chrome: deps.chrome, i18n: deps.i18n, notifications: deps.notifications, overlays: deps.overlays, diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index 6530a9330e8e0..fc37589bf6d4a 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -36,14 +36,13 @@ import { notificationServiceMock } from '../notifications/notifications_service. import { applicationServiceMock } from '../application/application_service.mock'; import { i18nServiceMock } from '../i18n/i18n_service.mock'; import { overlayServiceMock } from '../overlays/overlay_service.mock'; -import { PluginStartContext, PluginSetupContext } from './plugin_context'; import { chromeServiceMock } from '../chrome/chrome_service.mock'; import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.mock'; import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; -import { basePathServiceMock } from '../base_path/base_path_service.mock'; import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; -import { UiSettingsClient } from '../ui_settings'; import { httpServiceMock } from '../http/http_service.mock'; +import { UiSettingsClient } from '../ui_settings'; +import { CoreSetup, CoreStart } from '..'; export let mockPluginInitializers: Map; @@ -55,9 +54,9 @@ type DeeplyMocked = { [P in keyof T]: jest.Mocked }; const mockCoreContext: CoreContext = {}; let mockSetupDeps: DeeplyMocked; -let mockSetupContext: DeeplyMocked; +let mockSetupContext: DeeplyMocked; let mockStartDeps: DeeplyMocked; -let mockStartContext: DeeplyMocked; +let mockStartContext: DeeplyMocked; beforeEach(() => { mockSetupDeps = { @@ -74,11 +73,6 @@ beforeEach(() => { ]); return metadata; })(), - basePath: (function() { - const basePath = basePathServiceMock.createSetupContract(); - basePath.addToPath.mockImplementation(path => path); - return basePath; - })(), chrome: chromeServiceMock.createSetupContract(), fatalErrors: fatalErrorsServiceMock.createSetupContract(), http: httpServiceMock.createSetupContract(), @@ -89,9 +83,8 @@ beforeEach(() => { mockSetupContext = omit(mockSetupDeps, 'application', 'injectedMetadata'); mockStartDeps = { application: applicationServiceMock.createStartContract(), - basePath: basePathServiceMock.createStartContract(), - chrome: chromeServiceMock.createStartContract(), http: httpServiceMock.createStartContract(), + chrome: chromeServiceMock.createStartContract(), i18n: i18nServiceMock.createStartContract(), injectedMetadata: injectedMetadataServiceMock.createStartContract(), notifications: notificationServiceMock.createStartContract(), @@ -171,14 +164,14 @@ test('`PluginsService.setup` fails if any plugin instance does not have a setup ); }); -test('`PluginsService.setup` calls loadPluginBundles with basePath and plugins', async () => { +test('`PluginsService.setup` calls loadPluginBundles with http and plugins', async () => { const pluginsService = new PluginsService(mockCoreContext); await pluginsService.setup(mockSetupDeps); expect(mockLoadPluginBundle).toHaveBeenCalledTimes(3); - expect(mockLoadPluginBundle).toHaveBeenCalledWith(mockSetupDeps.basePath.addToPath, 'pluginA'); - expect(mockLoadPluginBundle).toHaveBeenCalledWith(mockSetupDeps.basePath.addToPath, 'pluginB'); - expect(mockLoadPluginBundle).toHaveBeenCalledWith(mockSetupDeps.basePath.addToPath, 'pluginC'); + expect(mockLoadPluginBundle).toHaveBeenCalledWith(mockSetupDeps.http.prependBasePath, 'pluginA'); + expect(mockLoadPluginBundle).toHaveBeenCalledWith(mockSetupDeps.http.prependBasePath, 'pluginB'); + expect(mockLoadPluginBundle).toHaveBeenCalledWith(mockSetupDeps.http.prependBasePath, 'pluginC'); }); test('`PluginsService.setup` initalizes plugins with CoreContext', async () => { diff --git a/src/core/public/plugins/plugins_service.ts b/src/core/public/plugins/plugins_service.ts index ed07c6736dfd8..c3b337a8956ec 100644 --- a/src/core/public/plugins/plugins_service.ts +++ b/src/core/public/plugins/plugins_service.ts @@ -17,7 +17,7 @@ * under the License. */ -import { CoreSetup, CoreStart } from '..'; +import { InternalCoreSetup, InternalCoreStart } from '..'; import { PluginName } from '../../server'; import { CoreService } from '../../types'; import { CoreContext } from '../core_system'; @@ -29,9 +29,9 @@ import { } from './plugin_context'; /** @internal */ -export type PluginsServiceSetupDeps = CoreSetup; +export type PluginsServiceSetupDeps = InternalCoreSetup; /** @internal */ -export type PluginsServiceStartDeps = CoreStart; +export type PluginsServiceStartDeps = InternalCoreStart; /** @internal */ export interface PluginsServiceSetup { @@ -70,7 +70,7 @@ export class PluginsService implements CoreService(); diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index d1d88a64558b3..a8e9b78ab228c 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -4,11 +4,9 @@ ```ts -import * as CSS from 'csstype'; -import { default } from 'react'; import { IconType } from '@elastic/eui'; import { Observable } from 'rxjs'; -import * as PropTypes from 'prop-types'; +import React from 'react'; import * as Rx from 'rxjs'; import { Toast } from '@elastic/eui'; @@ -36,16 +34,6 @@ export interface ApplicationStart { mount: (mountHandler: Function) => void; } -// @public -export interface BasePathSetup { - addToPath(path: string): string; - get(): string; - removeFromPath(path: string): string; -} - -// @public -export type BasePathStart = BasePathSetup; - // @public export interface Capabilities { [key: string]: Record>; @@ -118,10 +106,6 @@ export interface CoreContext { // @public export interface CoreSetup { - // (undocumented) - application: ApplicationSetup; - // (undocumented) - basePath: BasePathSetup; // (undocumented) chrome: ChromeSetup; // (undocumented) @@ -131,8 +115,6 @@ export interface CoreSetup { // (undocumented) i18n: I18nSetup; // (undocumented) - injectedMetadata: InjectedMetadataSetup; - // (undocumented) notifications: NotificationsSetup; // (undocumented) uiSettings: UiSettingsSetup; @@ -141,9 +123,7 @@ export interface CoreSetup { // @public export interface CoreStart { // (undocumented) - application: ApplicationStart; - // (undocumented) - basePath: BasePathStart; + application: Pick; // (undocumented) chrome: ChromeStart; // (undocumented) @@ -151,8 +131,6 @@ export interface CoreStart { // (undocumented) i18n: I18nStart; // (undocumented) - injectedMetadata: InjectedMetadataStart; - // (undocumented) notifications: NotificationsStart; // (undocumented) overlays: OverlayStart; @@ -172,6 +150,14 @@ export class CoreSystem { stop(): void; } +// Warning: (ae-missing-release-tag) "ErrorToastOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface ErrorToastOptions { + title: string; + toastMessage?: string; +} + // @public export interface FatalErrorInfo { // (undocumented) @@ -186,18 +172,74 @@ export interface FatalErrorsSetup { get$: () => Rx.Observable; } -// Warning: (ae-forgotten-export) The symbol "HttpService" needs to be exported by the entry point index.d.ts -// // @public (undocumented) -export type HttpSetup = ReturnType; +export interface HttpInterceptor { + // Warning: (ae-forgotten-export) The symbol "HttpInterceptController" needs to be exported by the entry point index.d.ts + // + // (undocumented) + request?(request: Request, controller: HttpInterceptController): Promise | Request | void; + // Warning: (ae-forgotten-export) The symbol "HttpErrorRequest" needs to be exported by the entry point index.d.ts + // + // (undocumented) + requestError?(httpErrorRequest: HttpErrorRequest, controller: HttpInterceptController): Promise | Request | void; + // Warning: (ae-forgotten-export) The symbol "HttpResponse" needs to be exported by the entry point index.d.ts + // + // (undocumented) + response?(httpResponse: HttpResponse, controller: HttpInterceptController): Promise | HttpResponse | void; + // Warning: (ae-forgotten-export) The symbol "HttpErrorResponse" needs to be exported by the entry point index.d.ts + // + // (undocumented) + responseError?(httpErrorResponse: HttpErrorResponse, controller: HttpInterceptController): Promise | HttpResponse | void; +} // @public (undocumented) -export type HttpStart = ReturnType; +export interface HttpServiceBase { + // (undocumented) + addLoadingCount(count$: Observable): void; + // (undocumented) + delete: HttpHandler; + // Warning: (ae-forgotten-export) The symbol "HttpHandler" needs to be exported by the entry point index.d.ts + // + // (undocumented) + fetch: HttpHandler; + // (undocumented) + get: HttpHandler; + // (undocumented) + getBasePath(): string; + // (undocumented) + getLoadingCount$(): Observable; + // (undocumented) + head: HttpHandler; + // (undocumented) + intercept(interceptor: HttpInterceptor): () => void; + // (undocumented) + options: HttpHandler; + // (undocumented) + patch: HttpHandler; + // (undocumented) + post: HttpHandler; + // (undocumented) + prependBasePath(path: string): string; + // (undocumented) + put: HttpHandler; + // (undocumented) + removeAllInterceptors(): void; + // (undocumented) + removeBasePath(path: string): string; + // (undocumented) + stop(): void; +} + +// @public (undocumented) +export type HttpSetup = HttpServiceBase; + +// @public (undocumented) +export type HttpStart = HttpServiceBase; // @public export interface I18nSetup { Context: ({ children }: { - children: default.ReactNode; + children: React.ReactNode; }) => JSX.Element; } @@ -205,87 +247,25 @@ export interface I18nSetup { export type I18nStart = I18nSetup; // @internal (undocumented) -export interface InjectedMetadataParams { - // (undocumented) - injectedMetadata: { - version: string; - buildNumber: number; - basePath: string; - csp: { - warnLegacyBrowsers: boolean; - }; - vars: { - [key: string]: unknown; - }; - uiPlugins: Array<{ - id: PluginName; - plugin: DiscoveredPlugin; - }>; - legacyMetadata: { - app: unknown; - translations: unknown; - bundleId: string; - nav: LegacyNavLink[]; - version: string; - branch: string; - buildNum: number; - buildSha: string; - basePath: string; - serverName: string; - devMode: boolean; - uiSettings: { - defaults: UiSettingsState; - user?: UiSettingsState; - }; - }; - }; -} - -// @public -export interface InjectedMetadataSetup { - // (undocumented) - getBasePath: () => string; +export interface InternalCoreSetup extends CoreSetup { // (undocumented) - getCspConfig: () => { - warnLegacyBrowsers: boolean; - }; + application: ApplicationSetup; + // Warning: (ae-forgotten-export) The symbol "InjectedMetadataSetup" needs to be exported by the entry point index.d.ts + // // (undocumented) - getInjectedVar: (name: string, defaultValue?: any) => unknown; + injectedMetadata: InjectedMetadataSetup; +} + +// @internal (undocumented) +export interface InternalCoreStart extends CoreStart { // (undocumented) - getInjectedVars: () => { - [key: string]: unknown; - }; + application: ApplicationStart; + // Warning: (ae-forgotten-export) The symbol "InjectedMetadataStart" needs to be exported by the entry point index.d.ts + // // (undocumented) - getKibanaBuildNumber: () => number; - // (undocumented) - getKibanaVersion: () => string; - // (undocumented) - getLegacyMetadata: () => { - app: unknown; - translations: unknown; - bundleId: string; - nav: LegacyNavLink[]; - version: string; - branch: string; - buildNum: number; - buildSha: string; - basePath: string; - serverName: string; - devMode: boolean; - uiSettings: { - defaults: UiSettingsState; - user?: UiSettingsState | undefined; - }; - }; - getPlugins: () => Array<{ - id: string; - plugin: DiscoveredPlugin; - }>; + injectedMetadata: InjectedMetadataStart; } -// @public (undocumented) -export type InjectedMetadataStart = InjectedMetadataSetup; - // @public (undocumented) export interface LegacyNavLink { // (undocumented) @@ -304,12 +284,19 @@ export interface LegacyNavLink { // @public (undocumented) export interface NotificationsSetup { + // Warning: (ae-forgotten-export) The symbol "ToastsSetup" needs to be exported by the entry point index.d.ts + // // (undocumented) - toasts: ToastsApi; + toasts: ToastsSetup; } // @public (undocumented) -export type NotificationsStart = NotificationsSetup; +export interface NotificationsStart { + // Warning: (ae-forgotten-export) The symbol "ToastsStart" needs to be exported by the entry point index.d.ts + // + // (undocumented) + toasts: ToastsStart; +} // Warning: (ae-missing-release-tag) "OverlayRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -321,8 +308,6 @@ export interface OverlayRef { // @public (undocumented) export interface OverlayStart { - // Warning: (ae-forgotten-export) The symbol "React" needs to be exported by the entry point index.d.ts - // // (undocumented) openFlyout: (flyoutChildren: React.ReactNode, flyoutProps?: { closeButtonAriaLabel?: string; @@ -338,9 +323,9 @@ export interface OverlayStart { // @public export interface Plugin = {}, TPluginsStart extends Record = {}> { // (undocumented) - setup: (core: PluginSetupContext, plugins: TPluginsSetup) => TSetup | Promise; + setup: (core: CoreSetup, plugins: TPluginsSetup) => TSetup | Promise; // (undocumented) - start: (core: PluginStartContext, plugins: TPluginsStart) => TStart | Promise; + start: (core: CoreStart, plugins: TPluginsStart) => TStart | Promise; // (undocumented) stop?: () => void; } @@ -352,60 +337,41 @@ export type PluginInitializer; - // (undocumented) - basePath: BasePathStart; - // (undocumented) - chrome: ChromeStart; - // (undocumented) - http: HttpStart; - // (undocumented) - i18n: I18nStart; - // (undocumented) - notifications: NotificationsStart; - // (undocumented) - overlays: OverlayStart; -} +// Warning: (ae-forgotten-export) The symbol "RecursiveReadonlyArray" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export type RecursiveReadonly = T extends (...args: any[]) => any ? T : T extends any[] ? RecursiveReadonlyArray : T extends object ? Readonly<{ + [K in keyof T]: RecursiveReadonly; +}> : T; export { Toast } +// Warning: (ae-forgotten-export) The symbol "ToastInputFields" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -export type ToastInput = string | Pick>; +export type ToastInput = string | ToastInputFields | Promise; // @public (undocumented) export class ToastsApi { + constructor(deps: { + uiSettings: UiSettingsSetup; + i18n: I18nSetup; + }); // (undocumented) add(toastOrTitle: ToastInput): Toast; // (undocumented) addDanger(toastOrTitle: ToastInput): Toast; // (undocumented) + addError(error: Error, options: ErrorToastOptions): Toast; + // (undocumented) addSuccess(toastOrTitle: ToastInput): Toast; // (undocumented) addWarning(toastOrTitle: ToastInput): Toast; // (undocumented) get$(): Rx.Observable; // (undocumented) + registerOverlays(overlays: OverlayStart): void; + // (undocumented) remove(toast: Toast): void; } @@ -450,11 +416,4 @@ export interface UiSettingsState { } -// Warnings were encountered during analysis: -// -// src/core/public/injected_metadata/injected_metadata_service.ts:48:7 - (ae-forgotten-export) The symbol "PluginName" needs to be exported by the entry point index.d.ts -// src/core/public/injected_metadata/injected_metadata_service.ts:49:7 - (ae-forgotten-export) The symbol "DiscoveredPlugin" needs to be exported by the entry point index.d.ts - -// (No @packageDocumentation comment for this package) - ``` diff --git a/src/core/public/ui_settings/__snapshots__/ui_settings_api.test.ts.snap b/src/core/public/ui_settings/__snapshots__/ui_settings_api.test.ts.snap index 7b25c214a58dd..cd55c77526d52 100644 --- a/src/core/public/ui_settings/__snapshots__/ui_settings_api.test.ts.snap +++ b/src/core/public/ui_settings/__snapshots__/ui_settings_api.test.ts.snap @@ -5,12 +5,10 @@ Array [ Array [ "/foo/bar/api/kibana/settings", Object { - "body": "{\\"changes\\":{\\"foo\\":\\"bar\\"}}", - "credentials": "same-origin", "headers": Object { "accept": "application/json", "content-type": "application/json", - "kbn-version": "v9.9.9", + "kbn-version": "kibanaVersion", }, "method": "POST", }, @@ -18,12 +16,10 @@ Array [ Array [ "/foo/bar/api/kibana/settings", Object { - "body": "{\\"changes\\":{\\"bar\\":\\"box\\"}}", - "credentials": "same-origin", "headers": Object { "accept": "application/json", "content-type": "application/json", - "kbn-version": "v9.9.9", + "kbn-version": "kibanaVersion", }, "method": "POST", }, @@ -36,12 +32,10 @@ Array [ Array [ "/foo/bar/api/kibana/settings", Object { - "body": "{\\"changes\\":{\\"foo\\":\\"a\\"}}", - "credentials": "same-origin", "headers": Object { "accept": "application/json", "content-type": "application/json", - "kbn-version": "v9.9.9", + "kbn-version": "kibanaVersion", }, "method": "POST", }, @@ -49,12 +43,10 @@ Array [ Array [ "/foo/bar/api/kibana/settings", Object { - "body": "{\\"changes\\":{\\"foo\\":\\"d\\"}}", - "credentials": "same-origin", "headers": Object { "accept": "application/json", "content-type": "application/json", - "kbn-version": "v9.9.9", + "kbn-version": "kibanaVersion", }, "method": "POST", }, @@ -67,12 +59,10 @@ Array [ Array [ "/foo/bar/api/kibana/settings", Object { - "body": "{\\"changes\\":{\\"foo\\":\\"bar\\"}}", - "credentials": "same-origin", "headers": Object { "accept": "application/json", "content-type": "application/json", - "kbn-version": "v9.9.9", + "kbn-version": "kibanaVersion", }, "method": "POST", }, @@ -80,30 +70,10 @@ Array [ Array [ "/foo/bar/api/kibana/settings", Object { - "body": "{\\"changes\\":{\\"box\\":\\"bar\\"}}", - "credentials": "same-origin", "headers": Object { "accept": "application/json", "content-type": "application/json", - "kbn-version": "v9.9.9", - }, - "method": "POST", - }, - ], -] -`; - -exports[`#batchSet buffers changes while first request is in progress, sends buffered changes after first request completes: initial, only one request 1`] = ` -Array [ - Array [ - "/foo/bar/api/kibana/settings", - Object { - "body": "{\\"changes\\":{\\"foo\\":\\"bar\\"}}", - "credentials": "same-origin", - "headers": Object { - "accept": "application/json", - "content-type": "application/json", - "kbn-version": "v9.9.9", + "kbn-version": "kibanaVersion", }, "method": "POST", }, @@ -134,17 +104,15 @@ exports[`#batchSet rejects on 404 response 1`] = `"Request failed with status co exports[`#batchSet rejects on 500 1`] = `"Request failed with status code: 500"`; -exports[`#batchSet sends a single change immediately: synchronous fetch 1`] = ` +exports[`#batchSet sends a single change immediately: single change 1`] = ` Array [ Array [ "/foo/bar/api/kibana/settings", Object { - "body": "{\\"changes\\":{\\"foo\\":\\"bar\\"}}", - "credentials": "same-origin", "headers": Object { "accept": "application/json", "content-type": "application/json", - "kbn-version": "v9.9.9", + "kbn-version": "kibanaVersion", }, "method": "POST", }, diff --git a/src/core/public/ui_settings/__snapshots__/ui_settings_service.test.ts.snap b/src/core/public/ui_settings/__snapshots__/ui_settings_service.test.ts.snap index fd53826de15dc..edbdef3f05099 100644 --- a/src/core/public/ui_settings/__snapshots__/ui_settings_service.test.ts.snap +++ b/src/core/public/ui_settings/__snapshots__/ui_settings_service.test.ts.snap @@ -5,11 +5,37 @@ exports[`#setup constructs UiSettingsClient and UiSettingsApi: UiSettingsApi arg "calls": Array [ Array [ Object { - "addToPath": [MockFunction], + "addLoadingCount": [MockFunction] { + "calls": Array [ + Array [ + Object { + "loadingCountObservable": true, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], + }, + "delete": [MockFunction], + "fetch": [MockFunction], "get": [MockFunction], - "removeFromPath": [MockFunction], + "getBasePath": [MockFunction], + "getLoadingCount$": [MockFunction], + "head": [MockFunction], + "intercept": [MockFunction], + "options": [MockFunction], + "patch": [MockFunction], + "post": [MockFunction], + "prependBasePath": [MockFunction], + "put": [MockFunction], + "removeAllInterceptors": [MockFunction], + "removeBasePath": [MockFunction], + "stop": [MockFunction], }, - "kibanaVersion", ], ], "results": Array [ diff --git a/src/core/public/ui_settings/ui_settings_api.test.ts b/src/core/public/ui_settings/ui_settings_api.test.ts index b0477b79c459e..048ae2ccbae7f 100644 --- a/src/core/public/ui_settings/ui_settings_api.test.ts +++ b/src/core/public/ui_settings/ui_settings_api.test.ts @@ -22,17 +22,18 @@ import fetchMock from 'fetch-mock/es5/client'; import * as Rx from 'rxjs'; import { takeUntil, toArray } from 'rxjs/operators'; -import { basePathServiceMock } from '../base_path/base_path_service.mock'; +import { setup as httpSetup } from '../../../test_utils/public/http_test_setup'; import { UiSettingsApi } from './ui_settings_api'; function setup() { - const basePath = basePathServiceMock.createSetupContract(); - basePath.addToPath.mockImplementation(path => `/foo/bar${path}`); + const { http } = httpSetup(injectedMetadata => { + injectedMetadata.getBasePath.mockReturnValue('/foo/bar'); + }); - const uiSettingsApi = new UiSettingsApi(basePath, 'v9.9.9'); + const uiSettingsApi = new UiSettingsApi(http); return { - basePath, + http, uiSettingsApi, }; } @@ -56,14 +57,14 @@ afterEach(() => { }); describe('#batchSet', () => { - it('sends a single change immediately', () => { + it('sends a single change immediately', async () => { fetchMock.mock('*', { body: { settings: {} }, }); const { uiSettingsApi } = setup(); - uiSettingsApi.batchSet('foo', 'bar'); - expect(fetchMock.calls()).toMatchSnapshot('synchronous fetch'); + await uiSettingsApi.batchSet('foo', 'bar'); + expect(fetchMock.calls()).toMatchSnapshot('single change'); }); it('buffers changes while first request is in progress, sends buffered changes after first request completes', async () => { @@ -76,7 +77,7 @@ describe('#batchSet', () => { uiSettingsApi.batchSet('foo', 'bar'); const finalPromise = uiSettingsApi.batchSet('box', 'bar'); - expect(fetchMock.calls()).toMatchSnapshot('initial, only one request'); + expect(uiSettingsApi.hasPendingChanges()).toBe(true); await finalPromise; expect(fetchMock.calls()).toMatchSnapshot('final, includes both requests'); }); diff --git a/src/core/public/ui_settings/ui_settings_api.ts b/src/core/public/ui_settings/ui_settings_api.ts index da5c15b1ee71e..33b43107acf1b 100644 --- a/src/core/public/ui_settings/ui_settings_api.ts +++ b/src/core/public/ui_settings/ui_settings_api.ts @@ -19,7 +19,7 @@ import { BehaviorSubject } from 'rxjs'; -import { BasePathSetup } from '../base_path'; +import { HttpSetup } from '../http'; import { UiSettingsState } from './types'; export interface UiSettingsApiResponse { @@ -47,7 +47,7 @@ export class UiSettingsApi { private readonly loadingCount$ = new BehaviorSubject(0); - constructor(private readonly basePath: BasePathSetup, private readonly kibanaVersion: string) {} + constructor(private readonly http: HttpSetup) {} /** * Adds a key+value that will be sent to the server ASAP. If a request is @@ -93,6 +93,13 @@ export class UiSettingsApi { this.loadingCount$.complete(); } + /** + * Report back if there are pending changes waiting to be sent. + */ + public hasPendingChanges() { + return !!(this.pendingChanges && this.sendInProgress); + } + /** * If there are changes that need to be sent to the server and there is not already a * request in progress, this method will start a request sending those changes. Once @@ -115,6 +122,7 @@ export class UiSettingsApi { try { this.sendInProgress = true; + changes.callback( undefined, await this.sendRequest('POST', '/api/kibana/settings', { @@ -131,28 +139,24 @@ export class UiSettingsApi { /** * Calls window.fetch() with the proper headers and error handling logic. - * - * TODO: migrate this to kfetch or whatever the new platform equivalent is once it exists */ - private async sendRequest(method: string, path: string, body: any) { + private async sendRequest(method: string, path: string, body: any): Promise { try { this.loadingCount$.next(this.loadingCount$.getValue() + 1); - const response = await fetch(this.basePath.addToPath(path), { + + return await this.http.fetch(path, { method, body: JSON.stringify(body), headers: { accept: 'application/json', - 'content-type': 'application/json', - 'kbn-version': this.kibanaVersion, }, - credentials: 'same-origin', }); - - if (response.status >= 300) { - throw new Error(`Request failed with status code: ${response.status}`); + } catch (err) { + if (err.response && err.response.status >= 300) { + throw new Error(`Request failed with status code: ${err.response.status}`); } - return await response.json(); + throw err; } finally { this.loadingCount$.next(this.loadingCount$.getValue() - 1); } diff --git a/src/core/public/ui_settings/ui_settings_service.mock.ts b/src/core/public/ui_settings/ui_settings_service.mock.ts index f37174d43ca61..153251623de7f 100644 --- a/src/core/public/ui_settings/ui_settings_service.mock.ts +++ b/src/core/public/ui_settings/ui_settings_service.mock.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import * as Rx from 'rxjs'; import { UiSettingsService, UiSettingsSetup } from './ui_settings_service'; const createSetupContractMock = () => { @@ -35,6 +36,11 @@ const createSetupContractMock = () => { getUpdateErrors$: jest.fn(), stop: jest.fn(), }; + setupContract.get$.mockReturnValue(new Rx.Subject()); + setupContract.getUpdate$.mockReturnValue(new Rx.Subject()); + setupContract.getSaved$.mockReturnValue(new Rx.Subject()); + setupContract.getUpdateErrors$.mockReturnValue(new Rx.Subject()); + // we have to suppress type errors until decide how to mock es6 class return (setupContract as unknown) as UiSettingsSetup; }; diff --git a/src/core/public/ui_settings/ui_settings_service.test.ts b/src/core/public/ui_settings/ui_settings_service.test.ts index 593e2cb5193a9..03b51ae5f6be6 100644 --- a/src/core/public/ui_settings/ui_settings_service.test.ts +++ b/src/core/public/ui_settings/ui_settings_service.test.ts @@ -19,7 +19,6 @@ import { MockUiSettingsApi, MockUiSettingsClient } from './ui_settings_service.test.mocks'; -import { basePathServiceMock } from '../base_path/base_path_service.mock'; import { httpServiceMock } from '../http/http_service.mock'; import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; import { UiSettingsService } from './ui_settings_service'; @@ -29,7 +28,6 @@ const httpSetup = httpServiceMock.createSetupContract(); const defaultDeps = { http: httpSetup, injectedMetadata: injectedMetadataServiceMock.createSetupContract(), - basePath: basePathServiceMock.createSetupContract(), }; afterEach(() => { diff --git a/src/core/public/ui_settings/ui_settings_service.ts b/src/core/public/ui_settings/ui_settings_service.ts index e7e28d3ee4dca..ea287d888fa37 100644 --- a/src/core/public/ui_settings/ui_settings_service.ts +++ b/src/core/public/ui_settings/ui_settings_service.ts @@ -17,7 +17,6 @@ * under the License. */ -import { BasePathSetup } from '../base_path'; import { HttpSetup } from '../http'; import { InjectedMetadataSetup } from '../injected_metadata'; @@ -27,7 +26,6 @@ import { UiSettingsClient } from './ui_settings_client'; interface UiSettingsServiceDeps { http: HttpSetup; injectedMetadata: InjectedMetadataSetup; - basePath: BasePathSetup; } /** @internal */ @@ -35,8 +33,8 @@ export class UiSettingsService { private uiSettingsApi?: UiSettingsApi; private uiSettingsClient?: UiSettingsClient; - public setup({ http, injectedMetadata, basePath }: UiSettingsServiceDeps): UiSettingsSetup { - this.uiSettingsApi = new UiSettingsApi(basePath, injectedMetadata.getKibanaVersion()); + public setup({ http, injectedMetadata }: UiSettingsServiceDeps): UiSettingsSetup { + this.uiSettingsApi = new UiSettingsApi(http); http.addLoadingCount(this.uiSettingsApi.getLoadingCount$()); // TODO: Migrate away from legacyMetadata https://github.com/elastic/kibana/issues/22779 diff --git a/src/core/public/utils/integration_tests/__fixtures__/frozen_object_mutation/index.ts b/src/core/public/utils/integration_tests/__fixtures__/frozen_object_mutation/index.ts index 9cf394fc2b0be..fd5e16987e2e6 100644 --- a/src/core/public/utils/integration_tests/__fixtures__/frozen_object_mutation/index.ts +++ b/src/core/public/utils/integration_tests/__fixtures__/frozen_object_mutation/index.ts @@ -17,7 +17,7 @@ * under the License. */ -import { deepFreeze } from '../../../deep_freeze'; +import { deepFreeze } from '../../../../../utils/deep_freeze'; deepFreeze( { 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 64fcbf1f74881..12307c46b95fa 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/core/server/config/__snapshots__/config_service.test.ts.snap b/src/core/server/config/__snapshots__/config_service.test.ts.snap index 9327b80dc79a0..1f5c442e1ae2f 100644 --- a/src/core/server/config/__snapshots__/config_service.test.ts.snap +++ b/src/core/server/config/__snapshots__/config_service.test.ts.snap @@ -1,14 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`correctly passes context 1`] = ` -ExampleClassWithSchema { - "value": Object { - "branchRef": "feature-v1", - "buildNumRef": 100, - "buildShaRef": "feature-v1-build-sha", - "devRef": true, - "prodRef": false, - "versionRef": "v1", - }, +Object { + "branchRef": "feature-v1", + "buildNumRef": 100, + "buildShaRef": "feature-v1-build-sha", + "devRef": true, + "prodRef": false, + "versionRef": "v1", } `; diff --git a/src/core/server/config/config.test.ts b/src/core/server/config/config.test.ts new file mode 100644 index 0000000000000..0748ec65302d5 --- /dev/null +++ b/src/core/server/config/config.test.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { hasConfigPathIntersection } from './config'; + +describe('hasConfigPathIntersection()', () => { + test('Should return true if leaf is descendent to the root', () => { + expect(hasConfigPathIntersection('a.b', 'a.b')).toBe(true); + expect(hasConfigPathIntersection('a.b.c', 'a')).toBe(true); + expect(hasConfigPathIntersection('a.b.c.d', 'a.b')).toBe(true); + }); + test('Should return false if leaf is not descendent to the root', () => { + expect(hasConfigPathIntersection('a.bc', 'a.b')).toBe(false); + expect(hasConfigPathIntersection('a', 'a.b')).toBe(false); + }); + test('Should throw if either path is empty', () => { + expect(() => hasConfigPathIntersection('a', '')).toThrow(); + expect(() => hasConfigPathIntersection('', 'a')).toThrow(); + expect(() => hasConfigPathIntersection('', '')).toThrow(); + }); +}); diff --git a/src/core/server/config/config.ts b/src/core/server/config/config.ts index b34b31493fb67..f054817fe9ee6 100644 --- a/src/core/server/config/config.ts +++ b/src/core/server/config/config.ts @@ -75,3 +75,16 @@ export interface Config { */ toRaw(): Record; } + +const pathDelimiter = '.'; +export function hasConfigPathIntersection(leafPath: string, rootPath: string) { + if (!leafPath) { + throw new Error('leafPath cannot be empty'); + } + if (!rootPath) { + throw new Error('rootPath cannot be empty'); + } + const leafSegments = leafPath.split(pathDelimiter); + const rootSegments = rootPath.split(pathDelimiter); + return rootSegments.every((rootSegment, index) => leafSegments[index] === rootSegment); +} diff --git a/src/core/server/config/config_service.test.ts b/src/core/server/config/config_service.test.ts index 27c9473c16324..61da9af7baa7c 100644 --- a/src/core/server/config/config_service.test.ts +++ b/src/core/server/config/config_service.test.ts @@ -19,12 +19,12 @@ /* eslint-disable max-classes-per-file */ -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, Observable } from 'rxjs'; import { first } from 'rxjs/operators'; import { mockPackage } from './config_service.test.mocks'; -import { schema, Type, TypeOf } from '@kbn/config-schema'; +import { schema } from '@kbn/config-schema'; import { ConfigService, Env, ObjectToConfigAdapter } from '.'; import { loggingServiceMock } from '../logging/logging_service.mock'; @@ -34,21 +34,17 @@ const emptyArgv = getEnvOptions(); const defaultEnv = new Env('/kibana', emptyArgv); const logger = loggingServiceMock.create(); -class ExampleClassWithStringSchema { - public static schema = schema.string(); - - constructor(readonly value: string) {} -} - test('returns config at path as observable', async () => { const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 'foo' })); const configService = new ConfigService(config$, defaultEnv, logger); - await configService.setSchema('key', schema.string()); + const stringSchema = schema.string(); + await configService.setSchema('key', stringSchema); - const configs = configService.atPath('key', ExampleClassWithStringSchema); - const exampleConfig = await configs.pipe(first()).toPromise(); + const value$ = configService.atPath('key'); + expect(value$).toBeInstanceOf(Observable); - expect(exampleConfig.value).toBe('foo'); + const value = await value$.pipe(first()).toPromise(); + expect(value).toBe('foo'); }); test('throws if config at path does not match schema', async () => { @@ -70,9 +66,9 @@ test('re-validate config when updated', async () => { configService.setSchema('key', schema.string()); const valuesReceived: any[] = []; - await configService.atPath('key', ExampleClassWithStringSchema).subscribe( - config => { - valuesReceived.push(config.value); + await configService.atPath('key').subscribe( + value => { + valuesReceived.push(value); }, error => { valuesReceived.push(error); @@ -93,10 +89,10 @@ test("returns undefined if fetching optional config at a path that doesn't exist const config$ = new BehaviorSubject(new ObjectToConfigAdapter({})); const configService = new ConfigService(config$, defaultEnv, logger); - const configs = configService.optionalAtPath('unique-name', ExampleClassWithStringSchema); - const exampleConfig = await configs.pipe(first()).toPromise(); + const value$ = configService.optionalAtPath('unique-name'); + const value = await value$.pipe(first()).toPromise(); - expect(exampleConfig).toBeUndefined(); + expect(value).toBeUndefined(); }); test('returns observable config at optional path if it exists', async () => { @@ -104,11 +100,10 @@ test('returns observable config at optional path if it exists', async () => { const configService = new ConfigService(config$, defaultEnv, logger); await configService.setSchema('value', schema.string()); - const configs = configService.optionalAtPath('value', ExampleClassWithStringSchema); - const exampleConfig: any = await configs.pipe(first()).toPromise(); + const value$ = configService.optionalAtPath('value'); + const value: any = await value$.pipe(first()).toPromise(); - expect(exampleConfig).toBeDefined(); - expect(exampleConfig.value).toBe('bar'); + expect(value).toBe('bar'); }); test("does not push new configs when reloading if config at path hasn't changed", async () => { @@ -117,8 +112,8 @@ test("does not push new configs when reloading if config at path hasn't changed" await configService.setSchema('key', schema.string()); const valuesReceived: any[] = []; - configService.atPath('key', ExampleClassWithStringSchema).subscribe(config => { - valuesReceived.push(config.value); + configService.atPath('key').subscribe(value => { + valuesReceived.push(value); }); config$.next(new ObjectToConfigAdapter({ key: 'value' })); @@ -132,8 +127,8 @@ test('pushes new config when reloading and config at path has changed', async () await configService.setSchema('key', schema.string()); const valuesReceived: any[] = []; - configService.atPath('key', ExampleClassWithStringSchema).subscribe(config => { - valuesReceived.push(config.value); + configService.atPath('key').subscribe(value => { + valuesReceived.push(value); }); config$.next(new ObjectToConfigAdapter({ key: 'new value' })); @@ -142,12 +137,10 @@ test('pushes new config when reloading and config at path has changed', async () }); test("throws error if 'schema' is not defined for a key", async () => { - class ExampleClass {} - const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 'value' })); const configService = new ConfigService(config$, defaultEnv, logger); - const configs = configService.atPath('key', ExampleClass as any); + const configs = configService.atPath('key'); await expect(configs.pipe(first()).toPromise()).rejects.toMatchInlineSnapshot( `[Error: No validation schema has been defined for key]` @@ -188,15 +181,8 @@ test('tracks unhandled paths', async () => { const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig)); const configService = new ConfigService(config$, defaultEnv, logger); - configService.atPath('foo', createClassWithSchema(schema.string())); - configService.atPath( - ['bar', 'deep2'], - createClassWithSchema( - schema.object({ - key: schema.string(), - }) - ) - ); + configService.atPath('foo'); + configService.atPath(['bar', 'deep2']); const unused = await configService.getUnusedPaths(); @@ -235,9 +221,9 @@ test('correctly passes context', async () => { }); const configService = new ConfigService(config$, env, logger); await configService.setSchema('foo', schemaDefinition); - const configs = configService.atPath('foo', createClassWithSchema(schemaDefinition)); + const value$ = configService.atPath('foo'); - expect(await configs.pipe(first()).toPromise()).toMatchSnapshot(); + expect(await value$.pipe(first()).toPromise()).toMatchSnapshot(); }); test('handles enabled path, but only marks the enabled path as used', async () => { @@ -306,11 +292,3 @@ test('treats config as enabled if config path is not present in config', async ( const unusedPaths = await configService.getUnusedPaths(); expect(unusedPaths).toEqual([]); }); - -function createClassWithSchema(s: Type) { - return class ExampleClassWithSchema { - public static schema = s; - - constructor(readonly value: TypeOf) {} - }; -} diff --git a/src/core/server/config/config_service.ts b/src/core/server/config/config_service.ts index dd3be624ebf51..fff19aa3af0f0 100644 --- a/src/core/server/config/config_service.ts +++ b/src/core/server/config/config_service.ts @@ -22,8 +22,9 @@ import { isEqual } from 'lodash'; import { Observable } from 'rxjs'; import { distinctUntilChanged, first, map } from 'rxjs/operators'; -import { Config, ConfigPath, ConfigWithSchema, Env } from '.'; +import { Config, ConfigPath, Env } from '.'; import { Logger, LoggerFactory } from '../logging'; +import { hasConfigPathIntersection } from './config'; /** @internal */ export class ConfigService { @@ -73,14 +74,9 @@ export class ConfigService { * against the static `schema` on the given `ConfigClass`. * * @param path - The path to the desired subset of the config. - * @param ConfigClass - A class (not an instance of a class) that contains a - * static `schema` that we validate the config at the given `path` against. */ - public atPath, TConfig>( - path: ConfigPath, - ConfigClass: ConfigWithSchema - ) { - return this.validateConfig(path).pipe(map(config => this.createConfig(config, ConfigClass))); + public atPath(path: ConfigPath) { + return this.validateConfig(path) as Observable; } /** @@ -89,15 +85,11 @@ export class ConfigService { * * {@link ConfigService.atPath} */ - public optionalAtPath, TConfig>( - path: ConfigPath, - ConfigClass: ConfigWithSchema - ) { + public optionalAtPath(path: ConfigPath) { return this.getDistinctConfig(path).pipe( map(config => { if (config === undefined) return undefined; - const validatedConfig = this.validate(path, config); - return this.createConfig(validatedConfig, ConfigClass); + return this.validate(path, config) as TSchema; }) ); } @@ -156,13 +148,6 @@ export class ConfigService { ); } - private createConfig, TConfig>( - validatedConfig: unknown, - ConfigClass: ConfigWithSchema - ) { - return new ConfigClass(validatedConfig, this.env); - } - private validateConfig(path: ConfigPath) { return this.getDistinctConfig(path).pipe(map(config => this.validate(path, config))); } @@ -196,4 +181,4 @@ const pathToString = (path: ConfigPath) => (Array.isArray(path) ? path.join('.') * handled paths. */ const isPathHandled = (path: string, handledPaths: string[]) => - handledPaths.some(handledPath => path.startsWith(handledPath)); + handledPaths.some(handledPath => hasConfigPathIntersection(path, handledPath)); diff --git a/src/core/server/config/config_with_schema.ts b/src/core/server/config/config_with_schema.ts deleted file mode 100644 index 1c392b93b6a75..0000000000000 --- a/src/core/server/config/config_with_schema.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -// TODO inline all of these -import { Type, TypeOf } from '@kbn/config-schema'; -import { Env } from './env'; - -/** - * Interface that defines the static side of a config class. - * - * (Remember that a class has two types: the type of the static side and the - * type of the instance side, see https://www.typescriptlang.org/docs/handbook/interfaces.html#difference-between-the-static-and-instance-sides-of-classes) - * - * This can't be used to define the config class because of how interfaces work - * in TypeScript, but it can be used to ensure we have a config class that - * matches whenever it's used. - */ -export interface ConfigWithSchema, Config> { - /** - * Any config class must define a schema that validates the config, based on - * the injected `schema` helper. - */ - schema: S; - - /** - * @param validatedConfig The result of validating the static `schema` above. - * @param env An instance of the `Env` class that defines environment specific - * variables. - */ - new (validatedConfig: TypeOf, env: Env): Config; -} diff --git a/src/core/server/config/index.ts b/src/core/server/config/index.ts index 3c7bce831cdf9..257263069cabd 100644 --- a/src/core/server/config/index.ts +++ b/src/core/server/config/index.ts @@ -19,9 +19,8 @@ export { ConfigService } from './config_service'; export { RawConfigService } from './raw_config_service'; -export { Config, ConfigPath, isConfigPath } from './config'; +export { Config, ConfigPath, isConfigPath, hasConfigPathIntersection } from './config'; export { ObjectToConfigAdapter } from './object_to_config_adapter'; export { CliArgs } from './env'; export { Env, EnvironmentMode, PackageInfo } from './env'; -export { ConfigWithSchema } from './config_with_schema'; diff --git a/src/core/server/dev/dev_config.ts b/src/core/server/dev/dev_config.ts index 6c99025da3fc0..1fb84e2e6625d 100644 --- a/src/core/server/dev/dev_config.ts +++ b/src/core/server/dev/dev_config.ts @@ -19,24 +19,18 @@ import { schema, TypeOf } from '@kbn/config-schema'; -const createDevSchema = schema.object({ - basePathProxyTarget: schema.number({ - defaultValue: 5603, - }), -}); - export const config = { path: 'dev', - schema: createDevSchema, + schema: schema.object({ + basePathProxyTarget: schema.number({ + defaultValue: 5603, + }), + }), }; -export type DevConfigType = TypeOf; -export class DevConfig { - /** - * @internal - */ - public static schema = createDevSchema; +export type DevConfigType = TypeOf; +export class DevConfig { public basePathProxyTargetPort: number; /** diff --git a/src/core/server/dev/index.ts b/src/core/server/dev/index.ts index cccd6b1c5d07f..d55ae05b239fc 100644 --- a/src/core/server/dev/index.ts +++ b/src/core/server/dev/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { DevConfig, config } from './dev_config'; +export { config, DevConfig, DevConfigType } from './dev_config'; diff --git a/src/core/server/elasticsearch/elasticsearch_config.test.ts b/src/core/server/elasticsearch/elasticsearch_config.test.ts index b97370eafa040..1a6a8929cd6a0 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.test.ts @@ -17,11 +17,11 @@ * under the License. */ -import { ElasticsearchConfig } from './elasticsearch_config'; +import { ElasticsearchConfig, config } from './elasticsearch_config'; test('set correct defaults', () => { - const config = new ElasticsearchConfig(ElasticsearchConfig.schema.validate({})); - expect(config).toMatchInlineSnapshot(` + const configValue = new ElasticsearchConfig(config.schema.validate({})); + expect(configValue).toMatchInlineSnapshot(` ElasticsearchConfig { "apiVersion": "master", "customHeaders": Object {}, @@ -51,58 +51,58 @@ ElasticsearchConfig { }); test('#hosts accepts both string and array of strings', () => { - let config = new ElasticsearchConfig( - ElasticsearchConfig.schema.validate({ hosts: 'http://some.host:1234' }) + let configValue = new ElasticsearchConfig( + config.schema.validate({ hosts: 'http://some.host:1234' }) ); - expect(config.hosts).toEqual(['http://some.host:1234']); + expect(configValue.hosts).toEqual(['http://some.host:1234']); - config = new ElasticsearchConfig( - ElasticsearchConfig.schema.validate({ hosts: ['http://some.host:1234'] }) + configValue = new ElasticsearchConfig( + config.schema.validate({ hosts: ['http://some.host:1234'] }) ); - expect(config.hosts).toEqual(['http://some.host:1234']); + expect(configValue.hosts).toEqual(['http://some.host:1234']); - config = new ElasticsearchConfig( - ElasticsearchConfig.schema.validate({ + configValue = new ElasticsearchConfig( + config.schema.validate({ hosts: ['http://some.host:1234', 'https://some.another.host'], }) ); - expect(config.hosts).toEqual(['http://some.host:1234', 'https://some.another.host']); + expect(configValue.hosts).toEqual(['http://some.host:1234', 'https://some.another.host']); }); test('#requestHeadersWhitelist accepts both string and array of strings', () => { - let config = new ElasticsearchConfig( - ElasticsearchConfig.schema.validate({ requestHeadersWhitelist: 'token' }) + let configValue = new ElasticsearchConfig( + config.schema.validate({ requestHeadersWhitelist: 'token' }) ); - expect(config.requestHeadersWhitelist).toEqual(['token']); + expect(configValue.requestHeadersWhitelist).toEqual(['token']); - config = new ElasticsearchConfig( - ElasticsearchConfig.schema.validate({ requestHeadersWhitelist: ['token'] }) + configValue = new ElasticsearchConfig( + config.schema.validate({ requestHeadersWhitelist: ['token'] }) ); - expect(config.requestHeadersWhitelist).toEqual(['token']); + expect(configValue.requestHeadersWhitelist).toEqual(['token']); - config = new ElasticsearchConfig( - ElasticsearchConfig.schema.validate({ + configValue = new ElasticsearchConfig( + config.schema.validate({ requestHeadersWhitelist: ['token', 'X-Forwarded-Proto'], }) ); - expect(config.requestHeadersWhitelist).toEqual(['token', 'X-Forwarded-Proto']); + expect(configValue.requestHeadersWhitelist).toEqual(['token', 'X-Forwarded-Proto']); }); test('#ssl.certificateAuthorities accepts both string and array of strings', () => { - let config = new ElasticsearchConfig( - ElasticsearchConfig.schema.validate({ ssl: { certificateAuthorities: 'some-path' } }) + let configValue = new ElasticsearchConfig( + config.schema.validate({ ssl: { certificateAuthorities: 'some-path' } }) ); - expect(config.ssl.certificateAuthorities).toEqual(['some-path']); + expect(configValue.ssl.certificateAuthorities).toEqual(['some-path']); - config = new ElasticsearchConfig( - ElasticsearchConfig.schema.validate({ ssl: { certificateAuthorities: ['some-path'] } }) + configValue = new ElasticsearchConfig( + config.schema.validate({ ssl: { certificateAuthorities: ['some-path'] } }) ); - expect(config.ssl.certificateAuthorities).toEqual(['some-path']); + expect(configValue.ssl.certificateAuthorities).toEqual(['some-path']); - config = new ElasticsearchConfig( - ElasticsearchConfig.schema.validate({ + configValue = new ElasticsearchConfig( + config.schema.validate({ ssl: { certificateAuthorities: ['some-path', 'another-path'] }, }) ); - expect(config.ssl.certificateAuthorities).toEqual(['some-path', 'another-path']); + expect(configValue.ssl.certificateAuthorities).toEqual(['some-path', 'another-path']); }); diff --git a/src/core/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts index a2b3e03e2cbec..fb585a8d67262 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.ts @@ -24,53 +24,51 @@ const hostURISchema = schema.uri({ scheme: ['http', 'https'] }); export const DEFAULT_API_VERSION = 'master'; -const configSchema = schema.object({ - sniffOnStart: schema.boolean({ defaultValue: false }), - sniffInterval: schema.oneOf([schema.duration(), schema.literal(false)], { defaultValue: false }), - sniffOnConnectionFault: schema.boolean({ defaultValue: false }), - hosts: schema.oneOf([hostURISchema, schema.arrayOf(hostURISchema, { minSize: 1 })], { - defaultValue: 'http://localhost:9200', - }), - preserveHost: schema.boolean({ defaultValue: true }), - username: schema.maybe(schema.string()), - password: schema.maybe(schema.string()), - requestHeadersWhitelist: schema.oneOf([schema.string(), schema.arrayOf(schema.string())], { - defaultValue: ['authorization'], - }), - customHeaders: schema.recordOf(schema.string(), schema.string(), { defaultValue: {} }), - shardTimeout: schema.duration({ defaultValue: '30s' }), - requestTimeout: schema.duration({ defaultValue: '30s' }), - pingTimeout: schema.duration({ defaultValue: schema.siblingRef('requestTimeout') }), - startupTimeout: schema.duration({ defaultValue: '5s' }), - logQueries: schema.boolean({ defaultValue: false }), - ssl: schema.object({ - verificationMode: schema.oneOf( - [schema.literal('none'), schema.literal('certificate'), schema.literal('full')], - { defaultValue: 'full' } - ), - certificateAuthorities: schema.maybe( - schema.oneOf([schema.string(), schema.arrayOf(schema.string(), { minSize: 1 })]) - ), - certificate: schema.maybe(schema.string()), - key: schema.maybe(schema.string()), - keyPassphrase: schema.maybe(schema.string()), - alwaysPresentCertificate: schema.boolean({ defaultValue: true }), - }), - apiVersion: schema.string({ defaultValue: DEFAULT_API_VERSION }), - healthCheck: schema.object({ delay: schema.duration({ defaultValue: 2500 }) }), -}); - -export type ElasticsearchConfigType = TypeOf; +export type ElasticsearchConfigType = TypeOf; type SslConfigSchema = ElasticsearchConfigType['ssl']; export const config = { path: 'elasticsearch', - schema: configSchema, + schema: schema.object({ + sniffOnStart: schema.boolean({ defaultValue: false }), + sniffInterval: schema.oneOf([schema.duration(), schema.literal(false)], { + defaultValue: false, + }), + sniffOnConnectionFault: schema.boolean({ defaultValue: false }), + hosts: schema.oneOf([hostURISchema, schema.arrayOf(hostURISchema, { minSize: 1 })], { + defaultValue: 'http://localhost:9200', + }), + preserveHost: schema.boolean({ defaultValue: true }), + username: schema.maybe(schema.string()), + password: schema.maybe(schema.string()), + requestHeadersWhitelist: schema.oneOf([schema.string(), schema.arrayOf(schema.string())], { + defaultValue: ['authorization'], + }), + customHeaders: schema.recordOf(schema.string(), schema.string(), { defaultValue: {} }), + shardTimeout: schema.duration({ defaultValue: '30s' }), + requestTimeout: schema.duration({ defaultValue: '30s' }), + pingTimeout: schema.duration({ defaultValue: schema.siblingRef('requestTimeout') }), + startupTimeout: schema.duration({ defaultValue: '5s' }), + logQueries: schema.boolean({ defaultValue: false }), + ssl: schema.object({ + verificationMode: schema.oneOf( + [schema.literal('none'), schema.literal('certificate'), schema.literal('full')], + { defaultValue: 'full' } + ), + certificateAuthorities: schema.maybe( + schema.oneOf([schema.string(), schema.arrayOf(schema.string(), { minSize: 1 })]) + ), + certificate: schema.maybe(schema.string()), + key: schema.maybe(schema.string()), + keyPassphrase: schema.maybe(schema.string()), + alwaysPresentCertificate: schema.boolean({ defaultValue: true }), + }), + apiVersion: schema.string({ defaultValue: DEFAULT_API_VERSION }), + healthCheck: schema.object({ delay: schema.duration({ defaultValue: 2500 }) }), + }), }; export class ElasticsearchConfig { - public static schema = configSchema; - /** * The interval between health check requests Kibana sends to the Elasticsearch. */ diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts index 41e72a11a7ee1..901ab78130480 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts @@ -33,13 +33,11 @@ import { ElasticsearchService } from './elasticsearch_service'; let elasticsearchService: ElasticsearchService; const configService = configServiceMock.create(); configService.atPath.mockReturnValue( - new BehaviorSubject( - new ElasticsearchConfig({ - hosts: ['http://1.2.3.4'], - healthCheck: {}, - ssl: {}, - } as any) - ) + new BehaviorSubject({ + hosts: ['http://1.2.3.4'], + healthCheck: {}, + ssl: {}, + } as any) ); let env: Env; diff --git a/src/core/server/elasticsearch/elasticsearch_service.ts b/src/core/server/elasticsearch/elasticsearch_service.ts index 4ede7d6af1453..b3faab892bd97 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.ts @@ -24,7 +24,7 @@ import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { ClusterClient } from './cluster_client'; import { ElasticsearchClientConfig } from './elasticsearch_client_config'; -import { ElasticsearchConfig } from './elasticsearch_config'; +import { ElasticsearchConfig, ElasticsearchConfigType } from './elasticsearch_config'; /** @internal */ interface CoreClusterClients { @@ -48,49 +48,51 @@ export interface ElasticsearchServiceSetup { /** @internal */ export class ElasticsearchService implements CoreService { private readonly log: Logger; + private readonly config$: Observable; private subscription?: Subscription; constructor(private readonly coreContext: CoreContext) { this.log = coreContext.logger.get('elasticsearch-service'); + this.config$ = coreContext.configService + .atPath('elasticsearch') + .pipe(map(rawConfig => new ElasticsearchConfig(rawConfig))); } public async setup(): Promise { this.log.debug('Setting up elasticsearch service'); - const clients$ = this.coreContext.configService - .atPath('elasticsearch', ElasticsearchConfig) - .pipe( - filter(() => { - if (this.subscription !== undefined) { - this.log.error('Clients cannot be changed after they are created'); - return false; - } - - return true; - }), - switchMap( - config => - new Observable(subscriber => { - this.log.debug(`Creating elasticsearch clients`); - - const coreClients = { - config, - adminClient: this.createClusterClient('admin', config), - dataClient: this.createClusterClient('data', config), - }; - - subscriber.next(coreClients); - - return () => { - this.log.debug(`Closing elasticsearch clients`); - - coreClients.adminClient.close(); - coreClients.dataClient.close(); - }; - }) - ), - publishReplay(1) - ) as ConnectableObservable; + const clients$ = this.config$.pipe( + filter(() => { + if (this.subscription !== undefined) { + this.log.error('Clients cannot be changed after they are created'); + return false; + } + + return true; + }), + switchMap( + config => + new Observable(subscriber => { + this.log.debug(`Creating elasticsearch clients`); + + const coreClients = { + config, + adminClient: this.createClusterClient('admin', config), + dataClient: this.createClusterClient('data', config), + }; + + subscriber.next(coreClients); + + return () => { + this.log.debug(`Closing elasticsearch clients`); + + coreClients.adminClient.close(); + coreClients.dataClient.close(); + }; + }) + ), + publishReplay(1) + ) as ConnectableObservable; this.subscription = clients$.connect(); diff --git a/src/core/server/elasticsearch/scoped_cluster_client.test.ts b/src/core/server/elasticsearch/scoped_cluster_client.test.ts index 6879d86ce2cdf..04c13f85a1ef7 100644 --- a/src/core/server/elasticsearch/scoped_cluster_client.test.ts +++ b/src/core/server/elasticsearch/scoped_cluster_client.test.ts @@ -106,6 +106,17 @@ describe('#callAsCurrentUser', () => { ); scopedAPICaller.mockClear(); + await expect( + clusterClient.callAsCurrentUser('security.authenticate', { some: 'some' }) + ).resolves.toBe(mockResponse); + expect(scopedAPICaller).toHaveBeenCalledTimes(1); + expect(scopedAPICaller).toHaveBeenCalledWith( + 'security.authenticate', + { some: 'some', headers: { one: '1' } }, + undefined + ); + scopedAPICaller.mockClear(); + await expect( clusterClient.callAsCurrentUser('ping', undefined, { wrap401Errors: true }) ).resolves.toBe(mockResponse); @@ -120,10 +131,11 @@ describe('#callAsCurrentUser', () => { await expect( clusterClient.callAsCurrentUser( 'security.authenticate', - { some: 'some', headers: { one: '1' } }, + { some: 'some' }, { wrap401Errors: true } ) ).resolves.toBe(mockResponse); + expect(scopedAPICaller).toHaveBeenCalledTimes(1); expect(scopedAPICaller).toHaveBeenCalledWith( 'security.authenticate', @@ -134,6 +146,31 @@ describe('#callAsCurrentUser', () => { expect(internalAPICaller).not.toHaveBeenCalled(); }); + test('callAsCurrentUser allows passing additional headers', async () => { + const mockResponse = { data: 'response' }; + scopedAPICaller.mockResolvedValue(mockResponse); + await expect( + clusterClient.callAsCurrentUser('security.authenticate', { + some: 'some', + headers: { additionalHeader: 'Oh Yes!' }, + }) + ).resolves.toBe(mockResponse); + expect(scopedAPICaller).toHaveBeenCalledTimes(1); + expect(scopedAPICaller).toHaveBeenCalledWith( + 'security.authenticate', + { some: 'some', headers: { one: '1', additionalHeader: 'Oh Yes!' } }, + undefined + ); + }); + + test('callAsCurrentUser cannot override default headers', async () => { + const expectedErrorResponse = new Error('Cannot override default header one.'); + const withHeaderOverride = async () => + clusterClient.callAsCurrentUser('security.authenticate', { headers: { one: 'OVERRIDE' } }); + await expect(withHeaderOverride()).rejects.toThrowError(expectedErrorResponse); + expect(scopedAPICaller).toHaveBeenCalledTimes(0); + }); + test('properly forwards errors returned by the API caller', async () => { const mockErrorResponse = new Error('some-error'); scopedAPICaller.mockRejectedValue(mockErrorResponse); @@ -152,8 +189,8 @@ describe('#callAsCurrentUser', () => { await expect(clusterClientWithoutHeaders.callAsCurrentUser('ping')).resolves.toBe(mockResponse); expect(scopedAPICaller).toHaveBeenCalledTimes(1); expect(scopedAPICaller).toHaveBeenCalledWith('ping', {}, undefined); - scopedAPICaller.mockClear(); + scopedAPICaller.mockClear(); await expect( clusterClientWithoutHeaders.callAsCurrentUser('security.authenticate', { some: 'some' }) ).resolves.toBe(mockResponse); diff --git a/src/core/server/elasticsearch/scoped_cluster_client.ts b/src/core/server/elasticsearch/scoped_cluster_client.ts index 6e4075cbfaded..b481ade9d1bab 100644 --- a/src/core/server/elasticsearch/scoped_cluster_client.ts +++ b/src/core/server/elasticsearch/scoped_cluster_client.ts @@ -17,6 +17,7 @@ * under the License. */ +import { intersection, isObject } from 'lodash'; import { Headers } from '../http/router'; import { CallAPIOptions } from './cluster_client'; @@ -72,10 +73,20 @@ export class ScopedClusterClient { clientParams: Record = {}, options?: CallAPIOptions ) { - if (this.headers !== undefined) { - clientParams.headers = this.headers; - } + const defaultHeaders = this.headers; + if (defaultHeaders !== undefined) { + const customHeaders: any = clientParams.headers; + if (isObject(customHeaders)) { + const duplicates = intersection(Object.keys(defaultHeaders), Object.keys(customHeaders)); + duplicates.forEach(duplicate => { + if (defaultHeaders[duplicate] !== (customHeaders as any)[duplicate]) { + throw Error(`Cannot override default header ${duplicate}.`); + } + }); + } + clientParams.headers = Object.assign({}, clientParams.headers, this.headers); + } return this.scopedAPICaller(endpoint, clientParams, options); } } diff --git a/src/core/server/http/auth_state_storage.ts b/src/core/server/http/auth_state_storage.ts new file mode 100644 index 0000000000000..bd7bf1e62968c --- /dev/null +++ b/src/core/server/http/auth_state_storage.ts @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Request } from 'hapi'; +import { KibanaRequest, toRawRequest } from './router'; + +export enum AuthStatus { + authenticated = 'authenticated', + unauthenticated = 'unauthenticated', + unknown = 'unknown', +} + +const getIncomingMessage = (request: KibanaRequest | Request) => + request instanceof KibanaRequest ? toRawRequest(request).raw.req : request.raw.req; + +export class AuthStateStorage { + private readonly storage = new WeakMap, unknown>(); + constructor(private readonly canBeAuthenticated: () => boolean) {} + public set = (request: KibanaRequest | Request, state: unknown) => { + this.storage.set(getIncomingMessage(request), state); + }; + public get = (request: KibanaRequest | Request) => { + const key = getIncomingMessage(request); + const state = this.storage.get(key); + const status: AuthStatus = this.storage.has(key) + ? AuthStatus.authenticated + : this.canBeAuthenticated() + ? AuthStatus.unauthenticated + : AuthStatus.unknown; + + return { status, state }; + }; + public isAuthenticated = (request: KibanaRequest | Request) => { + return this.get(request).status === AuthStatus.authenticated; + }; +} diff --git a/src/core/server/http/cookie_session_storage.ts b/src/core/server/http/cookie_session_storage.ts index 7301de6315606..f0cd50053cf14 100644 --- a/src/core/server/http/cookie_session_storage.ts +++ b/src/core/server/http/cookie_session_storage.ts @@ -19,6 +19,8 @@ import { Request, Server } from 'hapi'; import hapiAuthCookie from 'hapi-auth-cookie'; + +import { KibanaRequest, toRawRequest } from './router'; import { SessionStorageFactory, SessionStorage } from './session_storage'; export interface SessionStorageCookieOptions { @@ -29,10 +31,10 @@ export interface SessionStorageCookieOptions { } class ScopedCookieSessionStorage> implements SessionStorage { - constructor(private readonly server: Server, private readonly request: Request) {} + constructor(private readonly server: Server, private readonly request: Readonly) {} public async get(): Promise { try { - return await this.server.auth.test('security-cookie', this.request); + return await this.server.auth.test('security-cookie', this.request as Request); } catch (error) { return null; } @@ -71,8 +73,9 @@ export async function createCookieSessionStorageFactory( }); return { - asScoped(request: Request) { - return new ScopedCookieSessionStorage(server, request); + asScoped(request: Readonly | KibanaRequest) { + const req = request instanceof KibanaRequest ? toRawRequest(request) : request; + return new ScopedCookieSessionStorage(server, req); }, }; } diff --git a/src/core/server/http/http_config.test.ts b/src/core/server/http/http_config.test.ts index 54d28ef921fcf..35f3db9fb97c6 100644 --- a/src/core/server/http/http_config.test.ts +++ b/src/core/server/http/http_config.test.ts @@ -17,25 +17,25 @@ * under the License. */ -import { HttpConfig } from '.'; +import { config } from '.'; test('has defaults for config', () => { - const httpSchema = HttpConfig.schema; + const httpSchema = config.schema; const obj = {}; expect(httpSchema.validate(obj)).toMatchSnapshot(); }); test('accepts valid hostnames', () => { - const { host: host1 } = HttpConfig.schema.validate({ host: 'www.example.com' }); - const { host: host2 } = HttpConfig.schema.validate({ host: '8.8.8.8' }); - const { host: host3 } = HttpConfig.schema.validate({ host: '::1' }); - const { host: host4 } = HttpConfig.schema.validate({ host: 'localhost' }); + const { host: host1 } = config.schema.validate({ host: 'www.example.com' }); + const { host: host2 } = config.schema.validate({ host: '8.8.8.8' }); + const { host: host3 } = config.schema.validate({ host: '::1' }); + const { host: host4 } = config.schema.validate({ host: 'localhost' }); expect({ host1, host2, host3, host4 }).toMatchSnapshot('valid host names'); }); test('throws if invalid hostname', () => { - const httpSchema = HttpConfig.schema; + const httpSchema = config.schema; const obj = { host: 'asdf$%^', }; @@ -43,16 +43,15 @@ test('throws if invalid hostname', () => { }); test('can specify max payload as string', () => { - const httpSchema = HttpConfig.schema; const obj = { maxPayload: '2mb', }; - const config = httpSchema.validate(obj); - expect(config.maxPayload.getValueInBytes()).toBe(2 * 1024 * 1024); + const configValue = config.schema.validate(obj); + expect(configValue.maxPayload.getValueInBytes()).toBe(2 * 1024 * 1024); }); test('throws if basepath is missing prepended slash', () => { - const httpSchema = HttpConfig.schema; + const httpSchema = config.schema; const obj = { basePath: 'foo', }; @@ -60,7 +59,7 @@ test('throws if basepath is missing prepended slash', () => { }); test('throws if basepath appends a slash', () => { - const httpSchema = HttpConfig.schema; + const httpSchema = config.schema; const obj = { basePath: '/foo/', }; @@ -68,7 +67,7 @@ test('throws if basepath appends a slash', () => { }); test('throws if basepath is not specified, but rewriteBasePath is set', () => { - const httpSchema = HttpConfig.schema; + const httpSchema = config.schema; const obj = { rewriteBasePath: true, }; @@ -77,7 +76,7 @@ test('throws if basepath is not specified, but rewriteBasePath is set', () => { describe('with TLS', () => { test('throws if TLS is enabled but `key` is not specified', () => { - const httpSchema = HttpConfig.schema; + const httpSchema = config.schema; const obj = { ssl: { certificate: '/path/to/certificate', @@ -88,7 +87,7 @@ describe('with TLS', () => { }); test('throws if TLS is enabled but `certificate` is not specified', () => { - const httpSchema = HttpConfig.schema; + const httpSchema = config.schema; const obj = { ssl: { enabled: true, @@ -99,7 +98,7 @@ describe('with TLS', () => { }); test('throws if TLS is enabled but `redirectHttpFromPort` is equal to `port`', () => { - const httpSchema = HttpConfig.schema; + const httpSchema = config.schema; const obj = { port: 1234, ssl: { @@ -113,7 +112,6 @@ describe('with TLS', () => { }); test('can specify single `certificateAuthority` as a string', () => { - const httpSchema = HttpConfig.schema; const obj = { ssl: { certificate: '/path/to/certificate', @@ -123,12 +121,11 @@ describe('with TLS', () => { }, }; - const config = httpSchema.validate(obj); - expect(config.ssl.certificateAuthorities).toBe('/authority/'); + const configValue = config.schema.validate(obj); + expect(configValue.ssl.certificateAuthorities).toBe('/authority/'); }); test('can specify several `certificateAuthorities`', () => { - const httpSchema = HttpConfig.schema; const obj = { ssl: { certificate: '/path/to/certificate', @@ -138,12 +135,12 @@ describe('with TLS', () => { }, }; - const config = httpSchema.validate(obj); - expect(config.ssl.certificateAuthorities).toEqual(['/authority/1', '/authority/2']); + const configValue = config.schema.validate(obj); + expect(configValue.ssl.certificateAuthorities).toEqual(['/authority/1', '/authority/2']); }); test('accepts known protocols`', () => { - const httpSchema = HttpConfig.schema; + const httpSchema = config.schema; const singleKnownProtocol = { ssl: { certificate: '/path/to/certificate', @@ -170,7 +167,7 @@ describe('with TLS', () => { }); test('should accept known protocols`', () => { - const httpSchema = HttpConfig.schema; + const httpSchema = config.schema; const singleUnknownProtocol = { ssl: { diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts index 1848070b2a56f..4d2279e90abed 100644 --- a/src/core/server/http/http_config.ts +++ b/src/core/server/http/http_config.ts @@ -19,83 +19,75 @@ import { ByteSizeValue, schema, TypeOf } from '@kbn/config-schema'; import { Env } from '../config'; -import { SslConfig } from './ssl_config'; +import { SslConfig, sslSchema } from './ssl_config'; const validBasePathRegex = /(^$|^\/.*[^\/]$)/; const match = (regex: RegExp, errorMsg: string) => (str: string) => regex.test(str) ? undefined : errorMsg; -const createHttpSchema = schema.object( - { - autoListen: schema.boolean({ defaultValue: true }), - basePath: schema.maybe( - schema.string({ - validate: match(validBasePathRegex, "must start with a slash, don't end with one"), - }) - ), - cors: schema.conditional( - schema.contextRef('dev'), - true, - schema.object( - { - origin: schema.arrayOf(schema.string()), - }, - { - defaultValue: { - origin: ['*://localhost:9876'], // karma test server +export const config = { + path: 'server', + schema: schema.object( + { + autoListen: schema.boolean({ defaultValue: true }), + basePath: schema.maybe( + schema.string({ + validate: match(validBasePathRegex, "must start with a slash, don't end with one"), + }) + ), + cors: schema.conditional( + schema.contextRef('dev'), + true, + schema.object( + { + origin: schema.arrayOf(schema.string()), }, - } + { + defaultValue: { + origin: ['*://localhost:9876'], // karma test server + }, + } + ), + schema.boolean({ defaultValue: false }) ), - schema.boolean({ defaultValue: false }) - ), - host: schema.string({ - defaultValue: 'localhost', - hostname: true, - }), - maxPayload: schema.byteSize({ - defaultValue: '1048576b', - }), - port: schema.number({ - defaultValue: 5601, - }), - rewriteBasePath: schema.boolean({ defaultValue: false }), - ssl: SslConfig.schema, - }, - { - validate: config => { - if (!config.basePath && config.rewriteBasePath) { - return 'cannot use [rewriteBasePath] when [basePath] is not specified'; - } - - if ( - config.ssl.enabled && - config.ssl.redirectHttpFromPort !== undefined && - config.ssl.redirectHttpFromPort === config.port - ) { - return ( - 'Kibana does not accept http traffic to [port] when ssl is ' + - 'enabled (only https is allowed), so [ssl.redirectHttpFromPort] ' + - `cannot be configured to the same value. Both are [${config.port}].` - ); - } + host: schema.string({ + defaultValue: 'localhost', + hostname: true, + }), + maxPayload: schema.byteSize({ + defaultValue: '1048576b', + }), + port: schema.number({ + defaultValue: 5601, + }), + rewriteBasePath: schema.boolean({ defaultValue: false }), + ssl: sslSchema, }, - } -); - -export type HttpConfigType = TypeOf; + { + validate: rawConfig => { + if (!rawConfig.basePath && rawConfig.rewriteBasePath) { + return 'cannot use [rewriteBasePath] when [basePath] is not specified'; + } -export const config = { - path: 'server', - schema: createHttpSchema, + if ( + rawConfig.ssl.enabled && + rawConfig.ssl.redirectHttpFromPort !== undefined && + rawConfig.ssl.redirectHttpFromPort === rawConfig.port + ) { + return ( + 'Kibana does not accept http traffic to [port] when ssl is ' + + 'enabled (only https is allowed), so [ssl.redirectHttpFromPort] ' + + `cannot be configured to the same value. Both are [${rawConfig.port}].` + ); + } + }, + } + ), }; +export type HttpConfigType = TypeOf; export class HttpConfig { - /** - * @internal - */ - public static schema = createHttpSchema; - public autoListen: boolean; public host: string; public port: number; @@ -118,6 +110,6 @@ export class HttpConfig { this.basePath = rawConfig.basePath; this.rewriteBasePath = rawConfig.rewriteBasePath; this.publicDir = env.staticFilesDir; - this.ssl = new SslConfig(rawConfig.ssl); + this.ssl = new SslConfig(rawConfig.ssl || {}); } } diff --git a/src/core/server/http/http_server.mocks.ts b/src/core/server/http/http_server.mocks.ts new file mode 100644 index 0000000000000..5a916c6890d28 --- /dev/null +++ b/src/core/server/http/http_server.mocks.ts @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Request, ResponseToolkit } from 'hapi'; +import { merge } from 'lodash'; + +type DeepPartial = T extends any[] + ? DeepPartialArray + : T extends object + ? DeepPartialObject + : T; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface DeepPartialArray extends Array> {} + +type DeepPartialObject = { [P in keyof T]+?: DeepPartial }; + +function createRawRequestMock(customization: DeepPartial = {}) { + return merge( + {}, + { + headers: {}, + path: '/', + route: { settings: {} }, + raw: { + req: { + url: '/', + }, + }, + }, + customization + ) as Request; +} + +function createRawResponseToolkitMock(customization: DeepPartial = {}) { + return merge({}, customization) as ResponseToolkit; +} + +export const httpServerMock = { + createRawRequest: createRawRequestMock, + createRawResponseToolkit: createRawResponseToolkitMock, +}; diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index 21e1193471972..9eb786ebb586a 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -18,6 +18,8 @@ */ import { Server } from 'http'; +import request from 'request'; +import Boom from 'boom'; jest.mock('fs', () => ({ readFileSync: jest.fn(), @@ -31,6 +33,7 @@ import { HttpConfig, Router } from '.'; import { loggingServiceMock } from '../logging/logging_service.mock'; import { HttpServer } from './http_server'; import { KibanaRequest } from './router'; +import { httpServerMock } from './http_server.mocks'; const chance = new Chance(); @@ -58,7 +61,7 @@ test('listening after started', async () => { expect(server.isListening()).toBe(false); await server.setup(config); - await server.start(config); + await server.start(); expect(server.isListening()).toBe(true); }); @@ -66,13 +69,13 @@ test('listening after started', async () => { test('200 OK with body', async () => { const router = new Router('/foo'); - router.get({ path: '/', validate: false }, async (req, res) => { + router.get({ path: '/', validate: false }, (req, res) => { return res.ok({ key: 'value' }); }); const { registerRouter, server: innerServer } = await server.setup(config); registerRouter(router); - await server.start(config); + await server.start(); await supertest(innerServer.listener) .get('/foo/') @@ -85,14 +88,14 @@ test('200 OK with body', async () => { test('202 Accepted with body', async () => { const router = new Router('/foo'); - router.get({ path: '/', validate: false }, async (req, res) => { + router.get({ path: '/', validate: false }, (req, res) => { return res.accepted({ location: 'somewhere' }); }); const { registerRouter, server: innerServer } = await server.setup(config); registerRouter(router); - await server.start(config); + await server.start(); await supertest(innerServer.listener) .get('/foo/') @@ -105,14 +108,14 @@ test('202 Accepted with body', async () => { test('204 No content', async () => { const router = new Router('/foo'); - router.get({ path: '/', validate: false }, async (req, res) => { + router.get({ path: '/', validate: false }, (req, res) => { return res.noContent(); }); const { registerRouter, server: innerServer } = await server.setup(config); registerRouter(router); - await server.start(config); + await server.start(); await supertest(innerServer.listener) .get('/foo/') @@ -126,7 +129,7 @@ test('204 No content', async () => { test('400 Bad request with error', async () => { const router = new Router('/foo'); - router.get({ path: '/', validate: false }, async (req, res) => { + router.get({ path: '/', validate: false }, (req, res) => { const err = new Error('some message'); return res.badRequest(err); }); @@ -134,7 +137,7 @@ test('400 Bad request with error', async () => { const { registerRouter, server: innerServer } = await server.setup(config); registerRouter(router); - await server.start(config); + await server.start(); await supertest(innerServer.listener) .get('/foo/') @@ -156,7 +159,7 @@ test('valid params', async () => { }), }), }, - async (req, res) => { + (req, res) => { return res.ok({ key: req.params.test }); } ); @@ -164,7 +167,7 @@ test('valid params', async () => { const { registerRouter, server: innerServer } = await server.setup(config); registerRouter(router); - await server.start(config); + await server.start(); await supertest(innerServer.listener) .get('/foo/some-string') @@ -186,7 +189,7 @@ test('invalid params', async () => { }), }), }, - async (req, res) => { + (req, res) => { return res.ok({ key: req.params.test }); } ); @@ -194,7 +197,7 @@ test('invalid params', async () => { const { registerRouter, server: innerServer } = await server.setup(config); registerRouter(router); - await server.start(config); + await server.start(); await supertest(innerServer.listener) .get('/foo/some-string') @@ -219,7 +222,7 @@ test('valid query', async () => { }), }), }, - async (req, res) => { + (req, res) => { return res.ok(req.query); } ); @@ -227,7 +230,7 @@ test('valid query', async () => { const { registerRouter, server: innerServer } = await server.setup(config); registerRouter(router); - await server.start(config); + await server.start(); await supertest(innerServer.listener) .get('/foo/?bar=test&quux=123') @@ -249,7 +252,7 @@ test('invalid query', async () => { }), }), }, - async (req, res) => { + (req, res) => { return res.ok(req.query); } ); @@ -257,7 +260,7 @@ test('invalid query', async () => { const { registerRouter, server: innerServer } = await server.setup(config); registerRouter(router); - await server.start(config); + await server.start(); await supertest(innerServer.listener) .get('/foo/?bar=test') @@ -282,7 +285,7 @@ test('valid body', async () => { }), }), }, - async (req, res) => { + (req, res) => { return res.ok(req.body); } ); @@ -290,7 +293,7 @@ test('valid body', async () => { const { registerRouter, server: innerServer } = await server.setup(config); registerRouter(router); - await server.start(config); + await server.start(); await supertest(innerServer.listener) .post('/foo/') @@ -316,7 +319,7 @@ test('invalid body', async () => { }), }), }, - async (req, res) => { + (req, res) => { return res.ok(req.body); } ); @@ -324,7 +327,7 @@ test('invalid body', async () => { const { registerRouter, server: innerServer } = await server.setup(config); registerRouter(router); - await server.start(config); + await server.start(); await supertest(innerServer.listener) .post('/foo/') @@ -349,7 +352,7 @@ test('handles putting', async () => { }), }), }, - async (req, res) => { + (req, res) => { return res.ok(req.body); } ); @@ -357,7 +360,7 @@ test('handles putting', async () => { const { registerRouter, server: innerServer } = await server.setup(config); registerRouter(router); - await server.start(config); + await server.start(); await supertest(innerServer.listener) .put('/foo/') @@ -380,7 +383,7 @@ test('handles deleting', async () => { }), }), }, - async (req, res) => { + (req, res) => { return res.ok({ key: req.params.id }); } ); @@ -388,7 +391,7 @@ test('handles deleting', async () => { const { registerRouter, server: innerServer } = await server.setup(config); registerRouter(router); - await server.start(config); + await server.start(); await supertest(innerServer.listener) .delete('/foo/3') @@ -405,7 +408,7 @@ test('filtered headers', async () => { let filteredHeaders: any; - router.get({ path: '/', validate: false }, async (req, res) => { + router.get({ path: '/', validate: false }, (req, res) => { filteredHeaders = req.getFilteredHeaders(['x-kibana-foo', 'host']); return res.noContent(); @@ -414,7 +417,7 @@ test('filtered headers', async () => { const { registerRouter, server: innerServer } = await server.setup(config); registerRouter(router); - await server.start(config); + await server.start(); await supertest(innerServer.listener) .get('/foo/?bar=quux') @@ -439,15 +442,13 @@ describe('with `basepath: /bar` and `rewriteBasePath: false`', () => { } as HttpConfig; const router = new Router('/'); - router.get({ path: '/', validate: false }, async (req, res) => res.ok({ key: 'value:/' })); - router.get({ path: '/foo', validate: false }, async (req, res) => - res.ok({ key: 'value:/foo' }) - ); + router.get({ path: '/', validate: false }, (req, res) => res.ok({ key: 'value:/' })); + router.get({ path: '/foo', validate: false }, (req, res) => res.ok({ key: 'value:/foo' })); - const { registerRouter, server: innerServer } = await server.setup(config); + const { registerRouter, server: innerServer } = await server.setup(configWithBasePath); registerRouter(router); - await server.start(configWithBasePath); + await server.start(); innerServerListener = innerServer.listener; }); @@ -500,15 +501,13 @@ describe('with `basepath: /bar` and `rewriteBasePath: true`', () => { } as HttpConfig; const router = new Router('/'); - router.get({ path: '/', validate: false }, async (req, res) => res.ok({ key: 'value:/' })); - router.get({ path: '/foo', validate: false }, async (req, res) => - res.ok({ key: 'value:/foo' }) - ); + router.get({ path: '/', validate: false }, (req, res) => res.ok({ key: 'value:/' })); + router.get({ path: '/foo', validate: false }, (req, res) => res.ok({ key: 'value:/foo' })); - const { registerRouter, server: innerServer } = await server.setup(config); + const { registerRouter, server: innerServer } = await server.setup(configWithBasePath); registerRouter(router); - await server.start(configWithBasePath); + await server.start(); innerServerListener = innerServer.listener; }); @@ -569,12 +568,12 @@ describe('with defined `redirectHttpFromPort`', () => { } as HttpConfig; const router = new Router('/'); - router.get({ path: '/', validate: false }, async (req, res) => res.ok({ key: 'value:/' })); + router.get({ path: '/', validate: false }, (req, res) => res.ok({ key: 'value:/' })); - const { registerRouter } = await server.setup(config); + const { registerRouter } = await server.setup(configWithSSL); registerRouter(router); - await server.start(configWithSSL); + await server.start(); }); }); @@ -590,27 +589,16 @@ test('returns server and connection options on start', async () => { expect(options).toMatchSnapshot(); }); -test('registers auth request interceptor only once', async () => { - const { registerAuth } = await server.setup(config); - const doRegister = () => - registerAuth(() => null as any, { - encryptionKey: 'any_password', - } as any); - - await doRegister(); - expect(doRegister()).rejects.toThrowError('Auth interceptor was already registered'); -}); - -test('registers onRequest interceptor several times', async () => { - const { registerOnRequest } = await server.setup(config); - const doRegister = () => registerOnRequest(() => null as any); +test('registers registerOnPostAuth interceptor several times', async () => { + const { registerOnPostAuth } = await server.setup(config); + const doRegister = () => registerOnPostAuth(() => null as any); doRegister(); expect(doRegister).not.toThrowError(); }); test('throws an error if starts without set up', async () => { - await expect(server.start(config)).rejects.toThrowErrorMatchingInlineSnapshot( + await expect(server.start()).rejects.toThrowErrorMatchingInlineSnapshot( `"Http server is not setup up yet"` ); }); @@ -621,11 +609,11 @@ test('#getBasePathFor() returns base path associated with an incoming request', setBasePathFor, registerRouter, server: innerServer, - registerOnRequest, + registerOnPostAuth, } = await server.setup(config); const path = '/base-path'; - registerOnRequest((req, t) => { + registerOnPostAuth((req, t) => { setBasePathFor(req, path); return t.next(); }); @@ -634,7 +622,7 @@ test('#getBasePathFor() returns base path associated with an incoming request', router.get({ path: '/', validate: false }, (req, res) => res.ok({ key: getBasePathFor(req) })); registerRouter(router); - await server.start(config); + await server.start(); await supertest(innerServer.listener) .get('/') .expect(200) @@ -653,22 +641,20 @@ test('#getBasePathFor() is based on server base path', async () => { setBasePathFor, registerRouter, server: innerServer, - registerOnRequest, + registerOnPostAuth, } = await server.setup(configWithBasePath); const path = '/base-path'; - registerOnRequest((req, t) => { + registerOnPostAuth((req, t) => { setBasePathFor(req, path); return t.next(); }); const router = new Router('/'); - router.get({ path: '/', validate: false }, async (req, res) => - res.ok({ key: getBasePathFor(req) }) - ); + router.get({ path: '/', validate: false }, (req, res) => res.ok({ key: getBasePathFor(req) })); registerRouter(router); - await server.start(configWithBasePath); + await server.start(); await supertest(innerServer.listener) .get('/') .expect(200) @@ -678,21 +664,9 @@ test('#getBasePathFor() is based on server base path', async () => { }); test('#setBasePathFor() cannot be set twice for one request', async () => { - const incomingMessage = { - url: '/', - }; const kibanaRequestFactory = { from() { - return KibanaRequest.from( - { - headers: {}, - path: '/', - raw: { - req: incomingMessage, - }, - } as any, - undefined - ); + return KibanaRequest.from(httpServerMock.createRawRequest()); }, }; jest.doMock('./router/request', () => ({ @@ -700,11 +674,364 @@ test('#setBasePathFor() cannot be set twice for one request', async () => { })); const { setBasePathFor } = await server.setup(config); - - const setPath = () => setBasePathFor(kibanaRequestFactory.from(), '/path'); + const req = kibanaRequestFactory.from(); + const setPath = () => setBasePathFor(req, '/path'); setPath(); expect(setPath).toThrowErrorMatchingInlineSnapshot( `"Request basePath was previously set. Setting multiple times is not supported."` ); }); +const cookieOptions = { + name: 'sid', + encryptionKey: 'something_at_least_32_characters', + validate: () => true, + isSecure: false, +}; + +interface User { + id: string; + roles?: string[]; +} + +interface StorageData { + value: User; + expires: number; +} + +describe('#registerAuth', () => { + it('registers auth request interceptor only once', async () => { + const { registerAuth } = await server.setup(config); + const doRegister = () => + registerAuth(() => null as any, { + encryptionKey: 'any_password', + } as any); + + await doRegister(); + expect(doRegister()).rejects.toThrowError('Auth interceptor was already registered'); + }); + + it('supports implementing custom authentication logic', async () => { + const router = new Router(''); + router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' })); + + const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); + const { sessionStorageFactory } = await registerAuth((req, t) => { + const user = { id: '42' }; + const sessionStorage = sessionStorageFactory.asScoped(req); + sessionStorage.set({ value: user, expires: Date.now() + 1000 }); + return t.authenticated(user); + }, cookieOptions); + registerRouter(router); + await server.start(); + + const response = await supertest(innerServer.listener) + .get('/') + .expect(200, { content: 'ok' }); + + expect(response.header['set-cookie']).toBeDefined(); + const cookies = response.header['set-cookie']; + expect(cookies).toHaveLength(1); + + const sessionCookie = request.cookie(cookies[0]); + if (!sessionCookie) { + throw new Error('session cookie expected to be defined'); + } + expect(sessionCookie).toBeDefined(); + expect(sessionCookie.key).toBe('sid'); + expect(sessionCookie.value).toBeDefined(); + expect(sessionCookie.path).toBe('/'); + expect(sessionCookie.httpOnly).toBe(true); + }); + + it('supports rejecting a request from an unauthenticated user', async () => { + const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); + const router = new Router(''); + router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' })); + registerRouter(router); + + await registerAuth((req, t) => t.rejected(Boom.unauthorized()), cookieOptions); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(401); + }); + + it('supports redirecting', async () => { + const redirectTo = '/redirect-url'; + const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); + const router = new Router(''); + router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' })); + registerRouter(router); + + await registerAuth((req, t) => { + return t.redirected(redirectTo); + }, cookieOptions); + await server.start(); + + const response = await supertest(innerServer.listener) + .get('/') + .expect(302); + expect(response.header.location).toBe(redirectTo); + }); + + it(`doesn't expose internal error details`, async () => { + const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); + const router = new Router(''); + router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' })); + registerRouter(router); + + await registerAuth((req, t) => { + throw new Error('sensitive info'); + }, cookieOptions); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect({ + statusCode: 500, + error: 'Internal Server Error', + message: 'An internal server error occurred', + }); + }); + + it(`allows manipulating cookies from route handler`, async () => { + const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); + const { sessionStorageFactory } = await registerAuth((req, t) => { + const user = { id: '42' }; + const sessionStorage = sessionStorageFactory.asScoped(req); + sessionStorage.set({ value: user, expires: Date.now() + 1000 }); + return t.authenticated(); + }, cookieOptions); + + const router = new Router(''); + router.get({ path: '/', validate: false }, (req, res) => res.ok({ content: 'ok' })); + router.get({ path: '/with-cookie', validate: false }, (req, res) => { + const sessionStorage = sessionStorageFactory.asScoped(req); + sessionStorage.clear(); + return res.ok({ content: 'ok' }); + }); + registerRouter(router); + + await server.start(); + + const responseToSetCookie = await supertest(innerServer.listener) + .get('/') + .expect(200); + + expect(responseToSetCookie.header['set-cookie']).toBeDefined(); + + const responseToResetCookie = await supertest(innerServer.listener) + .get('/with-cookie') + .expect(200); + + expect(responseToResetCookie.header['set-cookie']).toEqual([ + 'sid=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Path=/', + ]); + }); +}); + +test('enables auth for a route by default if registerAuth has been called', async () => { + const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); + + const router = new Router(''); + router.get({ path: '/', validate: false }, (req, res) => + res.ok({ authRequired: req.route.options.authRequired }) + ); + registerRouter(router); + + const authenticate = jest.fn().mockImplementation((req, t) => t.authenticated()); + await registerAuth(authenticate, cookieOptions); + + await server.start(); + await supertest(innerServer.listener) + .get('/') + .expect(200, { authRequired: true }); + + expect(authenticate).toHaveBeenCalledTimes(1); +}); + +test('supports disabling auth for a route explicitly', async () => { + const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); + + const router = new Router(''); + router.get({ path: '/', validate: false, options: { authRequired: false } }, (req, res) => + res.ok({ authRequired: req.route.options.authRequired }) + ); + registerRouter(router); + const authenticate = jest.fn(); + await registerAuth(authenticate, cookieOptions); + + await server.start(); + await supertest(innerServer.listener) + .get('/') + .expect(200, { authRequired: false }); + + expect(authenticate).toHaveBeenCalledTimes(0); +}); + +test('supports enabling auth for a route explicitly', async () => { + const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); + + const router = new Router(''); + router.get({ path: '/', validate: false, options: { authRequired: true } }, (req, res) => + res.ok({ authRequired: req.route.options.authRequired }) + ); + registerRouter(router); + const authenticate = jest.fn().mockImplementation((req, t) => t.authenticated({})); + await registerAuth(authenticate, cookieOptions); + + await server.start(); + await supertest(innerServer.listener) + .get('/') + .expect(200, { authRequired: true }); + + expect(authenticate).toHaveBeenCalledTimes(1); +}); + +test('allows attaching metadata to attach meta-data tag strings to a route', async () => { + const tags = ['my:tag']; + const { registerRouter, server: innerServer } = await server.setup(config); + + const router = new Router(''); + router.get({ path: '/with-tags', validate: false, options: { tags } }, (req, res) => + res.ok({ tags: req.route.options.tags }) + ); + router.get({ path: '/without-tags', validate: false }, (req, res) => + res.ok({ tags: req.route.options.tags }) + ); + registerRouter(router); + + await server.start(); + await supertest(innerServer.listener) + .get('/with-tags') + .expect(200, { tags }); + + await supertest(innerServer.listener) + .get('/without-tags') + .expect(200, { tags: [] }); +}); + +test('exposes route details of incoming request to a route handler', async () => { + const { registerRouter, server: innerServer } = await server.setup(config); + + const router = new Router(''); + router.get({ path: '/', validate: false }, (req, res) => res.ok(req.route)); + registerRouter(router); + + await server.start(); + await supertest(innerServer.listener) + .get('/') + .expect(200, { + method: 'get', + path: '/', + options: { + authRequired: true, + tags: [], + }, + }); +}); + +describe('#auth.isAuthenticated()', () => { + it('returns true if has been authorized', async () => { + const { registerAuth, registerRouter, server: innerServer, auth } = await server.setup(config); + + const router = new Router(''); + router.get({ path: '/', validate: false }, (req, res) => + res.ok({ isAuthenticated: auth.isAuthenticated(req) }) + ); + registerRouter(router); + + await registerAuth((req, t) => t.authenticated(), cookieOptions); + + await server.start(); + await supertest(innerServer.listener) + .get('/') + .expect(200, { isAuthenticated: true }); + }); + + it('returns false if has not been authorized', async () => { + const { registerAuth, registerRouter, server: innerServer, auth } = await server.setup(config); + + const router = new Router(''); + router.get({ path: '/', validate: false, options: { authRequired: false } }, (req, res) => + res.ok({ isAuthenticated: auth.isAuthenticated(req) }) + ); + registerRouter(router); + + await registerAuth((req, t) => t.authenticated(), cookieOptions); + + await server.start(); + await supertest(innerServer.listener) + .get('/') + .expect(200, { isAuthenticated: false }); + }); + + it('returns false if no authorization mechanism has been registered', async () => { + const { registerRouter, server: innerServer, auth } = await server.setup(config); + + const router = new Router(''); + router.get({ path: '/', validate: false, options: { authRequired: false } }, (req, res) => + res.ok({ isAuthenticated: auth.isAuthenticated(req) }) + ); + registerRouter(router); + + await server.start(); + await supertest(innerServer.listener) + .get('/') + .expect(200, { isAuthenticated: false }); + }); +}); + +describe('#auth.get()', () => { + it('returns authenticated status and allow associate auth state with request', async () => { + const user = { id: '42' }; + const { registerRouter, registerAuth, server: innerServer, auth } = await server.setup(config); + const { sessionStorageFactory } = await registerAuth((req, t) => { + sessionStorageFactory.asScoped(req).set({ value: user, expires: Date.now() + 1000 }); + return t.authenticated(user); + }, cookieOptions); + + const router = new Router(''); + router.get({ path: '/', validate: false }, (req, res) => res.ok(auth.get(req))); + registerRouter(router); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, { state: user, status: 'authenticated' }); + }); + + it('returns correct authentication unknown status', async () => { + const { registerRouter, server: innerServer, auth } = await server.setup(config); + const router = new Router(''); + router.get({ path: '/', validate: false }, (req, res) => res.ok(auth.get(req))); + + registerRouter(router); + await server.start(); + await supertest(innerServer.listener) + .get('/') + .expect(200, { status: 'unknown' }); + }); + + it('returns correct unauthenticated status', async () => { + const authenticate = jest.fn(); + + const { registerRouter, registerAuth, server: innerServer, auth } = await server.setup(config); + await registerAuth(authenticate, cookieOptions); + const router = new Router(''); + router.get({ path: '/', validate: false, options: { authRequired: false } }, (req, res) => + res.ok(auth.get(req)) + ); + + registerRouter(router); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, { status: 'unauthenticated' }); + + expect(authenticate).not.toHaveBeenCalled(); + }); +}); diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 6dbae8a14d601..ab2d227193dce 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -24,45 +24,69 @@ import { Logger } from '../logging'; import { HttpConfig } from './http_config'; import { createServer, getServerOptions } from './http_tools'; import { adoptToHapiAuthFormat, AuthenticationHandler } from './lifecycle/auth'; -import { adoptToHapiOnRequestFormat, OnRequestHandler } from './lifecycle/on_request'; -import { Router, KibanaRequest } from './router'; +import { adoptToHapiOnPostAuthFormat, OnPostAuthHandler } from './lifecycle/on_post_auth'; +import { adoptToHapiOnPreAuthFormat, OnPreAuthHandler } from './lifecycle/on_pre_auth'; +import { Router, KibanaRequest, toRawRequest } from './router'; import { SessionStorageCookieOptions, createCookieSessionStorageFactory, } from './cookie_session_storage'; +import { SessionStorageFactory } from './session_storage'; +import { AuthStateStorage } from './auth_state_storage'; + +const getIncomingMessage = (request: KibanaRequest | Request) => + request instanceof KibanaRequest ? toRawRequest(request).raw.req : request.raw.req; export interface HttpServerSetup { server: Server; options: ServerOptions; registerRouter: (router: Router) => void; /** - * Define custom authentication and/or authorization mechanism for incoming requests. - * Applied to all resources by default. Only one AuthenticationHandler can be registered. + * To define custom authentication and/or authorization mechanism for incoming requests. + * A handler should return a state to associate with the incoming request. + * The state can be retrieved later via http.auth.get(..) + * Only one AuthenticationHandler can be registered. */ registerAuth: ( - authenticationHandler: AuthenticationHandler, + handler: AuthenticationHandler, cookieOptions: SessionStorageCookieOptions - ) => void; + ) => Promise<{ sessionStorageFactory: SessionStorageFactory }>; + /** + * To define custom logic to perform for incoming requests. Runs the handler before Auth + * hook performs a check that user has access to requested resources, so it's the only + * place when you can forward a request to another URL right on the server. + * Can register any number of registerOnPostAuth, which are called in sequence + * (from the first registered to the last). + */ + registerOnPreAuth: (handler: OnPreAuthHandler) => void; /** - * Define custom logic to perform for incoming requests. - * Applied to all resources by default. - * Can register any number of OnRequestHandlers, which are called in sequence (from the first registered to the last) + * To define custom logic to perform for incoming requests. Runs the handler after Auth hook + * did make sure a user has access to the requested resource. + * The auth state is available at stage via http.auth.get(..) + * Can register any number of registerOnPreAuth, which are called in sequence + * (from the first registered to the last). */ - registerOnRequest: (requestHandler: OnRequestHandler) => void; + registerOnPostAuth: (handler: OnPostAuthHandler) => void; getBasePathFor: (request: KibanaRequest | Request) => string; setBasePathFor: (request: KibanaRequest | Request, basePath: string) => void; + auth: { + get: AuthStateStorage['get']; + isAuthenticated: AuthStateStorage['isAuthenticated']; + }; } export class HttpServer { private server?: Server; + private config?: HttpConfig; private registeredRouters = new Set(); private authRegistered = false; - private basePathCache = new WeakMap< - ReturnType, - string - >(); + private basePathCache = new WeakMap, string>(); - constructor(private readonly log: Logger) {} + private readonly authState: AuthStateStorage; + + constructor(private readonly log: Logger) { + this.authState = new AuthStateStorage(() => this.authRegistered); + } public isListening() { return this.server !== undefined && this.server.listener.listening; @@ -79,8 +103,7 @@ export class HttpServer { // passing hapi Request works for BWC. can be deleted once we remove legacy server. private getBasePathFor(config: HttpConfig, request: KibanaRequest | Request) { - const incomingMessage = - request instanceof KibanaRequest ? request.unstable_getIncomingMessage() : request.raw.req; + const incomingMessage = getIncomingMessage(request); const requestScopePath = this.basePathCache.get(incomingMessage) || ''; const serverBasePath = config.basePath || ''; @@ -89,8 +112,8 @@ export class HttpServer { // should work only for KibanaRequest as soon as spaces migrate to NP private setBasePathFor(request: KibanaRequest | Request, basePath: string) { - const incomingMessage = - request instanceof KibanaRequest ? request.unstable_getIncomingMessage() : request.raw.req; + const incomingMessage = getIncomingMessage(request); + if (this.basePathCache.has(incomingMessage)) { throw new Error( 'Request basePath was previously set. Setting multiple times is not supported.' @@ -102,17 +125,23 @@ export class HttpServer { public setup(config: HttpConfig): HttpServerSetup { const serverOptions = getServerOptions(config); this.server = createServer(serverOptions); + this.config = config; + + this.setupBasePathRewrite(config); return { options: serverOptions, registerRouter: this.registerRouter.bind(this), - registerOnRequest: this.registerOnRequest.bind(this), - registerAuth: ( - fn: AuthenticationHandler, - cookieOptions: SessionStorageCookieOptions - ) => this.registerAuth(fn, cookieOptions, config.basePath), + registerOnPreAuth: this.registerOnPreAuth.bind(this), + registerOnPostAuth: this.registerOnPostAuth.bind(this), + registerAuth: (fn: AuthenticationHandler, cookieOptions: SessionStorageCookieOptions) => + this.registerAuth(fn, cookieOptions, config.basePath), getBasePathFor: this.getBasePathFor.bind(this, config), setBasePathFor: this.setBasePathFor.bind(this), + auth: { + get: this.authState.get, + isAuthenticated: this.authState.isAuthenticated, + }, // Return server instance with the connection options so that we can properly // bridge core and the "legacy" Kibana internally. Once this bridge isn't // needed anymore we shouldn't return the instance from this method. @@ -120,31 +149,30 @@ export class HttpServer { }; } - public async start(config: HttpConfig) { + public async start() { if (this.server === undefined) { throw new Error('Http server is not setup up yet'); } this.log.debug('starting http server'); - this.setupBasePathRewrite(this.server, config); - for (const router of this.registeredRouters) { for (const route of router.getRoutes()) { + const { authRequired = true, tags } = route.options; this.server.route({ handler: route.handler, method: route.method, path: this.getRouteFullPath(router.path, route.path), + options: { + auth: authRequired ? undefined : false, + tags: tags ? Array.from(tags) : undefined, + }, }); } } await this.server.start(); - - this.log.debug( - `http server running at ${this.server.info.uri}${ - config.rewriteBasePath ? config.basePath : '' - }` - ); + const serverPath = this.config!.rewriteBasePath || this.config!.basePath || ''; + this.log.debug(`http server running at ${this.server.info.uri}${serverPath}`); } public async stop() { @@ -157,13 +185,13 @@ export class HttpServer { this.server = undefined; } - private setupBasePathRewrite(server: Server, config: HttpConfig) { + private setupBasePathRewrite(config: HttpConfig) { if (config.basePath === undefined || !config.rewriteBasePath) { return; } const basePath = config.basePath; - server.ext('onRequest', (request, responseToolkit) => { + this.registerOnPreAuth((request, toolkit) => { const newURL = modifyUrl(request.url.href!, urlParts => { if (urlParts.pathname != null && urlParts.pathname.startsWith(basePath)) { urlParts.pathname = urlParts.pathname.replace(basePath, '') || '/'; @@ -173,18 +201,10 @@ export class HttpServer { }); if (!newURL) { - return responseToolkit - .response('Not Found') - .code(404) - .takeover(); + return toolkit.rejected(new Error('not found'), { statusCode: 404 }); } - request.setUrl(newURL); - // We should update raw request as well since it can be proxied to the old platform - // where base path isn't expected. - request.raw.req.url = request.url.href; - - return responseToolkit.continue; + return toolkit.redirected(newURL, { forward: true }); }); } @@ -195,16 +215,24 @@ export class HttpServer { return `${routerPath}${routePath.slice(routePathStartIndex)}`; } - private registerOnRequest(fn: OnRequestHandler) { + private registerOnPostAuth(fn: OnPostAuthHandler) { if (this.server === undefined) { throw new Error('Server is not created yet'); } - this.server.ext('onRequest', adoptToHapiOnRequestFormat(fn)); + this.server.ext('onPostAuth', adoptToHapiOnPostAuthFormat(fn)); + } + + private registerOnPreAuth(fn: OnPreAuthHandler) { + if (this.server === undefined) { + throw new Error('Server is not created yet'); + } + + this.server.ext('onRequest', adoptToHapiOnPreAuthFormat(fn)); } private async registerAuth( - fn: AuthenticationHandler, + fn: AuthenticationHandler, cookieOptions: SessionStorageCookieOptions, basePath?: string ) { @@ -216,14 +244,14 @@ export class HttpServer { } this.authRegistered = true; - const sessionStorage = await createCookieSessionStorageFactory( + const sessionStorageFactory = await createCookieSessionStorageFactory( this.server, cookieOptions, basePath ); this.server.auth.scheme('login', () => ({ - authenticate: adoptToHapiAuthFormat(fn, sessionStorage), + authenticate: adoptToHapiAuthFormat(fn, this.authState.set), })); this.server.auth.strategy('session', 'login'); @@ -232,5 +260,7 @@ export class HttpServer { // should be applied for all routes if they don't specify auth strategy in route declaration // https://github.com/hapijs/hapi/blob/master/API.md#-serverauthdefaultoptions this.server.auth.default('session'); + + return { sessionStorageFactory }; } } diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index 289eae0990531..7dccacee8509a 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -19,17 +19,26 @@ import { Server, ServerOptions } from 'hapi'; import { HttpService } from './http_service'; +import { HttpConfig } from './http_config'; +import { HttpServerSetup } from './http_server'; const createSetupContractMock = () => { const setupContract = { options: {} as ServerOptions, + registerOnPreAuth: jest.fn(), registerAuth: jest.fn(), - registerOnRequest: jest.fn(), + registerOnPostAuth: jest.fn(), registerRouter: jest.fn(), getBasePathFor: jest.fn(), setBasePathFor: jest.fn(), // we can mock some hapi server method when we need it server: {} as Server, + auth: { + get: jest.fn(), + isAuthenticated: jest.fn(), + }, + createNewServer: async (cfg: Partial): Promise => + ({} as HttpServerSetup), }; return setupContract; }; diff --git a/src/core/server/http/http_service.test.mocks.ts b/src/core/server/http/http_service.test.mocks.ts index a0d7ff5069eb0..c147944f2b7d8 100644 --- a/src/core/server/http/http_service.test.mocks.ts +++ b/src/core/server/http/http_service.test.mocks.ts @@ -19,6 +19,11 @@ export const mockHttpServer = jest.fn(); -jest.mock('./http_server', () => ({ - HttpServer: mockHttpServer, -})); +jest.mock('./http_server', () => { + const realHttpServer = jest.requireActual('./http_server'); + + return { + ...realHttpServer, + HttpServer: mockHttpServer, + }; +}); diff --git a/src/core/server/http/http_service.test.ts b/src/core/server/http/http_service.test.ts index 7b3fd024b477c..16f946ffcc7ae 100644 --- a/src/core/server/http/http_service.test.ts +++ b/src/core/server/http/http_service.test.ts @@ -76,6 +76,46 @@ test('creates and sets up http server', async () => { expect(httpServer.start).toHaveBeenCalledTimes(1); }); +// this is an integration test! +test('creates and sets up second http server', async () => { + const configService = createConfigService({ + host: 'localhost', + port: 1234, + }); + const { HttpServer } = jest.requireActual('./http_server'); + + mockHttpServer.mockImplementation((...args) => new HttpServer(...args)); + + const service = new HttpService({ configService, env, logger }); + const serverSetup = await service.setup(); + const cfg = { port: 2345 }; + await serverSetup.createNewServer(cfg); + const server = await service.start(); + expect(server.isListening()).toBeTruthy(); + expect(server.isListening(cfg.port)).toBeTruthy(); + + try { + await serverSetup.createNewServer(cfg); + } catch (err) { + expect(err.message).toBe('port 2345 is already in use'); + } + + try { + await serverSetup.createNewServer({ port: 1234 }); + } catch (err) { + expect(err.message).toBe('port 1234 is already in use'); + } + + try { + await serverSetup.createNewServer({ host: 'example.org' }); + } catch (err) { + expect(err.message).toBe('port must be defined'); + } + await service.stop(); + expect(server.isListening()).toBeFalsy(); + expect(server.isListening(cfg.port)).toBeFalsy(); +}); + test('logs error if already set up', async () => { const configService = createConfigService(); @@ -153,8 +193,9 @@ test('returns http server contract on setup', async () => { })); const service = new HttpService({ configService, env, logger }); - - expect(await service.setup()).toBe(httpServer); + const { createNewServer, ...setupHttpServer } = await service.setup(); + expect(createNewServer).toBeDefined(); + expect(setupHttpServer).toEqual(httpServer); }); test('does not start http server if process is dev cluster master', async () => { diff --git a/src/core/server/http/http_service.ts b/src/core/server/http/http_service.ts index ea172b881f546..fec3774e2f366 100644 --- a/src/core/server/http/http_service.ts +++ b/src/core/server/http/http_service.ts @@ -18,17 +18,20 @@ */ import { Observable, Subscription } from 'rxjs'; -import { first } from 'rxjs/operators'; +import { first, map } from 'rxjs/operators'; +import { LoggerFactory } from '../logging'; import { CoreService } from '../../types'; import { Logger } from '../logging'; import { CoreContext } from '../core_context'; -import { HttpConfig } from './http_config'; +import { HttpConfig, HttpConfigType, config as httpConfig } from './http_config'; import { HttpServer, HttpServerSetup } from './http_server'; import { HttpsRedirectServer } from './https_redirect_server'; /** @public */ -export type HttpServiceSetup = HttpServerSetup; +export interface HttpServiceSetup extends HttpServerSetup { + createNewServer: (cfg: Partial) => Promise; +} /** @public */ export interface HttpServiceStart { /** Indicates if http server is listening on a port */ @@ -38,15 +41,20 @@ export interface HttpServiceStart { /** @internal */ export class HttpService implements CoreService { private readonly httpServer: HttpServer; + private readonly secondaryServers: Map = new Map(); private readonly httpsRedirectServer: HttpsRedirectServer; private readonly config$: Observable; private configSubscription?: Subscription; + private readonly logger: LoggerFactory; private readonly log: Logger; constructor(private readonly coreContext: CoreContext) { + this.logger = coreContext.logger; this.log = coreContext.logger.get('http'); - this.config$ = this.coreContext.configService.atPath('server', HttpConfig); + this.config$ = coreContext.configService + .atPath('server') + .pipe(map(rawConfig => new HttpConfig(rawConfig, coreContext.env))); this.httpServer = new HttpServer(coreContext.logger.get('http', 'server')); this.httpsRedirectServer = new HttpsRedirectServer( @@ -67,7 +75,12 @@ export class HttpService implements CoreService server.start())); } return { - isListening: () => this.httpServer.isListening(), + isListening: (port = 0) => { + const server = this.secondaryServers.get(port); + if (server) return server.isListening(); + return this.httpServer.isListening(); + }, }; } + private async createServer(cfg: Partial) { + const { port } = cfg; + const config = await this.config$.pipe(first()).toPromise(); + + if (!port) { + throw new Error('port must be defined'); + } + + // verify that main server and none of the secondary servers are already using this port + if (this.secondaryServers.has(port) || config.port === port) { + throw new Error(`port ${port} is already in use`); + } + + for (const [key, val] of Object.entries(cfg)) { + httpConfig.schema.validateKey(key, val); + } + + const baseConfig = await this.config$.pipe(first()).toPromise(); + const finalConfig = { ...baseConfig, ...cfg }; + const log = this.logger.get('http', `server:${port}`); + + const httpServer = new HttpServer(log); + const httpSetup = await httpServer.setup(finalConfig); + this.secondaryServers.set(port, httpServer); + return httpSetup; + } + public async stop() { if (this.configSubscription === undefined) { return; @@ -102,5 +147,7 @@ export class HttpService implements CoreService s.stop())); + this.secondaryServers.clear(); } } diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index 465c5cb6a859b..056ee53cee89b 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -19,7 +19,15 @@ export { config, HttpConfig, HttpConfigType } from './http_config'; export { HttpService, HttpServiceSetup, HttpServiceStart } from './http_service'; -export { Router, KibanaRequest } from './router'; +export { + KibanaRequest, + KibanaRequestRoute, + Router, + RouteMethod, + RouteConfigOptions, +} from './router'; export { BasePathProxyServer } from './base_path_proxy_server'; +export { OnPreAuthHandler, OnPreAuthToolkit } from './lifecycle/on_pre_auth'; export { AuthenticationHandler, AuthToolkit } from './lifecycle/auth'; -export { OnRequestHandler, OnRequestToolkit } from './lifecycle/on_request'; +export { OnPostAuthHandler, OnPostAuthToolkit } from './lifecycle/on_post_auth'; +export { SessionStorageFactory, SessionStorage } from './session_storage'; diff --git a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/kibana.json b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/kibana.json deleted file mode 100644 index 0499e47abf9c3..0000000000000 --- a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/kibana.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "id": "dummy-on-request", - "version": "0.0.1", - "kibanaVersion": "kibana", - "ui": false, - "server": true -} diff --git a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/server/index.ts b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/server/index.ts deleted file mode 100644 index 9730472c8f84c..0000000000000 --- a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/server/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { DummyOnRequestPlugin } from './plugin'; -export const plugin = () => new DummyOnRequestPlugin(); diff --git a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/server/plugin.ts b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/server/plugin.ts deleted file mode 100644 index 0d9a7fd71a5eb..0000000000000 --- a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/server/plugin.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { CoreSetup } from '../../../../../..'; - -export const url = { - exception: '/exception', - failed: '/failed', - independentReq: '/independent-request', - root: '/', - redirect: '/redirect', - redirectTo: '/redirect-to', -}; - -export class DummyOnRequestPlugin { - public setup(core: CoreSetup) { - core.http.registerOnRequest(async (request, t) => { - await Promise.resolve(); - if (request.path === url.redirect) { - return t.redirected(url.redirectTo); - } - return t.next(); - }); - - core.http.registerOnRequest((request, t) => { - if (request.path === url.failed) { - return t.rejected(new Error('unexpected error'), { statusCode: 400 }); - } - return t.next(); - }); - - core.http.registerOnRequest((request, t) => { - if (request.path === url.exception) { - throw new Error('sensitive info'); - } - return t.next(); - }); - - core.http.registerOnRequest((request, t) => { - if (request.path === url.independentReq) { - // @ts-ignore. don't complain customField is not defined on Request type - request.customField = { value: 42 }; - } - return t.next(); - }); - - core.http.registerOnRequest((request, t) => { - if ( - request.path === url.independentReq && - // @ts-ignore don't complain customField is not defined on Request type - typeof request.customField !== 'undefined' - ) { - throw new Error('Request object was mutated'); - } - return t.next(); - }); - } - public start() {} -} diff --git a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/kibana.json b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/kibana.json deleted file mode 100644 index b6e84959322a9..0000000000000 --- a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/kibana.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "id": "dummy-security", - "version": "0.0.1", - "kibanaVersion": "kibana", - "ui": false, - "server": true -} diff --git a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/index.ts b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/index.ts deleted file mode 100644 index dd78ab308a8bc..0000000000000 --- a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { DummySecurityPlugin } from './plugin'; -export const plugin = () => new DummySecurityPlugin(); diff --git a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts deleted file mode 100644 index 1b4cf19b88eb8..0000000000000 --- a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import Boom from 'boom'; -import { AuthenticationHandler, CoreSetup } from '../../../../../../../../core/server'; - -interface User { - id: string; - roles?: string[]; -} - -interface Storage { - value: User; - expires: number; -} - -export const url = { - auth: '/auth', - authRedirect: '/auth/redirect', - exception: '/exception', - redirectTo: '/login', -}; - -export const sessionDurationMs = 1000; -export class DummySecurityPlugin { - public setup(core: CoreSetup) { - const authenticate: AuthenticationHandler = async (request, sessionStorage, t) => { - if (request.path === url.authRedirect) { - return t.redirected(url.redirectTo); - } - - if (request.path === url.exception) { - throw new Error('sensitive info'); - } - - if (request.headers.authorization) { - const user = { id: '42' }; - sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs }); - return t.authenticated({ credentials: user }); - } else { - return t.rejected(Boom.unauthorized()); - } - }; - - const cookieOptions = { - name: 'sid', - encryptionKey: 'something_at_least_32_characters', - validate: (session: Storage) => true, - isSecure: false, - path: '/', - }; - core.http.registerAuth(authenticate, cookieOptions); - return { - dummy() { - return 'Hello from dummy plugin'; - }, - }; - } - public start() {} -} diff --git a/src/core/server/http/integration_tests/http_service.test.ts b/src/core/server/http/integration_tests/http_service.test.ts index c635429159fe3..100efc4e2a607 100644 --- a/src/core/server/http/integration_tests/http_service.test.ts +++ b/src/core/server/http/integration_tests/http_service.test.ts @@ -16,76 +16,54 @@ * specific language governing permissions and limitations * under the License. */ +import Boom from 'boom'; -import path from 'path'; -import { parse } from 'url'; - -import request from 'request'; -import * as kbnTestServer from '../../../../test_utils/kbn_server'; import { Router } from '../router'; -import { url as authUrl } from './__fixtures__/plugins/dummy_security/server/plugin'; -import { url as onReqUrl } from './__fixtures__/plugins/dummy_on_request/server/plugin'; +import * as kbnTestServer from '../../../../test_utils/kbn_server'; + +interface User { + id: string; + roles?: string[]; +} + +interface StorageData { + value: User; + expires: number; +} describe('http service', () => { describe('setup contract', () => { describe('#registerAuth()', () => { - const plugin = path.resolve(__dirname, './__fixtures__/plugins/dummy_security'); + const sessionDurationMs = 1000; + const cookieOptions = { + name: 'sid', + encryptionKey: 'something_at_least_32_characters', + validate: (session: StorageData) => true, + isSecure: false, + path: '/', + }; + let root: ReturnType; - beforeAll(async () => { - root = kbnTestServer.createRoot( - { - plugins: { paths: [plugin] }, - }, - { - dev: true, - } - ); + beforeEach(async () => { + root = kbnTestServer.createRoot(); + }, 30000); - const router = new Router(''); - router.get({ path: authUrl.auth, validate: false }, async (req, res) => - res.ok({ content: 'ok' }) - ); + afterEach(async () => await root.shutdown()); + it('Should run auth for legacy routes and proxy request to legacy server route handlers', async () => { const { http } = await root.setup(); - http.registerRouter(router); + const { sessionStorageFactory } = await http.registerAuth((req, t) => { + if (req.headers.authorization) { + const user = { id: '42' }; + const sessionStorage = sessionStorageFactory.asScoped(req); + sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs }); + return t.authenticated(user); + } else { + return t.rejected(Boom.unauthorized()); + } + }, cookieOptions); await root.start(); - }, 30000); - - afterAll(async () => await root.shutdown()); - - it('Should support implementing custom authentication logic', async () => { - const response = await kbnTestServer.request - .get(root, authUrl.auth) - .expect(200, { content: 'ok' }); - - expect(response.header['set-cookie']).toBeDefined(); - const cookies = response.header['set-cookie']; - expect(cookies).toHaveLength(1); - - const sessionCookie = request.cookie(cookies[0]); - if (!sessionCookie) { - throw new Error('session cookie expected to be defined'); - } - expect(sessionCookie).toBeDefined(); - expect(sessionCookie.key).toBe('sid'); - expect(sessionCookie.value).toBeDefined(); - expect(sessionCookie.path).toBe('/'); - expect(sessionCookie.httpOnly).toBe(true); - }); - it('Should support rejecting a request from an unauthenticated user', async () => { - await kbnTestServer.request - .get(root, authUrl.auth) - .unset('Authorization') - .expect(401); - }); - - it('Should support redirecting', async () => { - const response = await kbnTestServer.request.get(root, authUrl.authRedirect).expect(302); - expect(response.header.location).toBe(authUrl.redirectTo); - }); - - it('Should run auth for legacy routes and proxy request to legacy server route handlers', async () => { const legacyUrl = '/legacy'; const kbnServer = kbnTestServer.getKbnServer(root); kbnServer.server.route({ @@ -101,66 +79,123 @@ describe('http service', () => { expect(response.header['set-cookie']).toBe(undefined); }); - it(`Shouldn't expose internal error details`, async () => { - await kbnTestServer.request.get(root, authUrl.exception).expect({ - statusCode: 500, - error: 'Internal Server Error', - message: 'An internal server error occurred', + it('Should pass associated auth state to Legacy platform', async () => { + const user = { id: '42' }; + + const { http } = await root.setup(); + const { sessionStorageFactory } = await http.registerAuth((req, t) => { + if (req.headers.authorization) { + const sessionStorage = sessionStorageFactory.asScoped(req); + sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs }); + return t.authenticated(user); + } else { + return t.rejected(Boom.unauthorized()); + } + }, cookieOptions); + await root.start(); + + const legacyUrl = '/legacy'; + const kbnServer = kbnTestServer.getKbnServer(root); + kbnServer.server.route({ + method: 'GET', + path: legacyUrl, + handler: kbnServer.newPlatform.setup.core.http.auth.get, }); + + const response = await kbnTestServer.request.get(root, legacyUrl).expect(200); + expect(response.body.state).toEqual(user); + expect(response.body.status).toEqual('authenticated'); + + expect(response.header['set-cookie']).toBe(undefined); }); }); - describe('#registerOnRequest()', () => { - const plugin = path.resolve(__dirname, './__fixtures__/plugins/dummy_on_request'); + describe('#registerOnPostAuth()', () => { let root: ReturnType; beforeEach(async () => { - root = kbnTestServer.createRoot( - { - plugins: { paths: [plugin] }, - }, - { - dev: true, - } - ); + root = kbnTestServer.createRoot(); + }, 30000); + afterEach(async () => await root.shutdown()); + it('Should support passing request through to the route handler', async () => { const router = new Router(''); - // routes with expected success status response should have handlers - [onReqUrl.root, onReqUrl.independentReq].forEach(url => - router.get({ path: url, validate: false }, async (req, res) => res.ok({ content: 'ok' })) - ); + router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' })); const { http } = await root.setup(); + http.registerOnPostAuth((req, t) => t.next()); + http.registerOnPostAuth(async (req, t) => { + await Promise.resolve(); + return t.next(); + }); http.registerRouter(router); - await root.start(); - }, 30000); - afterEach(async () => await root.shutdown()); - it('Should support passing request through to the route handler', async () => { - await kbnTestServer.request.get(root, onReqUrl.root).expect(200, { content: 'ok' }); + await kbnTestServer.request.get(root, '/').expect(200, { content: 'ok' }); }); + it('Should support redirecting to configured url', async () => { - const response = await kbnTestServer.request.get(root, onReqUrl.redirect).expect(302); - expect(response.header.location).toBe(onReqUrl.redirectTo); + const redirectTo = '/redirect-url'; + const { http } = await root.setup(); + http.registerOnPostAuth(async (req, t) => t.redirected(redirectTo)); + await root.start(); + + const response = await kbnTestServer.request.get(root, '/').expect(302); + expect(response.header.location).toBe(redirectTo); }); + it('Should failing a request with configured error and status code', async () => { + const { http } = await root.setup(); + http.registerOnPostAuth(async (req, t) => + t.rejected(new Error('unexpected error'), { statusCode: 400 }) + ); + await root.start(); + await kbnTestServer.request - .get(root, onReqUrl.failed) + .get(root, '/') .expect(400, { statusCode: 400, error: 'Bad Request', message: 'unexpected error' }); }); + it(`Shouldn't expose internal error details`, async () => { - await kbnTestServer.request.get(root, onReqUrl.exception).expect({ + const { http } = await root.setup(); + http.registerOnPostAuth(async (req, t) => { + throw new Error('sensitive info'); + }); + await root.start(); + + await kbnTestServer.request.get(root, '/').expect({ statusCode: 500, error: 'Internal Server Error', message: 'An internal server error occurred', }); }); + it(`Shouldn't share request object between interceptors`, async () => { - await kbnTestServer.request.get(root, onReqUrl.independentReq).expect(200); + const { http } = await root.setup(); + http.registerOnPostAuth(async (req, t) => { + // @ts-ignore. don't complain customField is not defined on Request type + req.customField = { value: 42 }; + return t.next(); + }); + http.registerOnPostAuth((req, t) => { + // @ts-ignore don't complain customField is not defined on Request type + if (typeof req.customField !== 'undefined') { + throw new Error('Request object was mutated'); + } + return t.next(); + }); + const router = new Router(''); + router.get({ path: '/', validate: false }, async (req, res) => + // @ts-ignore. don't complain customField is not defined on Request type + res.ok({ customField: String(req.customField) }) + ); + http.registerRouter(router); + await root.start(); + + await kbnTestServer.request.get(root, '/').expect(200, { customField: 'undefined' }); }); }); - describe('#registerOnRequest() toolkit', () => { + describe('#registerOnPostAuth() toolkit', () => { let root: ReturnType; beforeEach(async () => { root = kbnTestServer.createRoot(); @@ -169,9 +204,8 @@ describe('http service', () => { afterEach(async () => await root.shutdown()); it('supports Url change on the flight', async () => { const { http } = await root.setup(); - http.registerOnRequest((req, t) => { - t.setUrl(parse('/new-url')); - return t.next(); + http.registerOnPreAuth((req, t) => { + return t.redirected('/new-url', { forward: true }); }); const router = new Router('/'); @@ -188,9 +222,8 @@ describe('http service', () => { it('url re-write works for legacy server as well', async () => { const { http } = await root.setup(); const newUrl = '/new-url'; - http.registerOnRequest((req, t) => { - t.setUrl(newUrl); - return t.next(); + http.registerOnPreAuth((req, t) => { + return t.redirected(newUrl, { forward: true }); }); await root.start(); @@ -215,7 +248,7 @@ describe('http service', () => { it('basePath information for an incoming request is available in legacy server', async () => { const reqBasePath = '/requests-specific-base-path'; const { http } = await root.setup(); - http.registerOnRequest((req, t) => { + http.registerOnPreAuth((req, t) => { http.setBasePathFor(req, reqBasePath); return t.next(); }); diff --git a/src/core/server/http/lifecycle/auth.test.ts b/src/core/server/http/lifecycle/auth.test.ts index b8c0c7c5d1d50..031556c70483c 100644 --- a/src/core/server/http/lifecycle/auth.test.ts +++ b/src/core/server/http/lifecycle/auth.test.ts @@ -19,24 +19,16 @@ import Boom from 'boom'; import { adoptToHapiAuthFormat } from './auth'; - -const SessionStorageMock = { - asScoped: () => null as any, -}; -const requestMock = {} as any; -const createResponseToolkit = (customization = {}): any => ({ ...customization }); +import { httpServerMock } from '../http_server.mocks'; describe('adoptToHapiAuthFormat', () => { it('Should allow authenticating a user identity with given credentials', async () => { const credentials = {}; const authenticatedMock = jest.fn(); - const onAuth = adoptToHapiAuthFormat( - async (req, sessionStorage, t) => t.authenticated(credentials), - SessionStorageMock - ); + const onAuth = adoptToHapiAuthFormat((req, t) => t.authenticated(credentials)); await onAuth( - requestMock, - createResponseToolkit({ + httpServerMock.createRawRequest(), + httpServerMock.createRawResponseToolkit({ authenticated: authenticatedMock, }) ); @@ -47,15 +39,12 @@ describe('adoptToHapiAuthFormat', () => { it('Should allow redirecting to specified url', async () => { const redirectUrl = '/docs'; - const onAuth = adoptToHapiAuthFormat( - async (req, sessionStorage, t) => t.redirected(redirectUrl), - SessionStorageMock - ); + const onAuth = adoptToHapiAuthFormat((req, t) => t.redirected(redirectUrl)); const takeoverSymbol = {}; const redirectMock = jest.fn(() => ({ takeover: () => takeoverSymbol })); const result = await onAuth( - requestMock, - createResponseToolkit({ + httpServerMock.createRawRequest(), + httpServerMock.createRawResponseToolkit({ redirect: redirectMock, }) ); @@ -65,11 +54,13 @@ describe('adoptToHapiAuthFormat', () => { }); it('Should allow to specify statusCode and message for Boom error', async () => { - const onAuth = adoptToHapiAuthFormat( - async (req, sessionStorage, t) => t.rejected(new Error('not found'), { statusCode: 404 }), - SessionStorageMock + const onAuth = adoptToHapiAuthFormat((req, t) => + t.rejected(new Error('not found'), { statusCode: 404 }) ); - const result = (await onAuth(requestMock, createResponseToolkit())) as Boom; + const result = (await onAuth( + httpServerMock.createRawRequest(), + httpServerMock.createRawResponseToolkit() + )) as Boom; expect(result).toBeInstanceOf(Boom); expect(result.message).toBe('not found'); @@ -77,10 +68,13 @@ describe('adoptToHapiAuthFormat', () => { }); it('Should return Boom.internal error error if interceptor throws', async () => { - const onAuth = adoptToHapiAuthFormat(async (req, sessionStorage, t) => { + const onAuth = adoptToHapiAuthFormat((req, t) => { throw new Error('unknown error'); - }, SessionStorageMock); - const result = (await onAuth(requestMock, createResponseToolkit())) as Boom; + }); + const result = (await onAuth( + httpServerMock.createRawRequest(), + httpServerMock.createRawResponseToolkit() + )) as Boom; expect(result).toBeInstanceOf(Boom); expect(result.message).toBe('unknown error'); @@ -88,11 +82,11 @@ describe('adoptToHapiAuthFormat', () => { }); it('Should return Boom.internal error if interceptor returns unexpected result', async () => { - const onAuth = adoptToHapiAuthFormat( - async (req, sessionStorage, t) => undefined as any, - SessionStorageMock - ); - const result = (await onAuth(requestMock, createResponseToolkit())) as Boom; + const onAuth = adoptToHapiAuthFormat(async (req, t) => undefined as any); + const result = (await onAuth( + httpServerMock.createRawRequest(), + httpServerMock.createRawResponseToolkit() + )) as Boom; expect(result).toBeInstanceOf(Boom); expect(result.message).toBe( diff --git a/src/core/server/http/lifecycle/auth.ts b/src/core/server/http/lifecycle/auth.ts index 8205d21c5ff59..bcb7e454b4119 100644 --- a/src/core/server/http/lifecycle/auth.ts +++ b/src/core/server/http/lifecycle/auth.ts @@ -17,8 +17,8 @@ * under the License. */ import Boom from 'boom'; +import { noop } from 'lodash'; import { Lifecycle, Request, ResponseToolkit } from 'hapi'; -import { SessionStorage, SessionStorageFactory } from '../session_storage'; enum ResultType { authenticated = 'authenticated', @@ -26,39 +26,60 @@ enum ResultType { rejected = 'rejected', } -/** @internal */ -class AuthResult { - public static authenticated(credentials: any) { - return new AuthResult(ResultType.authenticated, credentials); - } - public static redirected(url: string) { - return new AuthResult(ResultType.redirected, url); - } - public static rejected(error: Error, options: { statusCode?: number } = {}) { - return new AuthResult(ResultType.rejected, { error, statusCode: options.statusCode }); - } - public static isValidResult(candidate: any) { - return candidate instanceof AuthResult; - } - constructor(private readonly type: ResultType, public readonly payload: any) {} - public isAuthenticated() { - return this.type === ResultType.authenticated; - } - public isRedirected() { - return this.type === ResultType.redirected; - } - public isRejected() { - return this.type === ResultType.rejected; - } +interface Authenticated { + type: ResultType.authenticated; + state: object; } +interface Redirected { + type: ResultType.redirected; + url: string; +} + +interface Rejected { + type: ResultType.rejected; + error: Error; + statusCode?: number; +} + +type AuthResult = Authenticated | Rejected | Redirected; + +const authResult = { + authenticated(state: object = {}): AuthResult { + return { type: ResultType.authenticated, state }; + }, + redirected(url: string): AuthResult { + return { type: ResultType.redirected, url }; + }, + rejected(error: Error, options: { statusCode?: number } = {}): AuthResult { + return { type: ResultType.rejected, error, statusCode: options.statusCode }; + }, + isValid(candidate: any): candidate is AuthResult { + return ( + candidate && + (candidate.type === ResultType.authenticated || + candidate.type === ResultType.rejected || + candidate.type === ResultType.redirected) + ); + }, + isAuthenticated(result: AuthResult): result is Authenticated { + return result.type === ResultType.authenticated; + }, + isRedirected(result: AuthResult): result is Redirected { + return result.type === ResultType.redirected; + }, + isRejected(result: AuthResult): result is Rejected { + return result.type === ResultType.rejected; + }, +}; + /** * @public * A tool set defining an outcome of Auth interceptor for incoming request. */ export interface AuthToolkit { /** Authentication is successful with given credentials, allow request to pass through */ - authenticated: (credentials: any) => AuthResult; + authenticated: (state?: object) => AuthResult; /** Authentication requires to interrupt request handling and redirect to a configured url */ redirected: (url: string) => AuthResult; /** Authentication is unsuccessful, fail the request with specified error. */ @@ -66,45 +87,42 @@ export interface AuthToolkit { } const toolkit: AuthToolkit = { - authenticated: AuthResult.authenticated, - redirected: AuthResult.redirected, - rejected: AuthResult.rejected, + authenticated: authResult.authenticated, + redirected: authResult.redirected, + rejected: authResult.rejected, }; /** @public */ -export type AuthenticationHandler = ( - request: Request, - sessionStorage: SessionStorage, +export type AuthenticationHandler = ( + request: Readonly, t: AuthToolkit -) => Promise; +) => AuthResult | Promise; /** @public */ -export function adoptToHapiAuthFormat( - fn: AuthenticationHandler, - sessionStorage: SessionStorageFactory +export function adoptToHapiAuthFormat( + fn: AuthenticationHandler, + onSuccess: (req: Request, state: unknown) => void = noop ) { return async function interceptAuth( req: Request, h: ResponseToolkit ): Promise { try { - const result = await fn(req, sessionStorage.asScoped(req), toolkit); - - if (AuthResult.isValidResult(result)) { - if (result.isAuthenticated()) { - return h.authenticated({ credentials: result.payload }); - } - if (result.isRedirected()) { - return h.redirect(result.payload).takeover(); - } - if (result.isRejected()) { - const { error, statusCode } = result.payload; - return Boom.boomify(error, { statusCode }); - } + const result = await fn(req, toolkit); + if (!authResult.isValid(result)) { + throw new Error( + `Unexpected result from Authenticate. Expected AuthResult, but given: ${result}.` + ); + } + if (authResult.isAuthenticated(result)) { + onSuccess(req, result.state); + return h.authenticated({ credentials: result.state }); + } + if (authResult.isRedirected(result)) { + return h.redirect(result.url).takeover(); } - throw new Error( - `Unexpected result from Authenticate. Expected AuthResult, but given: ${result}.` - ); + const { error, statusCode } = result; + return Boom.boomify(error, { statusCode }); } catch (error) { return Boom.internal(error.message, { statusCode: 500 }); } diff --git a/src/core/server/http/lifecycle/on_post_auth.test.ts b/src/core/server/http/lifecycle/on_post_auth.test.ts new file mode 100644 index 0000000000000..88e8fc91149b8 --- /dev/null +++ b/src/core/server/http/lifecycle/on_post_auth.test.ts @@ -0,0 +1,95 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Boom from 'boom'; +import { adoptToHapiOnPostAuthFormat } from './on_post_auth'; +import { httpServerMock } from '../http_server.mocks'; + +describe('adoptToHapiOnPostAuthFormat', () => { + it('Should allow passing request to the next handler', async () => { + const continueSymbol = Symbol(); + const onPostAuth = adoptToHapiOnPostAuthFormat((req, t) => t.next()); + const result = await onPostAuth( + httpServerMock.createRawRequest(), + httpServerMock.createRawResponseToolkit({ + ['continue']: continueSymbol, + }) + ); + + expect(result).toBe(continueSymbol); + }); + + it('Should support redirecting to specified url', async () => { + const redirectUrl = '/docs'; + const onPostAuth = adoptToHapiOnPostAuthFormat((req, t) => t.redirected(redirectUrl)); + const takeoverSymbol = {}; + const redirectMock = jest.fn(() => ({ takeover: () => takeoverSymbol })); + const result = await onPostAuth( + httpServerMock.createRawRequest(), + httpServerMock.createRawResponseToolkit({ + redirect: redirectMock, + }) + ); + + expect(redirectMock).toBeCalledWith(redirectUrl); + expect(result).toBe(takeoverSymbol); + }); + + it('Should support specifying statusCode and message for Boom error', async () => { + const onPostAuth = adoptToHapiOnPostAuthFormat((req, t) => { + return t.rejected(new Error('unexpected result'), { statusCode: 501 }); + }); + const result = (await onPostAuth( + httpServerMock.createRawRequest(), + httpServerMock.createRawResponseToolkit() + )) as Boom; + + expect(result).toBeInstanceOf(Boom); + expect(result.message).toBe('unexpected result'); + expect(result.output.statusCode).toBe(501); + }); + + it('Should return Boom.internal error if interceptor throws', async () => { + const onPostAuth = adoptToHapiOnPostAuthFormat((req, t) => { + throw new Error('unknown error'); + }); + const result = (await onPostAuth( + httpServerMock.createRawRequest(), + httpServerMock.createRawResponseToolkit() + )) as Boom; + + expect(result).toBeInstanceOf(Boom); + expect(result.message).toBe('unknown error'); + expect(result.output.statusCode).toBe(500); + }); + + it('Should return Boom.internal error if interceptor returns unexpected result', async () => { + const onPostAuth = adoptToHapiOnPostAuthFormat((req, toolkit) => undefined as any); + const result = (await onPostAuth( + httpServerMock.createRawRequest(), + httpServerMock.createRawResponseToolkit() + )) as Boom; + + expect(result).toBeInstanceOf(Boom); + expect(result.message).toMatchInlineSnapshot( + `"Unexpected result from OnPostAuth. Expected OnPostAuthResult, but given: undefined."` + ); + expect(result.output.statusCode).toBe(500); + }); +}); diff --git a/src/core/server/http/lifecycle/on_post_auth.ts b/src/core/server/http/lifecycle/on_post_auth.ts new file mode 100644 index 0000000000000..64d27bbe7c5c5 --- /dev/null +++ b/src/core/server/http/lifecycle/on_post_auth.ts @@ -0,0 +1,130 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Boom from 'boom'; +import { Lifecycle, Request, ResponseToolkit } from 'hapi'; +import { KibanaRequest } from '../router'; + +enum ResultType { + next = 'next', + redirected = 'redirected', + rejected = 'rejected', +} + +interface Next { + type: ResultType.next; +} + +interface Redirected { + type: ResultType.redirected; + url: string; +} + +interface Rejected { + type: ResultType.rejected; + error: Error; + statusCode?: number; +} + +type OnPostAuthResult = Next | Rejected | Redirected; + +const postAuthResult = { + next(): OnPostAuthResult { + return { type: ResultType.next }; + }, + redirected(url: string): OnPostAuthResult { + return { type: ResultType.redirected, url }; + }, + rejected(error: Error, options: { statusCode?: number } = {}): OnPostAuthResult { + return { type: ResultType.rejected, error, statusCode: options.statusCode }; + }, + isValid(candidate: any): candidate is OnPostAuthResult { + return ( + candidate && + (candidate.type === ResultType.next || + candidate.type === ResultType.rejected || + candidate.type === ResultType.redirected) + ); + }, + isNext(result: OnPostAuthResult): result is Next { + return result.type === ResultType.next; + }, + isRedirected(result: OnPostAuthResult): result is Redirected { + return result.type === ResultType.redirected; + }, + isRejected(result: OnPostAuthResult): result is Rejected { + return result.type === ResultType.rejected; + }, +}; + +/** + * @public + * A tool set defining an outcome of OnPostAuth interceptor for incoming request. + */ +export interface OnPostAuthToolkit { + /** To pass request to the next handler */ + next: () => OnPostAuthResult; + /** To interrupt request handling and redirect to a configured url */ + redirected: (url: string) => OnPostAuthResult; + /** Fail the request with specified error. */ + rejected: (error: Error, options?: { statusCode?: number }) => OnPostAuthResult; +} + +/** @public */ +export type OnPostAuthHandler = ( + request: KibanaRequest, + t: OnPostAuthToolkit +) => OnPostAuthResult | Promise; + +const toolkit: OnPostAuthToolkit = { + next: postAuthResult.next, + redirected: postAuthResult.redirected, + rejected: postAuthResult.rejected, +}; +/** + * @public + * Adopt custom request interceptor to Hapi lifecycle system. + * @param fn - an extension point allowing to perform custom logic for + * incoming HTTP requests. + */ +export function adoptToHapiOnPostAuthFormat(fn: OnPostAuthHandler) { + return async function interceptRequest( + request: Request, + h: ResponseToolkit + ): Promise { + try { + const result = await fn(KibanaRequest.from(request), toolkit); + if (!postAuthResult.isValid(result)) { + throw new Error( + `Unexpected result from OnPostAuth. Expected OnPostAuthResult, but given: ${result}.` + ); + } + if (postAuthResult.isNext(result)) { + return h.continue; + } + if (postAuthResult.isRedirected(result)) { + return h.redirect(result.url).takeover(); + } + const { error, statusCode } = result; + return Boom.boomify(error, { statusCode }); + } catch (error) { + return Boom.internal(error.message, { statusCode: 500 }); + } + }; +} diff --git a/src/core/server/http/lifecycle/on_pre_auth.test.ts b/src/core/server/http/lifecycle/on_pre_auth.test.ts new file mode 100644 index 0000000000000..bae7c9f16eb13 --- /dev/null +++ b/src/core/server/http/lifecycle/on_pre_auth.test.ts @@ -0,0 +1,115 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Boom from 'boom'; +import { adoptToHapiOnPreAuthFormat } from './on_pre_auth'; +import { httpServerMock } from '../http_server.mocks'; + +describe('adoptToHapiOnPreAuthFormat', () => { + it('Should allow passing request to the next handler', async () => { + const continueSymbol = Symbol(); + const onPreAuth = adoptToHapiOnPreAuthFormat((req, t) => t.next()); + const result = await onPreAuth( + httpServerMock.createRawRequest(), + httpServerMock.createRawResponseToolkit({ + ['continue']: continueSymbol, + }) + ); + + expect(result).toBe(continueSymbol); + }); + + it('Should support redirecting to specified url', async () => { + const redirectUrl = '/docs'; + const onPreAuth = adoptToHapiOnPreAuthFormat((req, t) => t.redirected(redirectUrl)); + const takeoverSymbol = {}; + const redirectMock = jest.fn(() => ({ takeover: () => takeoverSymbol })); + const result = await onPreAuth( + httpServerMock.createRawRequest(), + httpServerMock.createRawResponseToolkit({ + redirect: redirectMock, + }) + ); + + expect(redirectMock).toBeCalledWith(redirectUrl); + expect(result).toBe(takeoverSymbol); + }); + + it('Should support request forwarding to specified url', async () => { + const redirectUrl = '/docs'; + const onPreAuth = adoptToHapiOnPreAuthFormat((req, t) => + t.redirected(redirectUrl, { forward: true }) + ); + const continueSymbol = Symbol(); + const setUrl = jest.fn(); + const mockedRequest = httpServerMock.createRawRequest({ setUrl }); + const result = await onPreAuth( + mockedRequest, + httpServerMock.createRawResponseToolkit({ + ['continue']: continueSymbol, + }) + ); + + expect(setUrl).toBeCalledWith(redirectUrl); + expect(mockedRequest.raw.req.url).toBe(redirectUrl); + expect(result).toBe(continueSymbol); + }); + + it('Should support specifying statusCode and message for Boom error', async () => { + const onPreAuth = adoptToHapiOnPreAuthFormat((req, t) => { + return t.rejected(new Error('unexpected result'), { statusCode: 501 }); + }); + const result = (await onPreAuth( + httpServerMock.createRawRequest(), + httpServerMock.createRawResponseToolkit() + )) as Boom; + + expect(result).toBeInstanceOf(Boom); + expect(result.message).toBe('unexpected result'); + expect(result.output.statusCode).toBe(501); + }); + + it('Should return Boom.internal error if interceptor throws', async () => { + const onPreAuth = adoptToHapiOnPreAuthFormat((req, t) => { + throw new Error('unknown error'); + }); + const result = (await onPreAuth( + httpServerMock.createRawRequest(), + httpServerMock.createRawResponseToolkit() + )) as Boom; + + expect(result).toBeInstanceOf(Boom); + expect(result.message).toBe('unknown error'); + expect(result.output.statusCode).toBe(500); + }); + + it('Should return Boom.internal error if interceptor returns unexpected result', async () => { + const onPreAuth = adoptToHapiOnPreAuthFormat((req, toolkit) => undefined as any); + const result = (await onPreAuth( + httpServerMock.createRawRequest(), + httpServerMock.createRawResponseToolkit() + )) as Boom; + + expect(result).toBeInstanceOf(Boom); + expect(result.message).toMatchInlineSnapshot( + `"Unexpected result from OnPreAuth. Expected OnPreAuthResult, but given: undefined."` + ); + expect(result.output.statusCode).toBe(500); + }); +}); diff --git a/src/core/server/http/lifecycle/on_pre_auth.ts b/src/core/server/http/lifecycle/on_pre_auth.ts new file mode 100644 index 0000000000000..13b267b148666 --- /dev/null +++ b/src/core/server/http/lifecycle/on_pre_auth.ts @@ -0,0 +1,145 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Boom from 'boom'; +import { Lifecycle, Request, ResponseToolkit } from 'hapi'; +import { KibanaRequest } from '../router'; + +enum ResultType { + next = 'next', + redirected = 'redirected', + rejected = 'rejected', +} + +interface Next { + type: ResultType.next; +} + +interface Redirected { + type: ResultType.redirected; + url: string; + forward?: boolean; +} + +interface Rejected { + type: ResultType.rejected; + error: Error; + statusCode?: number; +} + +type OnPreAuthResult = Next | Rejected | Redirected; + +const preAuthResult = { + next(): OnPreAuthResult { + return { type: ResultType.next }; + }, + redirected(url: string, options: { forward?: boolean } = {}): OnPreAuthResult { + return { type: ResultType.redirected, url, forward: options.forward }; + }, + rejected(error: Error, options: { statusCode?: number } = {}): OnPreAuthResult { + return { type: ResultType.rejected, error, statusCode: options.statusCode }; + }, + isValid(candidate: any): candidate is OnPreAuthResult { + return ( + candidate && + (candidate.type === ResultType.next || + candidate.type === ResultType.rejected || + candidate.type === ResultType.redirected) + ); + }, + isNext(result: OnPreAuthResult): result is Next { + return result.type === ResultType.next; + }, + isRedirected(result: OnPreAuthResult): result is Redirected { + return result.type === ResultType.redirected; + }, + isRejected(result: OnPreAuthResult): result is Rejected { + return result.type === ResultType.rejected; + }, +}; + +/** + * @public + * A tool set defining an outcome of OnPreAuth interceptor for incoming request. + */ +export interface OnPreAuthToolkit { + /** To pass request to the next handler */ + next: () => OnPreAuthResult; + /** + * To interrupt request handling and redirect to a configured url. + * If "options.forwarded" = true, request will be forwarded to another url right on the server. + * */ + redirected: (url: string, options?: { forward: boolean }) => OnPreAuthResult; + /** Fail the request with specified error. */ + rejected: (error: Error, options?: { statusCode?: number }) => OnPreAuthResult; +} + +const toolkit: OnPreAuthToolkit = { + next: preAuthResult.next, + redirected: preAuthResult.redirected, + rejected: preAuthResult.rejected, +}; + +/** @public */ +export type OnPreAuthHandler = ( + request: KibanaRequest, + t: OnPreAuthToolkit +) => OnPreAuthResult | Promise; + +/** + * @public + * Adopt custom request interceptor to Hapi lifecycle system. + * @param fn - an extension point allowing to perform custom logic for + * incoming HTTP requests. + */ +export function adoptToHapiOnPreAuthFormat(fn: OnPreAuthHandler) { + return async function interceptPreAuthRequest( + request: Request, + h: ResponseToolkit + ): Promise { + try { + const result = await fn(KibanaRequest.from(request), toolkit); + + if (!preAuthResult.isValid(result)) { + throw new Error( + `Unexpected result from OnPreAuth. Expected OnPreAuthResult, but given: ${result}.` + ); + } + if (preAuthResult.isNext(result)) { + return h.continue; + } + + if (preAuthResult.isRedirected(result)) { + const { url, forward } = result; + if (forward) { + request.setUrl(url); + // We should update raw request as well since it can be proxied to the old platform + request.raw.req.url = url; + return h.continue; + } + return h.redirect(url).takeover(); + } + + const { error, statusCode } = result; + return Boom.boomify(error, { statusCode }); + } catch (error) { + return Boom.internal(error.message, { statusCode: 500 }); + } + }; +} diff --git a/src/core/server/http/lifecycle/on_request.test.ts b/src/core/server/http/lifecycle/on_request.test.ts deleted file mode 100644 index bc4410c773288..0000000000000 --- a/src/core/server/http/lifecycle/on_request.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import Boom from 'boom'; -import { adoptToHapiOnRequestFormat } from './on_request'; - -const requestMock = {} as any; -const createResponseToolkit = (customization = {}): any => ({ ...customization }); - -describe('adoptToHapiOnRequestFormat', () => { - it('Should allow passing request to the next handler', async () => { - const continueSymbol = {}; - const onRequest = adoptToHapiOnRequestFormat((req, t) => t.next()); - const result = await onRequest( - requestMock, - createResponseToolkit({ - ['continue']: continueSymbol, - }) - ); - - expect(result).toBe(continueSymbol); - }); - - it('Should support redirecting to specified url', async () => { - const redirectUrl = '/docs'; - const onRequest = adoptToHapiOnRequestFormat((req, t) => t.redirected(redirectUrl)); - const takeoverSymbol = {}; - const redirectMock = jest.fn(() => ({ takeover: () => takeoverSymbol })); - const result = await onRequest( - requestMock, - createResponseToolkit({ - redirect: redirectMock, - }) - ); - - expect(redirectMock).toBeCalledWith(redirectUrl); - expect(result).toBe(takeoverSymbol); - }); - - it('Should support specifying statusCode and message for Boom error', async () => { - const onRequest = adoptToHapiOnRequestFormat((req, t) => { - return t.rejected(new Error('unexpected result'), { statusCode: 501 }); - }); - const result = (await onRequest(requestMock, createResponseToolkit())) as Boom; - - expect(result).toBeInstanceOf(Boom); - expect(result.message).toBe('unexpected result'); - expect(result.output.statusCode).toBe(501); - }); - - it('Should return Boom.internal error if interceptor throws', async () => { - const onRequest = adoptToHapiOnRequestFormat((req, t) => { - throw new Error('unknown error'); - }); - const result = (await onRequest(requestMock, createResponseToolkit())) as Boom; - - expect(result).toBeInstanceOf(Boom); - expect(result.message).toBe('unknown error'); - expect(result.output.statusCode).toBe(500); - }); - - it('Should return Boom.internal error if interceptor returns unexpected result', async () => { - const onRequest = adoptToHapiOnRequestFormat((req, toolkit) => undefined as any); - const result = (await onRequest(requestMock, createResponseToolkit())) as Boom; - - expect(result).toBeInstanceOf(Boom); - expect(result.message).toBe( - 'Unexpected result from OnRequest. Expected OnRequestResult, but given: undefined.' - ); - expect(result.output.statusCode).toBe(500); - }); -}); diff --git a/src/core/server/http/lifecycle/on_request.ts b/src/core/server/http/lifecycle/on_request.ts deleted file mode 100644 index 168b4f513400f..0000000000000 --- a/src/core/server/http/lifecycle/on_request.ts +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Url } from 'url'; -import Boom from 'boom'; -import { Lifecycle, Request, ResponseToolkit } from 'hapi'; -import { KibanaRequest } from '../router'; - -enum ResultType { - next = 'next', - redirected = 'redirected', - rejected = 'rejected', -} - -/** @internal */ -class OnRequestResult { - public static next() { - return new OnRequestResult(ResultType.next); - } - public static redirected(url: string) { - return new OnRequestResult(ResultType.redirected, url); - } - public static rejected(error: Error, options: { statusCode?: number } = {}) { - return new OnRequestResult(ResultType.rejected, { error, statusCode: options.statusCode }); - } - public static isValidResult(candidate: any) { - return candidate instanceof OnRequestResult; - } - constructor(private readonly type: ResultType, public readonly payload?: any) {} - public isNext() { - return this.type === ResultType.next; - } - public isRedirected() { - return this.type === ResultType.redirected; - } - public isRejected() { - return this.type === ResultType.rejected; - } -} - -/** - * @public - * A tool set defining an outcome of OnRequest interceptor for incoming request. - */ -export interface OnRequestToolkit { - /** To pass request to the next handler */ - next: () => OnRequestResult; - /** To interrupt request handling and redirect to a configured url */ - redirected: (url: string) => OnRequestResult; - /** Fail the request with specified error. */ - rejected: (error: Error, options?: { statusCode?: number }) => OnRequestResult; - /** Change url for an incoming request. */ - setUrl: (newUrl: string | Url) => void; -} - -/** @public */ -export type OnRequestHandler = ( - req: KibanaRequest, - t: OnRequestToolkit -) => OnRequestResult | Promise; - -/** - * @public - * Adopt custom request interceptor to Hapi lifecycle system. - * @param fn - an extension point allowing to perform custom logic for - * incoming HTTP requests. - */ -export function adoptToHapiOnRequestFormat(fn: OnRequestHandler) { - return async function interceptRequest( - request: Request, - h: ResponseToolkit - ): Promise { - try { - const result = await fn(KibanaRequest.from(request, undefined), { - next: OnRequestResult.next, - redirected: OnRequestResult.redirected, - rejected: OnRequestResult.rejected, - setUrl: (newUrl: string | Url) => { - request.setUrl(newUrl); - // We should update raw request as well since it can be proxied to the old platform - request.raw.req.url = typeof newUrl === 'string' ? newUrl : newUrl.href; - }, - }); - if (OnRequestResult.isValidResult(result)) { - if (result.isNext()) { - return h.continue; - } - if (result.isRedirected()) { - return h.redirect(result.payload).takeover(); - } - if (result.isRejected()) { - const { error, statusCode } = result.payload; - return Boom.boomify(error, { statusCode }); - } - } - - throw new Error( - `Unexpected result from OnRequest. Expected OnRequestResult, but given: ${result}.` - ); - } catch (error) { - return Boom.internal(error.message, { statusCode: 500 }); - } - }; -} diff --git a/src/core/server/http/router/index.ts b/src/core/server/http/router/index.ts index 7be20cccd589a..cb941326e23f1 100644 --- a/src/core/server/http/router/index.ts +++ b/src/core/server/http/router/index.ts @@ -19,4 +19,5 @@ export { Headers, filterHeaders } from './headers'; export { Router } from './router'; -export { KibanaRequest } from './request'; +export { KibanaRequest, KibanaRequestRoute, toRawRequest } from './request'; +export { RouteMethod, RouteConfigOptions } from './route'; diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts index 03b62f4948306..3c235ffbf8bd9 100644 --- a/src/core/server/http/router/request.ts +++ b/src/core/server/http/router/request.ts @@ -17,21 +17,39 @@ * under the License. */ +import { Url } from 'url'; import { ObjectType, TypeOf } from '@kbn/config-schema'; import { Request } from 'hapi'; +import { deepFreeze, RecursiveReadonly } from '../../../utils'; import { filterHeaders, Headers } from './headers'; -import { RouteSchemas } from './route'; +import { RouteMethod, RouteSchemas, RouteConfigOptions } from './route'; -/** @public */ +const requestSymbol = Symbol('request'); + +/** + * Request specific route information exposed to a handler. + * @public + * */ +export interface KibanaRequestRoute { + path: string; + method: RouteMethod | 'patch' | 'options'; + options: Required; +} + +/** + * Kibana specific abstraction for an incoming request. + * @public + * */ export class KibanaRequest { /** * Factory for creating requests. Validates the request before creating an * instance of a KibanaRequest. + * @internal */ public static from

( req: Request, - routeSchemas: RouteSchemas | undefined + routeSchemas?: RouteSchemas ) { const requestParts = KibanaRequest.validate(req, routeSchemas); return new KibanaRequest(req, requestParts.params, requestParts.query, requestParts.body); @@ -69,24 +87,44 @@ export class KibanaRequest { } public readonly headers: Headers; - public readonly path: string; + public readonly url: Url; + public readonly route: RecursiveReadonly; + + /** @internal */ + protected readonly [requestSymbol]: Request; constructor( - private readonly request: Request, + request: Request, readonly params: Params, readonly query: Query, readonly body: Body ) { this.headers = request.headers; - this.path = request.path; + this.url = request.url; + + this[requestSymbol] = request; + this.route = deepFreeze(this.getRouteInfo()); } public getFilteredHeaders(headersToKeep: string[]) { return filterHeaders(this.headers, headersToKeep); } - // eslint-disable-next-line @typescript-eslint/camelcase - public unstable_getIncomingMessage() { - return this.request.raw.req; + private getRouteInfo() { + const request = this[requestSymbol]; + return { + path: request.path, + method: request.method, + options: { + authRequired: request.route.settings.auth !== false, + tags: request.route.settings.tags || [], + }, + }; } } + +/** + * Returns underlying Hapi Request object for KibanaRequest + * @internal + */ +export const toRawRequest = (request: KibanaRequest) => request[requestSymbol]; diff --git a/src/core/server/http/router/route.ts b/src/core/server/http/router/route.ts index 64ed67e8f940b..c44b03ac87c1a 100644 --- a/src/core/server/http/router/route.ts +++ b/src/core/server/http/router/route.ts @@ -18,7 +18,31 @@ */ import { ObjectType, Schema } from '@kbn/config-schema'; -export type RouteMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'; +/** + * The set of common HTTP methods supported by Kibana routing. + * @public + * */ +export type RouteMethod = 'get' | 'post' | 'put' | 'delete'; + +/** + * Route specific configuration. + * @public + * */ +export interface RouteConfigOptions { + /** + * A flag shows that authentication for a route: + * enabled when true + * disabled when false + * + * Enabled by default. + */ + authRequired?: boolean; + + /** + * Additional metadata tag strings to attach to the route. + */ + tags?: ReadonlyArray; +} export interface RouteConfig

{ /** @@ -35,6 +59,8 @@ export interface RouteConfig

| false; + + options?: RouteConfigOptions; } export type RouteValidateFactory< diff --git a/src/core/server/http/router/router.ts b/src/core/server/http/router/router.ts index a640a413fd81b..817832bdc59d7 100644 --- a/src/core/server/http/router/router.ts +++ b/src/core/server/http/router/router.ts @@ -22,11 +22,12 @@ import { Request, ResponseObject, ResponseToolkit } from 'hapi'; import { KibanaRequest } from './request'; import { KibanaResponse, ResponseFactory, responseFactory } from './response'; -import { RouteConfig, RouteMethod, RouteSchemas } from './route'; +import { RouteConfig, RouteConfigOptions, RouteMethod, RouteSchemas } from './route'; export interface RouterRoute { - method: 'GET' | 'POST' | 'PUT' | 'DELETE'; + method: RouteMethod; path: string; + options: RouteConfigOptions; handler: (req: Request, responseToolkit: ResponseToolkit) => Promise; } @@ -43,12 +44,14 @@ export class Router { route: RouteConfig, handler: RequestHandler ) { - const routeSchemas = this.routeSchemasFromRouteConfig(route, 'GET'); + const { path, options = {} } = route; + const routeSchemas = this.routeSchemasFromRouteConfig(route, 'get'); this.routes.push({ handler: async (req, responseToolkit) => await this.handle(routeSchemas, req, responseToolkit, handler), - method: 'GET', - path: route.path, + method: 'get', + path, + options, }); } @@ -59,12 +62,14 @@ export class Router { route: RouteConfig, handler: RequestHandler ) { - const routeSchemas = this.routeSchemasFromRouteConfig(route, 'POST'); + const { path, options = {} } = route; + const routeSchemas = this.routeSchemasFromRouteConfig(route, 'post'); this.routes.push({ handler: async (req, responseToolkit) => await this.handle(routeSchemas, req, responseToolkit, handler), - method: 'POST', - path: route.path, + method: 'post', + path, + options, }); } @@ -75,12 +80,14 @@ export class Router { route: RouteConfig, handler: RequestHandler ) { - const routeSchemas = this.routeSchemasFromRouteConfig(route, 'POST'); + const { path, options = {} } = route; + const routeSchemas = this.routeSchemasFromRouteConfig(route, 'put'); this.routes.push({ handler: async (req, responseToolkit) => await this.handle(routeSchemas, req, responseToolkit, handler), - method: 'PUT', - path: route.path, + method: 'put', + path, + options, }); } @@ -91,12 +98,14 @@ export class Router { route: RouteConfig, handler: RequestHandler ) { - const routeSchemas = this.routeSchemasFromRouteConfig(route, 'DELETE'); + const { path, options = {} } = route; + const routeSchemas = this.routeSchemasFromRouteConfig(route, 'delete'); this.routes.push({ handler: async (req, responseToolkit) => await this.handle(routeSchemas, req, responseToolkit, handler), - method: 'DELETE', - path: route.path, + method: 'delete', + path, + options, }); } diff --git a/src/core/server/http/session_storage.ts b/src/core/server/http/session_storage.ts index 4f9d28991fe78..2c726ce34a3cb 100644 --- a/src/core/server/http/session_storage.ts +++ b/src/core/server/http/session_storage.ts @@ -18,8 +18,10 @@ */ import { Request } from 'hapi'; +import { KibanaRequest } from './router'; /** * Provides an interface to store and retrieve data across requests. + * @public */ export interface SessionStorage { /** @@ -37,6 +39,9 @@ export interface SessionStorage { clear(): void; } +/** + * SessionStorage factory to bind one to an incoming request + * @public */ export interface SessionStorageFactory { - asScoped: (request: Request) => SessionStorage; + asScoped: (request: Readonly | KibanaRequest) => SessionStorage; } diff --git a/src/core/server/http/ssl_config.ts b/src/core/server/http/ssl_config.ts index 7db0a95ca7760..bab3e1d12615c 100644 --- a/src/core/server/http/ssl_config.ts +++ b/src/core/server/http/ssl_config.ts @@ -30,7 +30,7 @@ const protocolMap = new Map([ ['TLSv1.2', cryptoConstants.SSL_OP_NO_TLSv1_2], ]); -const sslSchema = schema.object( +export const sslSchema = schema.object( { certificate: schema.maybe(schema.string()), certificateAuthorities: schema.maybe( @@ -62,11 +62,6 @@ const sslSchema = schema.object( type SslConfigType = TypeOf; export class SslConfig { - /** - * @internal - */ - public static schema = sslSchema; - public enabled: boolean; public redirectHttpFromPort: number | undefined; public key: string | undefined; diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 8d8b38d2e78de..4cebe24f39f15 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -16,7 +16,27 @@ * specific language governing permissions and limitations * under the License. */ -import { ElasticsearchServiceSetup } from './elasticsearch'; + +/** + * The Kibana Core APIs for server-side plugins. + * + * A plugin's `server/index` file must contain a named import, `plugin`, that + * implements {@link PluginInitializer} which returns an object that implements + * {@link Plugin}. + * + * The plugin integrates with the core system via lifecycle events: `setup`, + * `start`, and `stop`. In each lifecycle method, the plugin will receive the + * corresponding core services available (either {@link CoreSetup} or + * {@link CoreStart}) and any interfaces returned by dependency plugins' + * lifecycle method. Anything returned by the plugin's lifecycle method will be + * exposed to downstream dependencies when their corresponding lifecycle methods + * are invoked. + * + * @packageDocumentation + */ + +import { Observable } from 'rxjs'; +import { ClusterClient, ElasticsearchServiceSetup } from './elasticsearch'; import { HttpServiceSetup, HttpServiceStart } from './http'; import { PluginsServiceSetup, PluginsServiceStart } from './plugins'; @@ -34,9 +54,16 @@ export { AuthenticationHandler, AuthToolkit, KibanaRequest, - OnRequestHandler, - OnRequestToolkit, + KibanaRequestRoute, + OnPreAuthHandler, + OnPreAuthToolkit, + OnPostAuthHandler, + OnPostAuthToolkit, Router, + RouteMethod, + RouteConfigOptions, + SessionStorageFactory, + SessionStorage, } from './http'; export { Logger, LoggerFactory, LogMeta, LogRecord, LogLevel } from './logging'; @@ -46,12 +73,39 @@ export { PluginInitializer, PluginInitializerContext, PluginName, - PluginSetupContext, - PluginStartContext, } from './plugins'; -/** @public */ +export { RecursiveReadonly } from '../utils'; + +/** + * Context passed to the plugins `setup` method. + * + * @public + */ export interface CoreSetup { + elasticsearch: { + adminClient$: Observable; + dataClient$: Observable; + }; + http: { + registerOnPreAuth: HttpServiceSetup['registerOnPreAuth']; + registerAuth: HttpServiceSetup['registerAuth']; + registerOnPostAuth: HttpServiceSetup['registerOnPostAuth']; + getBasePathFor: HttpServiceSetup['getBasePathFor']; + setBasePathFor: HttpServiceSetup['setBasePathFor']; + createNewServer: HttpServiceSetup['createNewServer']; + }; +} + +/** + * Context passed to the plugins `start` method. + * + * @public + */ +export interface CoreStart {} // eslint-disable-line @typescript-eslint/no-empty-interface + +/** @internal */ +export interface InternalCoreSetup { http: HttpServiceSetup; elasticsearch: ElasticsearchServiceSetup; plugins: PluginsServiceSetup; @@ -60,7 +114,7 @@ export interface CoreSetup { /** * @public */ -export interface CoreStart { +export interface InternalCoreStart { http: HttpServiceStart; plugins: PluginsServiceStart; } diff --git a/src/core/server/legacy/__snapshots__/legacy_service.test.ts.snap b/src/core/server/legacy/__snapshots__/legacy_service.test.ts.snap index 82192eda44a80..bc7e8f72c4d61 100644 --- a/src/core/server/legacy/__snapshots__/legacy_service.test.ts.snap +++ b/src/core/server/legacy/__snapshots__/legacy_service.test.ts.snap @@ -1,62 +1,24 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`once LegacyService is set up in \`devClusterMaster\` mode creates ClusterManager with base path proxy.: cluster manager with base path proxy 1`] = ` -Array [ - Array [ - Object { - "basePath": true, - "dev": true, - "open": false, - "optimize": false, - "quiet": true, - "repl": false, - "silent": false, - "watch": false, - }, - Object { - "server": Object { - "autoListen": true, - }, - }, - BasePathProxyServer { - "devConfig": Object { - "basePathProxyTargetPort": 100500, - }, - "httpConfig": Object { - "basePath": "/abc", - "maxPayload": ByteSizeValue { - "valueInBytes": 1073741824, - }, - }, - "httpsAgent": undefined, - "log": Object { - "context": Array [ - "server", - ], - "debug": [MockFunction] { - "calls": Array [ - Array [ - "starting legacy service", - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, - "error": [MockFunction], - "fatal": [MockFunction], - "info": [MockFunction], - "log": [MockFunction], - "trace": [MockFunction], - "warn": [MockFunction], - }, - "server": undefined, - }, - ], -] +exports[`once LegacyService is set up in \`devClusterMaster\` mode creates ClusterManager with base path proxy.: cli args. cluster manager with base path proxy 1`] = ` +Object { + "basePath": true, + "dev": true, + "open": false, + "optimize": false, + "quiet": true, + "repl": false, + "silent": false, + "watch": false, +} +`; + +exports[`once LegacyService is set up in \`devClusterMaster\` mode creates ClusterManager with base path proxy.: config. cluster manager with base path proxy 1`] = ` +Object { + "server": Object { + "autoListen": true, + }, +} `; exports[`once LegacyService is set up in \`devClusterMaster\` mode creates ClusterManager without base path proxy.: cluster manager without base path proxy 1`] = ` diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index a63e4a44a3910..759a2eb76fd0c 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -32,7 +32,7 @@ import { Config, Env, ObjectToConfigAdapter } from '../config'; import { getEnvOptions } from '../config/__mocks__/env'; import { configServiceMock } from '../config/config_service.mock'; import { ElasticsearchServiceSetup } from '../elasticsearch'; -import { HttpServiceStart } from '../http'; +import { HttpServiceStart, BasePathProxyServer } from '../http'; import { loggingServiceMock } from '../logging/logging_service.mock'; import { DiscoveredPlugin, DiscoveredPluginInternal } from '../plugins'; import { PluginsServiceSetup, PluginsServiceStart } from '../plugins/plugins_service'; @@ -41,18 +41,23 @@ import { LegacyPlatformProxy } from './legacy_platform_proxy'; const MockKbnServer: jest.Mock = KbnServer as any; const MockLegacyPlatformProxy: jest.Mock = LegacyPlatformProxy as any; -let legacyService: LegacyService; let env: Env; let config$: BehaviorSubject; let setupDeps: { - elasticsearch: ElasticsearchServiceSetup; - http: any; - plugins: PluginsServiceSetup; + core: { + elasticsearch: ElasticsearchServiceSetup; + http: any; + plugins: PluginsServiceSetup; + }; + plugins: Record; }; let startDeps: { - http: HttpServiceStart; - plugins: PluginsServiceStart; + core: { + http: HttpServiceStart; + plugins: PluginsServiceStart; + }; + plugins: Record; }; const logger = loggingServiceMock.create(); @@ -65,27 +70,31 @@ beforeEach(() => { MockKbnServer.prototype.ready = jest.fn().mockReturnValue(Promise.resolve()); setupDeps = { - elasticsearch: { legacy: {} } as any, - http: { - options: { someOption: 'foo', someAnotherOption: 'bar' }, - server: { listener: { addListener: jest.fn() }, route: jest.fn() }, - }, - plugins: { - contracts: new Map([['plugin-id', 'plugin-value']]), - uiPlugins: { - public: new Map([['plugin-id', {} as DiscoveredPlugin]]), - internal: new Map([['plugin-id', {} as DiscoveredPluginInternal]]), + core: { + elasticsearch: { legacy: {} } as any, + http: { + options: { someOption: 'foo', someAnotherOption: 'bar' }, + server: { listener: { addListener: jest.fn() }, route: jest.fn() }, + }, + plugins: { + contracts: new Map([['plugin-id', 'plugin-value']]), + uiPlugins: { + public: new Map([['plugin-id', {} as DiscoveredPlugin]]), + internal: new Map([['plugin-id', {} as DiscoveredPluginInternal]]), + }, }, }, + plugins: { 'plugin-id': 'plugin-value' }, }; startDeps = { - http: { - isListening: () => true, - }, - plugins: { - contracts: new Map(), + core: { + http: { + isListening: () => true, + }, + plugins: { contracts: new Map() }, }, + plugins: {}, }; config$ = new BehaviorSubject( @@ -97,8 +106,6 @@ beforeEach(() => { configService.getConfig$.mockReturnValue(config$); configService.getUsedPaths.mockResolvedValue(['foo.bar']); - - legacyService = new LegacyService({ env, logger, configService: configService as any }); }); afterEach(() => { @@ -107,14 +114,16 @@ afterEach(() => { describe('once LegacyService is set up with connection info', () => { test('register proxy route.', async () => { + const legacyService = new LegacyService({ env, logger, configService: configService as any }); await legacyService.setup(setupDeps); await legacyService.start(startDeps); - expect(setupDeps.http.server.route.mock.calls).toMatchSnapshot('proxy route options'); + expect(setupDeps.core.http.server.route.mock.calls).toMatchSnapshot('proxy route options'); }); test('proxy route responds with `503` if `kbnServer` is not ready yet.', async () => { configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); + const legacyService = new LegacyService({ env, logger, configService: configService as any }); const kbnServerListen$ = new Subject(); MockKbnServer.prototype.listen = jest.fn(() => { @@ -138,7 +147,7 @@ describe('once LegacyService is set up with connection info', () => { }; const mockRequest = { raw: { req: { a: 1 }, res: { b: 2 } } }; - const [[{ handler }]] = setupDeps.http.server.route.mock.calls; + const [[{ handler }]] = setupDeps.core.http.server.route.mock.calls; const response503 = await handler(mockRequest, mockResponseToolkit); expect(response503).toBe(mockResponse); @@ -172,6 +181,7 @@ describe('once LegacyService is set up with connection info', () => { test('creates legacy kbnServer and calls `listen`.', async () => { configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); + const legacyService = new LegacyService({ env, logger, configService: configService as any }); await legacyService.setup(setupDeps); await legacyService.start(startDeps); @@ -200,6 +210,7 @@ describe('once LegacyService is set up with connection info', () => { test('creates legacy kbnServer but does not call `listen` if `autoListen: false`.', async () => { configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: false })); + const legacyService = new LegacyService({ env, logger, configService: configService as any }); await legacyService.setup(setupDeps); await legacyService.start(startDeps); @@ -228,6 +239,7 @@ describe('once LegacyService is set up with connection info', () => { test('creates legacy kbnServer and closes it if `listen` fails.', async () => { configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); MockKbnServer.prototype.listen.mockRejectedValue(new Error('something failed')); + const legacyService = new LegacyService({ env, logger, configService: configService as any }); await legacyService.setup(setupDeps); await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingSnapshot(); @@ -239,6 +251,7 @@ describe('once LegacyService is set up with connection info', () => { test('throws if fails to retrieve initial config.', async () => { configService.getConfig$.mockReturnValue(throwError(new Error('something failed'))); + const legacyService = new LegacyService({ env, logger, configService: configService as any }); await legacyService.setup(setupDeps); await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingSnapshot(); @@ -248,6 +261,7 @@ describe('once LegacyService is set up with connection info', () => { }); test('reconfigures logging configuration if new config is received.', async () => { + const legacyService = new LegacyService({ env, logger, configService: configService as any }); await legacyService.setup(setupDeps); await legacyService.start(startDeps); @@ -262,6 +276,7 @@ describe('once LegacyService is set up with connection info', () => { }); test('logs error if re-configuring fails.', async () => { + const legacyService = new LegacyService({ env, logger, configService: configService as any }); await legacyService.setup(setupDeps); await legacyService.start(startDeps); @@ -280,6 +295,7 @@ describe('once LegacyService is set up with connection info', () => { }); test('logs error if config service fails.', async () => { + const legacyService = new LegacyService({ env, logger, configService: configService as any }); await legacyService.setup(setupDeps); await legacyService.start(startDeps); @@ -295,13 +311,14 @@ describe('once LegacyService is set up with connection info', () => { }); test('proxy route abandons request processing and forwards it to the legacy Kibana', async () => { + const legacyService = new LegacyService({ env, logger, configService: configService as any }); const mockResponseToolkit = { response: jest.fn(), abandon: Symbol('abandon') }; const mockRequest = { raw: { req: { a: 1 }, res: { b: 2 } } }; await legacyService.setup(setupDeps); await legacyService.start(startDeps); - const [[{ handler }]] = setupDeps.http.server.route.mock.calls; + const [[{ handler }]] = setupDeps.core.http.server.route.mock.calls; const response = await handler(mockRequest, mockResponseToolkit); expect(response).toBe(mockResponseToolkit.abandon); @@ -320,20 +337,24 @@ describe('once LegacyService is set up with connection info', () => { describe('once LegacyService is set up without connection info', () => { const disabledHttpStartDeps = { - http: { - isListening: () => false, - }, - plugins: { - contracts: new Map(), + core: { + http: { + isListening: () => false, + }, + plugins: { contracts: new Map() }, }, + plugins: {}, }; + let legacyService: LegacyService; beforeEach(async () => { + legacyService = new LegacyService({ env, logger, configService: configService as any }); + await legacyService.setup(setupDeps); await legacyService.start(disabledHttpStartDeps); }); test('creates legacy kbnServer with `autoListen: false`.', () => { - expect(setupDeps.http.server.route).not.toHaveBeenCalled(); + expect(setupDeps.core.http.server.route).not.toHaveBeenCalled(); expect(MockKbnServer).toHaveBeenCalledTimes(1); expect(MockKbnServer).toHaveBeenCalledWith( { server: { autoListen: true } }, @@ -380,18 +401,15 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => { configService: configService as any, }); - await devClusterLegacyService.setup({ - elasticsearch: setupDeps.elasticsearch, - plugins: { contracts: new Map(), uiPlugins: { public: new Map(), internal: new Map() } }, - http: setupDeps.http, - }); + await devClusterLegacyService.setup(setupDeps); await devClusterLegacyService.start({ - http: { - isListening: () => false, - }, - plugins: { - contracts: new Map(), + core: { + http: { + isListening: () => false, + }, + plugins: { contracts: new Map() }, }, + plugins: {}, }); expect(MockClusterManager.create.mock.calls).toMatchSnapshot( @@ -411,27 +429,28 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => { configService: configService as any, }); - await devClusterLegacyService.setup({ - elasticsearch: setupDeps.elasticsearch, - plugins: { contracts: new Map(), uiPlugins: { public: new Map(), internal: new Map() } }, - http: setupDeps.http, - }); + await devClusterLegacyService.setup(setupDeps); await devClusterLegacyService.start({ - http: { - isListening: () => false, - }, - plugins: { - contracts: new Map(), + core: { + http: { + isListening: () => false, + }, + plugins: { contracts: new Map() }, }, + plugins: {}, }); - expect(MockClusterManager.create.mock.calls).toMatchSnapshot( - 'cluster manager with base path proxy' - ); + expect(MockClusterManager.create).toBeCalledTimes(1); + + const [[cliArgs, config, basePathProxy]] = MockClusterManager.create.mock.calls; + expect(cliArgs).toMatchSnapshot('cli args. cluster manager with base path proxy'); + expect(config).toMatchSnapshot('config. cluster manager with base path proxy'); + expect(basePathProxy).toBeInstanceOf(BasePathProxyServer); }); }); test('Cannot start without setup phase', async () => { + const legacyService = new LegacyService({ env, logger, configService: configService as any }); await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingInlineSnapshot( `"Legacy service is not setup yet."` ); diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 8ca561569ee5e..fd1b46d7fa711 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -18,14 +18,14 @@ */ import { Server as HapiServer } from 'hapi'; -import { combineLatest, ConnectableObservable, EMPTY, Subscription } from 'rxjs'; +import { combineLatest, ConnectableObservable, EMPTY, Observable, Subscription } from 'rxjs'; import { first, map, mergeMap, publishReplay, tap } from 'rxjs/operators'; import { CoreService } from '../../types'; -import { CoreSetup, CoreStart } from '../../server'; +import { InternalCoreSetup, InternalCoreStart } from '../../server'; import { Config } from '../config'; import { CoreContext } from '../core_context'; -import { DevConfig } from '../dev'; -import { BasePathProxyServer, HttpConfig } from '../http'; +import { DevConfig, DevConfigType } from '../dev'; +import { BasePathProxyServer, HttpConfig, HttpConfigType } from '../http'; import { Logger } from '../logging'; import { LegacyPlatformProxy } from './legacy_platform_proxy'; @@ -48,20 +48,38 @@ function getLegacyRawConfig(config: Config) { return rawConfig; } +interface SetupDeps { + core: InternalCoreSetup; + plugins: Record; +} + +interface StartDeps { + core: InternalCoreStart; + plugins: Record; +} + /** @internal */ export class LegacyService implements CoreService { private readonly log: Logger; + private readonly devConfig$: Observable; + private readonly httpConfig$: Observable; private kbnServer?: LegacyKbnServer; private configSubscription?: Subscription; - private setupDeps?: CoreSetup; + private setupDeps?: SetupDeps; constructor(private readonly coreContext: CoreContext) { this.log = coreContext.logger.get('legacy-service'); + this.devConfig$ = coreContext.configService + .atPath('dev') + .pipe(map(rawConfig => new DevConfig(rawConfig))); + this.httpConfig$ = coreContext.configService + .atPath('server') + .pipe(map(rawConfig => new HttpConfig(rawConfig, coreContext.env))); } - public async setup(setupDeps: CoreSetup) { + public async setup(setupDeps: SetupDeps) { this.setupDeps = setupDeps; } - public async start(startDeps: CoreStart) { + public async start(startDeps: StartDeps) { const { setupDeps } = this; if (!setupDeps) { throw new Error('Legacy service is not setup yet.'); @@ -111,10 +129,7 @@ export class LegacyService implements CoreService { private async createClusterManager(config: Config) { const basePathProxy$ = this.coreContext.env.cliArgs.basePath - ? combineLatest( - this.coreContext.configService.atPath('dev', DevConfig), - this.coreContext.configService.atPath('server', HttpConfig) - ).pipe( + ? combineLatest(this.devConfig$, this.httpConfig$).pipe( first(), map( ([devConfig, httpConfig]) => @@ -130,7 +145,7 @@ export class LegacyService implements CoreService { ); } - private async createKbnServer(config: Config, setupDeps: CoreSetup, startDeps: CoreStart) { + private async createKbnServer(config: Config, setupDeps: SetupDeps, startDeps: StartDeps) { // eslint-disable-next-line @typescript-eslint/no-var-requires const KbnServer = require('../../../legacy/server/kbn_server'); const kbnServer: LegacyKbnServer = new KbnServer(getLegacyRawConfig(config), { @@ -139,10 +154,10 @@ export class LegacyService implements CoreService { // bridge with the "legacy" Kibana. If server isn't run (e.g. if process is // managed by ClusterManager or optimizer) then we won't have that info, // so we can't start "legacy" server either. - serverOptions: startDeps.http.isListening() + serverOptions: startDeps.core.http.isListening() ? { - ...setupDeps.http.options, - listener: this.setupProxyListener(setupDeps.http.server), + ...setupDeps.core.http.options, + listener: this.setupProxyListener(setupDeps.core.http.server), } : { autoListen: false }, handledConfigPaths: await this.coreContext.configService.getUsedPaths(), @@ -158,10 +173,7 @@ export class LegacyService implements CoreService { require('../../../cli/repl').startRepl(kbnServer); } - const httpConfig = await this.coreContext.configService - .atPath('server', HttpConfig) - .pipe(first()) - .toPromise(); + const httpConfig = await this.httpConfig$.pipe(first()).toPromise(); if (httpConfig.autoListen) { try { diff --git a/src/core/server/logging/index.ts b/src/core/server/logging/index.ts index aa23a11e39b44..cde85c2600ffc 100644 --- a/src/core/server/logging/index.ts +++ b/src/core/server/logging/index.ts @@ -22,6 +22,6 @@ export { LoggerFactory } from './logger_factory'; export { LogRecord } from './log_record'; export { LogLevel } from './log_level'; /** @internal */ -export { LoggingConfig, config } from './logging_config'; +export { config, LoggingConfigType } from './logging_config'; /** @internal */ export { LoggingService } from './logging_service'; diff --git a/src/core/server/logging/logger.test.ts b/src/core/server/logging/logger.test.ts index 928b37a3f510e..40c310c4e94c7 100644 --- a/src/core/server/logging/logger.test.ts +++ b/src/core/server/logging/logger.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { LoggingConfig } from '.'; +import { LoggingConfig } from './logging_config'; import { Appender } from './appenders/appenders'; import { LogLevel } from './log_level'; import { BaseLogger } from './logger'; diff --git a/src/core/server/logging/logging_config.test.ts b/src/core/server/logging/logging_config.test.ts index f21b5aaf3c1a7..8eb79ac46e499 100644 --- a/src/core/server/logging/logging_config.test.ts +++ b/src/core/server/logging/logging_config.test.ts @@ -17,18 +17,15 @@ * under the License. */ -import { LoggingConfig } from '.'; +import { LoggingConfig, config } from './logging_config'; test('`schema` creates correct schema with defaults.', () => { - const loggingConfigSchema = LoggingConfig.schema; - expect(loggingConfigSchema.validate({})).toMatchSnapshot(); + expect(config.schema.validate({})).toMatchSnapshot(); }); test('`schema` throws if `root` logger does not have appenders configured.', () => { - const loggingConfigSchema = LoggingConfig.schema; - expect(() => - loggingConfigSchema.validate({ + config.schema.validate({ root: { appenders: [], }, @@ -50,21 +47,19 @@ test('`getLoggerContext()` returns correct joined context name.', () => { }); test('correctly fills in default `appenders` config.', () => { - const loggingConfigSchema = LoggingConfig.schema; - const config = new LoggingConfig(loggingConfigSchema.validate({})); + const configValue = new LoggingConfig(config.schema.validate({})); - expect(config.appenders.size).toBe(1); + expect(configValue.appenders.size).toBe(1); - expect(config.appenders.get('default')).toEqual({ + expect(configValue.appenders.get('default')).toEqual({ kind: 'console', layout: { kind: 'pattern', highlight: true }, }); }); test('correctly fills in custom `appenders` config.', () => { - const loggingConfigSchema = LoggingConfig.schema; - const config = new LoggingConfig( - loggingConfigSchema.validate({ + const configValue = new LoggingConfig( + config.schema.validate({ appenders: { console: { kind: 'console', @@ -79,19 +74,19 @@ test('correctly fills in custom `appenders` config.', () => { }) ); - expect(config.appenders.size).toBe(3); + expect(configValue.appenders.size).toBe(3); - expect(config.appenders.get('default')).toEqual({ + expect(configValue.appenders.get('default')).toEqual({ kind: 'console', layout: { kind: 'pattern', highlight: true }, }); - expect(config.appenders.get('console')).toEqual({ + expect(configValue.appenders.get('console')).toEqual({ kind: 'console', layout: { kind: 'pattern' }, }); - expect(config.appenders.get('file')).toEqual({ + expect(configValue.appenders.get('file')).toEqual({ kind: 'file', layout: { kind: 'pattern' }, path: 'path', @@ -99,11 +94,10 @@ test('correctly fills in custom `appenders` config.', () => { }); test('correctly fills in default `loggers` config.', () => { - const loggingConfigSchema = LoggingConfig.schema; - const config = new LoggingConfig(loggingConfigSchema.validate({})); + const configValue = new LoggingConfig(config.schema.validate({})); - expect(config.loggers.size).toBe(1); - expect(config.loggers.get('root')).toEqual({ + expect(configValue.loggers.size).toBe(1); + expect(configValue.loggers.get('root')).toEqual({ appenders: ['default'], context: 'root', level: 'info', @@ -111,9 +105,8 @@ test('correctly fills in default `loggers` config.', () => { }); test('correctly fills in custom `loggers` config.', () => { - const loggingConfigSchema = LoggingConfig.schema; - const config = new LoggingConfig( - loggingConfigSchema.validate({ + const configValue = new LoggingConfig( + config.schema.validate({ appenders: { file: { kind: 'file', @@ -140,23 +133,23 @@ test('correctly fills in custom `loggers` config.', () => { }) ); - expect(config.loggers.size).toBe(4); - expect(config.loggers.get('root')).toEqual({ + expect(configValue.loggers.size).toBe(4); + expect(configValue.loggers.get('root')).toEqual({ appenders: ['default'], context: 'root', level: 'info', }); - expect(config.loggers.get('plugins')).toEqual({ + expect(configValue.loggers.get('plugins')).toEqual({ appenders: ['file'], context: 'plugins', level: 'warn', }); - expect(config.loggers.get('plugins.pid')).toEqual({ + expect(configValue.loggers.get('plugins.pid')).toEqual({ appenders: ['file'], context: 'plugins.pid', level: 'trace', }); - expect(config.loggers.get('http')).toEqual({ + expect(configValue.loggers.get('http')).toEqual({ appenders: ['default'], context: 'http', level: 'error', @@ -164,8 +157,7 @@ test('correctly fills in custom `loggers` config.', () => { }); test('fails if loggers use unknown appenders.', () => { - const loggingConfigSchema = LoggingConfig.schema; - const validateConfig = loggingConfigSchema.validate({ + const validateConfig = config.schema.validate({ loggers: [ { appenders: ['unknown'], diff --git a/src/core/server/logging/logging_config.ts b/src/core/server/logging/logging_config.ts index de85bde3959df..84d707a3247e6 100644 --- a/src/core/server/logging/logging_config.ts +++ b/src/core/server/logging/logging_config.ts @@ -61,38 +61,34 @@ const createLoggerSchema = schema.object({ level: createLevelSchema, }); -const loggingSchema = schema.object({ - appenders: schema.mapOf(schema.string(), Appenders.configSchema, { - defaultValue: new Map(), - }), - loggers: schema.arrayOf(createLoggerSchema, { - defaultValue: [], - }), - root: schema.object({ - appenders: schema.arrayOf(schema.string(), { - defaultValue: [DEFAULT_APPENDER_NAME], - minSize: 1, - }), - level: createLevelSchema, - }), -}); - /** @internal */ export type LoggerConfigType = TypeOf; export const config = { path: 'logging', - schema: loggingSchema, + schema: schema.object({ + appenders: schema.mapOf(schema.string(), Appenders.configSchema, { + defaultValue: new Map(), + }), + loggers: schema.arrayOf(createLoggerSchema, { + defaultValue: [], + }), + root: schema.object({ + appenders: schema.arrayOf(schema.string(), { + defaultValue: [DEFAULT_APPENDER_NAME], + minSize: 1, + }), + level: createLevelSchema, + }), + }), }; -type LoggingConfigType = TypeOf; +export type LoggingConfigType = TypeOf; /** * Describes the config used to fully setup logging subsystem. * @internal */ export class LoggingConfig { - public static schema = loggingSchema; - /** * Helper method that joins separate string context parts into single context string. * In case joined context is an empty string, `root` context name is returned. diff --git a/src/core/server/logging/logging_service.test.ts b/src/core/server/logging/logging_service.test.ts index ee56b777b63b2..380488ff9f62d 100644 --- a/src/core/server/logging/logging_service.test.ts +++ b/src/core/server/logging/logging_service.test.ts @@ -29,7 +29,7 @@ let mockConsoleLog: jest.SpyInstance; import { createWriteStream } from 'fs'; const mockCreateWriteStream = (createWriteStream as unknown) as jest.Mock; -import { LoggingConfig, LoggingService } from '.'; +import { LoggingService, config } from '.'; let service: LoggingService; beforeEach(() => { @@ -68,12 +68,10 @@ test('flushes memory buffer logger and switches to real logger once config is pr // Switch to console appender with `info` level, so that `trace` message won't go through. service.upgrade( - new LoggingConfig( - LoggingConfig.schema.validate({ - appenders: { default: { kind: 'console', layout: { kind: 'json' } } }, - root: { level: 'info' }, - }) - ) + config.schema.validate({ + appenders: { default: { kind: 'console', layout: { kind: 'json' } } }, + root: { level: 'info' }, + }) ); expect(mockConsoleLog.mock.calls).toMatchSnapshot('buffered messages'); @@ -99,18 +97,16 @@ test('appends records via multiple appenders.', () => { expect(mockCreateWriteStream).not.toHaveBeenCalled(); service.upgrade( - new LoggingConfig( - LoggingConfig.schema.validate({ - appenders: { - default: { kind: 'console', layout: { kind: 'pattern' } }, - file: { kind: 'file', layout: { kind: 'pattern' }, path: 'path' }, - }, - loggers: [ - { appenders: ['file'], context: 'tests', level: 'warn' }, - { context: 'tests.child', level: 'error' }, - ], - }) - ) + config.schema.validate({ + appenders: { + default: { kind: 'console', layout: { kind: 'pattern' } }, + file: { kind: 'file', layout: { kind: 'pattern' }, path: 'path' }, + }, + loggers: [ + { appenders: ['file'], context: 'tests', level: 'warn' }, + { context: 'tests.child', level: 'error' }, + ], + }) ); // Now all logs should added to configured appenders. @@ -120,11 +116,9 @@ test('appends records via multiple appenders.', () => { test('uses `root` logger if context is not specified.', () => { service.upgrade( - new LoggingConfig( - LoggingConfig.schema.validate({ - appenders: { default: { kind: 'console', layout: { kind: 'pattern' } } }, - }) - ) + config.schema.validate({ + appenders: { default: { kind: 'console', layout: { kind: 'pattern' } } }, + }) ); const rootLogger = service.get(); @@ -135,12 +129,10 @@ test('uses `root` logger if context is not specified.', () => { test('`stop()` disposes all appenders.', async () => { service.upgrade( - new LoggingConfig( - LoggingConfig.schema.validate({ - appenders: { default: { kind: 'console', layout: { kind: 'json' } } }, - root: { level: 'info' }, - }) - ) + config.schema.validate({ + appenders: { default: { kind: 'console', layout: { kind: 'json' } } }, + root: { level: 'info' }, + }) ); const bufferDisposeSpy = jest.spyOn((service as any).bufferAppender, 'dispose'); @@ -156,12 +148,10 @@ test('asLoggerFactory() only allows to create new loggers.', () => { const logger = service.asLoggerFactory().get('test', 'context'); service.upgrade( - new LoggingConfig( - LoggingConfig.schema.validate({ - appenders: { default: { kind: 'console', layout: { kind: 'json' } } }, - root: { level: 'all' }, - }) - ) + config.schema.validate({ + appenders: { default: { kind: 'console', layout: { kind: 'json' } } }, + root: { level: 'all' }, + }) ); logger.trace('buffered trace message'); diff --git a/src/core/server/logging/logging_service.ts b/src/core/server/logging/logging_service.ts index 7934ed4afe76e..e340e769ac20e 100644 --- a/src/core/server/logging/logging_service.ts +++ b/src/core/server/logging/logging_service.ts @@ -22,7 +22,7 @@ import { LogLevel } from './log_level'; import { BaseLogger, Logger } from './logger'; import { LoggerAdapter } from './logger_adapter'; import { LoggerFactory } from './logger_factory'; -import { LoggerConfigType, LoggingConfig } from './logging_config'; +import { LoggingConfigType, LoggerConfigType, LoggingConfig } from './logging_config'; /** * Service that is responsible for maintaining loggers and logger appenders. @@ -56,7 +56,8 @@ export class LoggingService implements LoggerFactory { * Updates all current active loggers with the new config values. * @param config New config instance. */ - public upgrade(config: LoggingConfig) { + public upgrade(rawConfig: LoggingConfigType) { + const config = new LoggingConfig(rawConfig); // Config update is asynchronous and may require some time to complete, so we should invalidate // config so that new loggers will be using BufferAppender until newly configured appenders are ready. this.config = undefined; diff --git a/src/core/server/plugins/discovery/plugins_discovery.test.ts b/src/core/server/plugins/discovery/plugins_discovery.test.ts index 16fa4a18804d3..6e174e03cfcb1 100644 --- a/src/core/server/plugins/discovery/plugins_discovery.test.ts +++ b/src/core/server/plugins/discovery/plugins_discovery.test.ts @@ -26,7 +26,7 @@ import { Config, ConfigService, Env, ObjectToConfigAdapter } from '../../config' import { getEnvOptions } from '../../config/__mocks__/env'; import { loggingServiceMock } from '../../logging/logging_service.mock'; import { PluginWrapper } from '../plugin'; -import { PluginsConfig, config } from '../plugins_config'; +import { PluginsConfig, PluginsConfigType, config } from '../plugins_config'; import { discover } from './plugins_discovery'; const TEST_PLUGIN_SEARCH_PATHS = { @@ -123,11 +123,15 @@ test('properly iterates through plugin search locations', async () => { ); await configService.setSchema(config.path, config.schema); - const pluginsConfig = await configService - .atPath('plugins', PluginsConfig) + const rawConfig = await configService + .atPath('plugins') .pipe(first()) .toPromise(); - const { plugin$, error$ } = discover(pluginsConfig, { configService, env, logger }); + const { plugin$, error$ } = discover(new PluginsConfig(rawConfig, env), { + configService, + env, + logger, + }); const plugins = await plugin$.pipe(toArray()).toPromise(); expect(plugins).toHaveLength(4); diff --git a/src/core/server/plugins/index.ts b/src/core/server/plugins/index.ts index 9a1107b979fc5..c2e66cbb0bb7d 100644 --- a/src/core/server/plugins/index.ts +++ b/src/core/server/plugins/index.ts @@ -29,4 +29,4 @@ export { PluginInitializer, PluginName, } from './plugin'; -export { PluginInitializerContext, PluginSetupContext, PluginStartContext } from './plugin_context'; +export { PluginInitializerContext } from './plugin_context'; diff --git a/src/core/server/plugins/plugin.ts b/src/core/server/plugins/plugin.ts index d47ed2a742fd7..c703c6f1d9440 100644 --- a/src/core/server/plugins/plugin.ts +++ b/src/core/server/plugins/plugin.ts @@ -24,7 +24,8 @@ import { Type } from '@kbn/config-schema'; import { ConfigPath } from '../config'; import { Logger } from '../logging'; -import { PluginInitializerContext, PluginSetupContext, PluginStartContext } from './plugin_context'; +import { PluginInitializerContext } from './plugin_context'; +import { CoreSetup, CoreStart } from '..'; export type PluginConfigSchema = Type | null; @@ -140,8 +141,8 @@ export interface Plugin< TPluginsSetup extends Record = {}, TPluginsStart extends Record = {} > { - setup: (core: PluginSetupContext, plugins: TPluginsSetup) => TSetup | Promise; - start: (core: PluginStartContext, plugins: TPluginsStart) => TStart | Promise; + setup: (core: CoreSetup, plugins: TPluginsSetup) => TSetup | Promise; + start: (core: CoreStart, plugins: TPluginsStart) => TStart | Promise; stop?: () => void; } @@ -202,7 +203,7 @@ export class PluginWrapper< * @param plugins The dictionary where the key is the dependency name and the value * is the contract returned by the dependency's `setup` function. */ - public async setup(setupContext: PluginSetupContext, plugins: TPluginsSetup) { + public async setup(setupContext: CoreSetup, plugins: TPluginsSetup) { this.instance = this.createPluginInstance(); this.log.info('Setting up plugin'); @@ -217,7 +218,7 @@ export class PluginWrapper< * @param plugins The dictionary where the key is the dependency name and the value * is the contract returned by the dependency's `start` function. */ - public async start(startContext: PluginStartContext, plugins: TPluginsStart) { + public async start(startContext: CoreStart, plugins: TPluginsStart) { if (this.instance === undefined) { throw new Error(`Plugin "${this.name}" can't be started since it isn't set up.`); } diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index f216ae6d6fb5e..9dfc2df1c2d20 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -17,15 +17,13 @@ * under the License. */ -import { Type } from '@kbn/config-schema'; import { Observable } from 'rxjs'; -import { ConfigWithSchema, EnvironmentMode } from '../config'; +import { EnvironmentMode } from '../config'; import { CoreContext } from '../core_context'; -import { ClusterClient } from '../elasticsearch'; -import { HttpServiceSetup } from '../http'; import { LoggerFactory } from '../logging'; import { PluginWrapper, PluginManifest } from './plugin'; import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service'; +import { CoreSetup, CoreStart } from '..'; /** * Context that's available to plugins during initialization stage. @@ -36,40 +34,11 @@ export interface PluginInitializerContext { env: { mode: EnvironmentMode }; logger: LoggerFactory; config: { - create: , Config>( - ConfigClass: ConfigWithSchema - ) => Observable; - createIfExists: , Config>( - ConfigClass: ConfigWithSchema - ) => Observable; + create: () => Observable; + createIfExists: () => Observable; }; } -/** - * Context passed to the plugins `setup` method. - * - * @public - */ -export interface PluginSetupContext { - elasticsearch: { - adminClient$: Observable; - dataClient$: Observable; - }; - http: { - registerAuth: HttpServiceSetup['registerAuth']; - registerOnRequest: HttpServiceSetup['registerOnRequest']; - getBasePathFor: HttpServiceSetup['getBasePathFor']; - setBasePathFor: HttpServiceSetup['setBasePathFor']; - }; -} - -/** - * Context passed to the plugins `start` method. - * - * @public - */ -export interface PluginStartContext {} // eslint-disable-line @typescript-eslint/no-empty-interface - /** * This returns a facade for `CoreContext` that will be exposed to the plugin initializer. * This facade should be safe to use across entire plugin lifespan. @@ -113,11 +82,11 @@ export function createPluginInitializerContext( * @param ConfigClass A class (not an instance of a class) that contains a * static `schema` that we validate the config at the given `path` against. */ - create(ConfigClass) { - return coreContext.configService.atPath(pluginManifest.configPath, ConfigClass); + create() { + return coreContext.configService.atPath(pluginManifest.configPath); }, - createIfExists(ConfigClass) { - return coreContext.configService.optionalAtPath(pluginManifest.configPath, ConfigClass); + createIfExists() { + return coreContext.configService.optionalAtPath(pluginManifest.configPath); }, }, }; @@ -141,17 +110,19 @@ export function createPluginSetupContext( coreContext: CoreContext, deps: PluginsServiceSetupDeps, plugin: PluginWrapper -): PluginSetupContext { +): CoreSetup { return { elasticsearch: { adminClient$: deps.elasticsearch.adminClient$, dataClient$: deps.elasticsearch.dataClient$, }, http: { + registerOnPreAuth: deps.http.registerOnPreAuth, registerAuth: deps.http.registerAuth, - registerOnRequest: deps.http.registerOnRequest, + registerOnPostAuth: deps.http.registerOnPostAuth, getBasePathFor: deps.http.getBasePathFor, setBasePathFor: deps.http.setBasePathFor, + createNewServer: deps.http.createNewServer, }, }; } @@ -172,6 +143,6 @@ export function createPluginStartContext( coreContext: CoreContext, deps: PluginsServiceStartDeps, plugin: PluginWrapper -): PluginStartContext { +): CoreStart { return {}; } diff --git a/src/core/server/plugins/plugins_config.ts b/src/core/server/plugins/plugins_config.ts index d2c258faab308..3075c5a393f63 100644 --- a/src/core/server/plugins/plugins_config.ts +++ b/src/core/server/plugins/plugins_config.ts @@ -20,26 +20,23 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { Env } from '../config'; -const pluginsSchema = schema.object({ - initialize: schema.boolean({ defaultValue: true }), +export type PluginsConfigType = TypeOf; - /** - * Defines an array of directories where another plugin should be loaded from. - * Should only be used in a development environment. - */ - paths: schema.arrayOf(schema.string(), { defaultValue: [] }), -}); - -export type PluginsConfigType = TypeOf; export const config = { path: 'plugins', - schema: pluginsSchema, + schema: schema.object({ + initialize: schema.boolean({ defaultValue: true }), + + /** + * Defines an array of directories where another plugin should be loaded from. + * Should only be used in a development environment. + */ + paths: schema.arrayOf(schema.string(), { defaultValue: [] }), + }), }; /** @internal */ export class PluginsConfig { - public static schema = pluginsSchema; - /** * Indicates whether or not plugins should be initialized. */ diff --git a/src/core/server/plugins/plugins_service.mock.ts b/src/core/server/plugins/plugins_service.mock.ts index 59b6f7fbd1026..c9045ad04e1eb 100644 --- a/src/core/server/plugins/plugins_service.mock.ts +++ b/src/core/server/plugins/plugins_service.mock.ts @@ -26,6 +26,14 @@ const createServiceMock = () => { start: jest.fn(), stop: jest.fn(), }; + mocked.setup.mockResolvedValue({ + contracts: new Map(), + uiPlugins: { + public: new Map(), + internal: new Map(), + }, + }); + mocked.start.mockResolvedValue({ contracts: new Map() }); return mocked; }; diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index e26f0f5d22c10..95d3f26fff91e 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -18,7 +18,7 @@ */ import { Observable } from 'rxjs'; -import { filter, first, mergeMap, tap, toArray } from 'rxjs/operators'; +import { filter, first, map, mergeMap, tap, toArray } from 'rxjs/operators'; import { CoreService } from '../../types'; import { CoreContext } from '../core_context'; import { ElasticsearchServiceSetup } from '../elasticsearch/elasticsearch_service'; @@ -26,7 +26,7 @@ import { HttpServiceSetup } from '../http/http_service'; import { Logger } from '../logging'; import { discover, PluginDiscoveryError, PluginDiscoveryErrorType } from './discovery'; import { DiscoveredPlugin, DiscoveredPluginInternal, PluginWrapper, PluginName } from './plugin'; -import { PluginsConfig } from './plugins_config'; +import { PluginsConfig, PluginsConfigType } from './plugins_config'; import { PluginsSystem } from './plugins_system'; /** @public */ @@ -56,19 +56,20 @@ export interface PluginsServiceStartDeps {} // eslint-disable-line @typescript-e export class PluginsService implements CoreService { private readonly log: Logger; private readonly pluginsSystem: PluginsSystem; + private readonly config$: Observable; constructor(private readonly coreContext: CoreContext) { this.log = coreContext.logger.get('plugins-service'); this.pluginsSystem = new PluginsSystem(coreContext); + this.config$ = coreContext.configService + .atPath('plugins') + .pipe(map(rawConfig => new PluginsConfig(rawConfig, coreContext.env))); } public async setup(deps: PluginsServiceSetupDeps) { this.log.debug('Setting up plugins service'); - const config = await this.coreContext.configService - .atPath('plugins', PluginsConfig) - .pipe(first()) - .toPromise(); + const config = await this.config$.pipe(first()).toPromise(); const { error$, plugin$ } = discover(config, this.coreContext); await this.handleDiscoveryErrors(error$); diff --git a/src/core/server/root/index.ts b/src/core/server/root/index.ts index ff4c9da4bcc9a..ac6ef79483280 100644 --- a/src/core/server/root/index.ts +++ b/src/core/server/root/index.ts @@ -21,7 +21,7 @@ import { ConnectableObservable, Observable, Subscription } from 'rxjs'; import { first, map, publishReplay, switchMap, tap } from 'rxjs/operators'; import { Config, Env } from '../config'; -import { Logger, LoggerFactory, LoggingConfig, LoggingService } from '../logging'; +import { Logger, LoggerFactory, LoggingConfigType, LoggingService } from '../logging'; import { Server } from '../server'; /** @@ -98,7 +98,7 @@ export class Root { // Stream that maps config updates to logger updates, including update failures. const update$ = configService.getConfig$().pipe( // always read the logging config when the underlying config object is re-read - switchMap(() => configService.atPath('logging', LoggingConfig)), + switchMap(() => configService.atPath('logging')), map(config => this.loggingService.upgrade(config)), // This specifically console.logs because we were not able to configure the logger. // eslint-disable-next-line no-console diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index ddee8e3eb07e3..88ebe1e54bdce 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -4,6 +4,7 @@ ```ts +import { ByteSizeValue } from '@kbn/config-schema'; import { ConfigOptions } from 'elasticsearch'; import { Duration } from 'moment'; import { ObjectType } from '@kbn/config-schema'; @@ -21,15 +22,14 @@ import { Url } from 'url'; // @public (undocumented) export type APICaller = (endpoint: string, clientParams: Record, options?: CallAPIOptions) => Promise; -// Warning: (ae-forgotten-export) The symbol "SessionStorage" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "AuthResult" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export type AuthenticationHandler = (request: Request, sessionStorage: SessionStorage, t: AuthToolkit) => Promise; +export type AuthenticationHandler = (request: Readonly, t: AuthToolkit) => AuthResult | Promise; // @public export interface AuthToolkit { - authenticated: (credentials: any) => AuthResult; + authenticated: (state?: object) => AuthResult; redirected: (url: string) => AuthResult; rejected: (error: Error, options?: { statusCode?: number; @@ -61,8 +61,7 @@ export class ConfigService { // Warning: (ae-forgotten-export) The symbol "Config" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "Env" needs to be exported by the entry point index.d.ts constructor(config$: Observable, env: Env, logger: LoggerFactory); - // Warning: (ae-forgotten-export) The symbol "ConfigWithSchema" needs to be exported by the entry point index.d.ts - atPath, TConfig>(path: ConfigPath, ConfigClass: ConfigWithSchema): Observable; + atPath(path: ConfigPath): Observable; getConfig$(): Observable; // (undocumented) getUnusedPaths(): Promise; @@ -70,27 +69,31 @@ export class ConfigService { getUsedPaths(): Promise; // (undocumented) isEnabledAtPath(path: ConfigPath): Promise; - optionalAtPath, TConfig>(path: ConfigPath, ConfigClass: ConfigWithSchema): Observable; + optionalAtPath(path: ConfigPath): Observable; // Warning: (ae-forgotten-export) The symbol "ConfigPath" needs to be exported by the entry point index.d.ts setSchema(path: ConfigPath, schema: Type): Promise; } -// @public (undocumented) +// @public export interface CoreSetup { // (undocumented) - elasticsearch: ElasticsearchServiceSetup; - // (undocumented) - http: HttpServiceSetup; + elasticsearch: { + adminClient$: Observable; + dataClient$: Observable; + }; // (undocumented) - plugins: PluginsServiceSetup; + http: { + registerOnPreAuth: HttpServiceSetup['registerOnPreAuth']; + registerAuth: HttpServiceSetup['registerAuth']; + registerOnPostAuth: HttpServiceSetup['registerOnPostAuth']; + getBasePathFor: HttpServiceSetup['getBasePathFor']; + setBasePathFor: HttpServiceSetup['setBasePathFor']; + createNewServer: HttpServiceSetup['createNewServer']; + }; } -// @public (undocumented) +// @public export interface CoreStart { - // (undocumented) - http: HttpServiceStart; - // (undocumented) - plugins: PluginsServiceStart; } // @public @@ -131,20 +134,47 @@ export type Headers = Record; // Warning: (ae-forgotten-export) The symbol "HttpServerSetup" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export type HttpServiceSetup = HttpServerSetup; +export interface HttpServiceSetup extends HttpServerSetup { + // Warning: (ae-forgotten-export) The symbol "HttpConfig" needs to be exported by the entry point index.d.ts + // + // (undocumented) + createNewServer: (cfg: Partial) => Promise; +} // @public (undocumented) export interface HttpServiceStart { isListening: () => boolean; } +// @internal (undocumented) +export interface InternalCoreSetup { + // (undocumented) + elasticsearch: ElasticsearchServiceSetup; + // (undocumented) + http: HttpServiceSetup; + // (undocumented) + plugins: PluginsServiceSetup; +} + // @public (undocumented) +export interface InternalCoreStart { + // (undocumented) + http: HttpServiceStart; + // (undocumented) + plugins: PluginsServiceStart; +} + +// @public export class KibanaRequest { + // @internal (undocumented) + protected readonly [requestSymbol]: Request; constructor(request: Request, params: Params, query: Query, body: Body); // (undocumented) readonly body: Body; // Warning: (ae-forgotten-export) The symbol "RouteSchemas" needs to be exported by the entry point index.d.ts - static from

(req: Request, routeSchemas: RouteSchemas | undefined): KibanaRequest; + // + // @internal + static from

(req: Request, routeSchemas?: RouteSchemas): KibanaRequest; // (undocumented) getFilteredHeaders(headersToKeep: string[]): Pick, string>; // (undocumented) @@ -152,13 +182,23 @@ export class KibanaRequest { // (undocumented) readonly params: Params; // (undocumented) - readonly path: string; - // (undocumented) readonly query: Query; // (undocumented) - unstable_getIncomingMessage(): import("http").IncomingMessage; + readonly route: RecursiveReadonly; + // (undocumented) + readonly url: Url; } +// @public +export interface KibanaRequestRoute { + // (undocumented) + method: RouteMethod | 'patch' | 'options'; + // (undocumented) + options: Required; + // (undocumented) + path: string; +} + // @public export interface Logger { debug(message: string, meta?: LogMeta): void; @@ -228,27 +268,42 @@ export interface LogRecord { timestamp: Date; } -// Warning: (ae-forgotten-export) The symbol "OnRequestResult" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "OnPostAuthResult" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export type OnPostAuthHandler = (request: KibanaRequest, t: OnPostAuthToolkit) => OnPostAuthResult | Promise; + +// @public +export interface OnPostAuthToolkit { + next: () => OnPostAuthResult; + redirected: (url: string) => OnPostAuthResult; + rejected: (error: Error, options?: { + statusCode?: number; + }) => OnPostAuthResult; +} + +// Warning: (ae-forgotten-export) The symbol "OnPreAuthResult" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export type OnRequestHandler = (req: KibanaRequest, t: OnRequestToolkit) => OnRequestResult | Promise; +export type OnPreAuthHandler = (request: KibanaRequest, t: OnPreAuthToolkit) => OnPreAuthResult | Promise; // @public -export interface OnRequestToolkit { - next: () => OnRequestResult; - redirected: (url: string) => OnRequestResult; +export interface OnPreAuthToolkit { + next: () => OnPreAuthResult; + redirected: (url: string, options?: { + forward: boolean; + }) => OnPreAuthResult; rejected: (error: Error, options?: { statusCode?: number; - }) => OnRequestResult; - setUrl: (newUrl: string | Url) => void; + }) => OnPreAuthResult; } // @public export interface Plugin = {}, TPluginsStart extends Record = {}> { // (undocumented) - setup: (core: PluginSetupContext, plugins: TPluginsSetup) => TSetup | Promise; + setup: (core: CoreSetup, plugins: TPluginsSetup) => TSetup | Promise; // (undocumented) - start: (core: PluginStartContext, plugins: TPluginsStart) => TStart | Promise; + start: (core: CoreStart, plugins: TPluginsStart) => TStart | Promise; // (undocumented) stop?: () => void; } @@ -260,8 +315,8 @@ export type PluginInitializer, Config>(ConfigClass: ConfigWithSchema) => Observable; - createIfExists: , Config>(ConfigClass: ConfigWithSchema) => Observable; + create: () => Observable; + createIfExists: () => Observable; }; // (undocumented) env: { @@ -274,22 +329,6 @@ export interface PluginInitializerContext { // @public export type PluginName = string; -// @public -export interface PluginSetupContext { - // (undocumented) - elasticsearch: { - adminClient$: Observable; - dataClient$: Observable; - }; - // (undocumented) - http: { - registerAuth: HttpServiceSetup['registerAuth']; - registerOnRequest: HttpServiceSetup['registerOnRequest']; - getBasePathFor: HttpServiceSetup['getBasePathFor']; - setBasePathFor: HttpServiceSetup['setBasePathFor']; - }; -} - // @public (undocumented) export interface PluginsServiceSetup { // (undocumented) @@ -307,10 +346,22 @@ export interface PluginsServiceStart { contracts: Map; } +// Warning: (ae-forgotten-export) The symbol "RecursiveReadonlyArray" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export type RecursiveReadonly = T extends (...args: any[]) => any ? T : T extends any[] ? RecursiveReadonlyArray : T extends object ? Readonly<{ + [K in keyof T]: RecursiveReadonly; +}> : T; + // @public -export interface PluginStartContext { +export interface RouteConfigOptions { + authRequired?: boolean; + tags?: ReadonlyArray; } +// @public +export type RouteMethod = 'get' | 'post' | 'put' | 'delete'; + // @public (undocumented) export class Router { constructor(path: string); @@ -336,12 +387,23 @@ export class ScopedClusterClient { callAsInternalUser(endpoint: string, clientParams?: Record, options?: CallAPIOptions): Promise; } +// @public +export interface SessionStorage { + clear(): void; + get(): Promise; + set(sessionValue: T): void; +} + +// @public +export interface SessionStorageFactory { + // (undocumented) + asScoped: (request: Readonly | KibanaRequest) => SessionStorage; +} + // Warnings were encountered during analysis: // -// src/core/server/plugins/plugin_context.ts:36:10 - (ae-forgotten-export) The symbol "EnvironmentMode" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/plugin_context.ts:34:10 - (ae-forgotten-export) The symbol "EnvironmentMode" needs to be exported by the entry point index.d.ts // src/core/server/plugins/plugins_service.ts:37:5 - (ae-forgotten-export) The symbol "DiscoveredPluginInternal" needs to be exported by the entry point index.d.ts -// (No @packageDocumentation comment for this package) - ``` diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 9a416546d02e5..4f56e20f2c021 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -30,6 +30,7 @@ import { config as elasticsearchConfig } from './elasticsearch'; import { config as httpConfig } from './http'; import { config as loggingConfig } from './logging'; import { config as devConfig } from './dev'; +import { mapToObject } from '../utils/'; export class Server { public readonly configService: ConfigService; @@ -73,23 +74,29 @@ export class Server { plugins: pluginsSetup, }; - await this.legacy.setup(coreSetup); + await this.legacy.setup({ + core: coreSetup, + plugins: mapToObject(pluginsSetup.contracts), + }); return coreSetup; } public async start() { const httpStart = await this.http.start(); - const plugins = await this.plugins.start({}); + const pluginsStart = await this.plugins.start({}); - const startDeps = { + const coreStart = { http: httpStart, - plugins, + plugins: pluginsStart, }; - await this.legacy.start(startDeps); + await this.legacy.start({ + core: coreStart, + plugins: mapToObject(pluginsStart.contracts), + }); - return startDeps; + return coreStart; } public async stop() { diff --git a/src/core/public/utils/deep_freeze.test.ts b/src/core/utils/deep_freeze.test.ts similarity index 100% rename from src/core/public/utils/deep_freeze.test.ts rename to src/core/utils/deep_freeze.test.ts diff --git a/src/core/public/utils/deep_freeze.ts b/src/core/utils/deep_freeze.ts similarity index 93% rename from src/core/public/utils/deep_freeze.ts rename to src/core/utils/deep_freeze.ts index 86996fefce62c..8c3f8f2258b61 100644 --- a/src/core/public/utils/deep_freeze.ts +++ b/src/core/utils/deep_freeze.ts @@ -23,7 +23,10 @@ type Freezable = { [k: string]: any } | any[]; // eslint-disable-next-line @typescript-eslint/no-empty-interface interface RecursiveReadonlyArray extends Array> {} -export type RecursiveReadonly = T extends any[] +/** @public */ +export type RecursiveReadonly = T extends (...args: any[]) => any + ? T + : T extends any[] ? RecursiveReadonlyArray : T extends object ? Readonly<{ [K in keyof T]: RecursiveReadonly }> diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts index 484fc5b649f54..d4391f576df4a 100644 --- a/src/core/utils/index.ts +++ b/src/core/utils/index.ts @@ -18,6 +18,8 @@ */ export * from './get'; +export * from './map_to_object'; export * from './pick'; export * from './assert_never'; export * from './url'; +export * from './deep_freeze'; diff --git a/src/core/utils/map_to_object.ts b/src/core/utils/map_to_object.ts new file mode 100644 index 0000000000000..14767f566674f --- /dev/null +++ b/src/core/utils/map_to_object.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export function mapToObject(map: Map) { + const result: Record = Object.create(null); + for (const [key, value] of map) { + result[key] = value; + } + return result; +} diff --git a/src/dev/build/lib/fs.js b/src/dev/build/lib/fs.js index 2942ed527e912..3cf8bdd96cf54 100644 --- a/src/dev/build/lib/fs.js +++ b/src/dev/build/lib/fs.js @@ -188,8 +188,8 @@ export async function untar(source, destination, extractOptions = {}) { export async function compress(type, options = {}, source, destination) { const output = fs.createWriteStream(destination); - const archive = archiver(type, options); - const name = source.split(sep).slice(-1)[0]; + const archive = archiver(type, options.archiverOptions); + const name = (options.createRootDirectory ? source.split(sep).slice(-1)[0] : false); archive.pipe(output); diff --git a/src/dev/build/tasks/create_archives_task.js b/src/dev/build/tasks/create_archives_task.js index ea7d16d608115..cc4365dc1eb75 100644 --- a/src/dev/build/tasks/create_archives_task.js +++ b/src/dev/build/tasks/create_archives_task.js @@ -34,11 +34,36 @@ export const CreateArchivesTask = { switch (path.extname(destination)) { case '.zip': - await compress('zip', { zlib: { level: 9 } }, source, destination); + await compress( + 'zip', + { + archiverOptions: { + zlib: { + level: 9 + } + }, + createRootDirectory: true + }, + source, + destination + ); break; case '.gz': - await compress('tar', { gzip: true, gzipOptions: { level: 9 } }, source, destination); + await compress( + 'tar', + { + archiverOptions: { + gzip: true, + gzipOptions: { + level: 9 + } + }, + createRootDirectory: true + }, + source, + destination + ); break; default: diff --git a/src/dev/build/tasks/create_empty_dirs_and_files_task.js b/src/dev/build/tasks/create_empty_dirs_and_files_task.js index e9561a067613f..7badb1c498902 100644 --- a/src/dev/build/tasks/create_empty_dirs_and_files_task.js +++ b/src/dev/build/tasks/create_empty_dirs_and_files_task.js @@ -26,7 +26,7 @@ export const CreateEmptyDirsAndFilesTask = { await Promise.all([ mkdirp(build.resolvePath('plugins')), mkdirp(build.resolvePath('data')), - write(build.resolvePath('optimize/.babelcache.json'), '{}'), + write(build.resolvePath('optimize/.babel_register_cache.json'), '{}'), ]); }, }; diff --git a/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js b/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js index 1d3c71730a8cc..f88e2744873f9 100644 --- a/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js +++ b/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js @@ -71,6 +71,10 @@ export const CleanClientModulesOnDLLTask = { const dllEntries = await getDllEntries(dllManifestPath, whiteListedModules); for (const relativeEntryPath of dllEntries) { + if (relativeEntryPath.includes('@elastic/eui')) { + continue; + } + const entryPath = `${baseDir}/${relativeEntryPath}`; // Clean a module included into the dll diff --git a/src/dev/build/tasks/optimize_task.js b/src/dev/build/tasks/optimize_task.js index 1b71c5e1f5190..f6de0c717abfe 100644 --- a/src/dev/build/tasks/optimize_task.js +++ b/src/dev/build/tasks/optimize_task.js @@ -48,6 +48,7 @@ export const OptimizeBuildTask = { cwd: build.resolvePath('.'), env: { FORCE_DLL_CREATION: 'true', + KBN_CACHE_LOADER_WRITABLE: 'true', NODE_OPTIONS: '--max-old-space-size=2048' }, }); diff --git a/src/dev/build/tasks/os_packages/docker_generator/bundle_dockerfiles.js b/src/dev/build/tasks/os_packages/docker_generator/bundle_dockerfiles.js index f4f2745e9671e..6b01083946378 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/bundle_dockerfiles.js +++ b/src/dev/build/tasks/os_packages/docker_generator/bundle_dockerfiles.js @@ -59,10 +59,13 @@ export async function bundleDockerFiles(config, log, build, scope) { await compress( 'tar', { - gzip: true, - gzipOptions: { - level: 9 - } + archiverOptions: { + gzip: true, + gzipOptions: { + level: 9 + } + }, + createRootDirectory: false }, dockerFilesBuildDir, dockerFilesOutputDir diff --git a/src/dev/ci_setup/setup.sh b/src/dev/ci_setup/setup.sh index 860d1e9edb783..1d9ed703fe94d 100755 --- a/src/dev/ci_setup/setup.sh +++ b/src/dev/ci_setup/setup.sh @@ -181,6 +181,22 @@ if [ "$GIT_CHANGES" ]; then exit 1 fi +### +### rebuild kbn-pm distributable to ensure it's not out of date +### +echo " -- building renovate config" +node scripts/build_renovate_config + +### +### verify no git modifications +### +GIT_CHANGES="$(git ls-files --modified)" +if [ "$GIT_CHANGES" ]; then + echo -e "\n${RED}ERROR: 'node scripts/build_renovate_config' caused changes to the following files:${C_RESET}\n" + echo -e "$GIT_CHANGES\n" + exit 1 +fi + ### ### github-checks-reporter kill switch. Remove to disable ### diff --git a/src/dev/eslint/lint_files.js b/src/dev/eslint/lint_files.js index a10610c5d37a5..a003db51be6ae 100644 --- a/src/dev/eslint/lint_files.js +++ b/src/dev/eslint/lint_files.js @@ -30,15 +30,20 @@ import { REPO_ROOT } from '../constants'; * @param {Array} files * @return {undefined} */ -export function lintFiles(log, files) { +export function lintFiles(log, files, { fix } = {}) { const cli = new CLIEngine({ cache: true, cwd: REPO_ROOT, + fix }); const paths = files.map(file => file.getRelativePath()); const report = cli.executeOnFiles(paths); + if (fix) { + CLIEngine.outputFixes(report); + } + const failTypes = []; if (report.errorCount > 0) failTypes.push('errors'); if (report.warningCount > 0) failTypes.push('warning'); diff --git a/src/dev/failed_tests/__fixtures__/ftr_report.xml b/src/dev/failed_tests/__fixtures__/ftr_report.xml new file mode 100644 index 0000000000000..c006b0933bfa1 --- /dev/null +++ b/src/dev/failed_tests/__fixtures__/ftr_report.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/dev/failed_tests/__fixtures__/jest_report.xml b/src/dev/failed_tests/__fixtures__/jest_report.xml new file mode 100644 index 0000000000000..1ec4f9d5496ca --- /dev/null +++ b/src/dev/failed_tests/__fixtures__/jest_report.xml @@ -0,0 +1,15 @@ + + + + + + + + .test (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-intake/node/immutable/kibana/x-pack/plugins/code/server/lsp/abstract_launcher.test.ts:166:10)]]> + + + + + + diff --git a/src/dev/failed_tests/cli.js b/src/dev/failed_tests/cli.js index 7a0df52f8f4b1..b5d2dd78daa74 100644 --- a/src/dev/failed_tests/cli.js +++ b/src/dev/failed_tests/cli.js @@ -17,6 +17,23 @@ * under the License. */ +const { resolve } = require('path'); + +// force cwd +process.chdir(resolve(__dirname, '../../..')); + +if (!process.env.JOB_NAME) { + console.log('Unable to determine job name'); + process.exit(1); +} + +// JOB_NAME is formatted as `elastic+kibana+7.x` in some places and `elastic+kibana+7.x/JOB=kibana-intake,node=immutable` in others +const [org, proj, branch] = process.env.JOB_NAME.split(/\+|\//); +const masterOrVersion = branch === 'master' || branch.match(/^\d+\.(x|\d+)$/); +if (!(org === 'elastic' && proj === 'kibana' && masterOrVersion)) { + console.log('Failure issues only created on master/version branch jobs'); + process.exit(0); +} require('../../setup_node_env'); require('./report').reportFailedTests(); diff --git a/src/dev/failed_tests/report.js b/src/dev/failed_tests/report.js index aca70659a9d09..60c3b78c80e89 100644 --- a/src/dev/failed_tests/report.js +++ b/src/dev/failed_tests/report.js @@ -29,10 +29,28 @@ const GITHUB_OWNER = 'elastic'; const GITHUB_REPO = 'kibana'; const BUILD_URL = process.env.BUILD_URL; +const indent = text => ( + ` ${text.split('\n').map(l => ` ${l}`).join('\n')}` +); + +const isLikelyIrrelevant = ({ failure }) => { + if (failure.includes('NoSuchSessionError: This driver instance does not have a valid session ID')) { + return true; + } + + if (failure.includes('Error: No Living connections')) { + return true; + } + + if (failure.includes('Unable to fetch Kibana status API response from Kibana')) { + return true; + } +}; + /** * Parses junit XML files into JSON */ -const mapXml = createMapStream((file) => new Promise((resolve, reject) => { +export const mapXml = () => createMapStream((file) => new Promise((resolve, reject) => { xml2js.parseString(file.contents.toString(), (err, result) => { if (err) { return reject(err); @@ -44,26 +62,35 @@ const mapXml = createMapStream((file) => new Promise((resolve, reject) => { /** * Filters all testsuites to find failed testcases */ -const filterFailures = createMapStream((testSuite) => { +export const filterFailures = () => createMapStream((testSuite) => { // Grab the failures. Reporters may report multiple testsuites in a single file. const testFiles = testSuite.testsuites ? testSuite.testsuites.testsuite : [testSuite.testsuite]; - const failures = testFiles.reduce((failures, testFile) => { + const failures = []; + for (const testFile of testFiles) { for (const testCase of testFile.testcase) { - if (testCase.failure) { - // unwrap xml weirdness - failures.push({ - ...testCase.$, - // Strip ANSI color characters - failure: stripAnsi(testCase.failure[0]) - }); + if (!testCase.failure) { + continue; } - } - return failures; - }, []); + // unwrap xml weirdness + const failureCase = { + ...testCase.$, + // Strip ANSI color characters + failure: stripAnsi(testCase.failure[0]) + }; + + + if (isLikelyIrrelevant(failureCase)) { + console.log(`Ignoring likely irrelevant failure: ${failureCase.classname} - ${failureCase.name}\n${indent(failureCase.failure)}`); + continue; + } + + failures.push(failureCase); + } + } console.log(`Found ${failures.length} test failures`); @@ -145,8 +172,8 @@ export async function reportFailedTests() { vfs .src(['./target/junit/**/*.xml']) - .pipe(mapXml) - .pipe(filterFailures) + .pipe(mapXml()) + .pipe(filterFailures()) .pipe(updateGithubIssues(githubClient, issues)) .on('done', () => console.log(`Finished reporting test failures.`)); } diff --git a/src/dev/failed_tests/report.test.js b/src/dev/failed_tests/report.test.js new file mode 100644 index 0000000000000..f6cc8d4550720 --- /dev/null +++ b/src/dev/failed_tests/report.test.js @@ -0,0 +1,110 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* eslint-disable max-len */ + +import { resolve } from 'path'; + +import vfs from 'vinyl-fs'; + +import { mapXml, filterFailures } from './report'; +import { createPromiseFromStreams } from '../../legacy/utils/streams/promise_from_streams'; +import { createConcatStream } from '../../legacy/utils/streams/concat_stream'; + +console.log = jest.fn(); +afterEach(() => jest.resetAllMocks()); + +describe('irrelevant failure filtering', () => { + describe('jest report', () => { + it('allows relevant tests', async () => { + const failures = await createPromiseFromStreams([ + vfs.src([resolve(__dirname, '__fixtures__/jest_report.xml')]), + mapXml(), + filterFailures(), + createConcatStream(), + ]); + + expect(console.log.mock.calls).toMatchInlineSnapshot(` +Array [ + Array [ + "Found 1 test failures", + ], +] +`); + expect(failures).toMatchInlineSnapshot(` +Array [ + Object { + "classname": "X-Pack Jest Tests.x-pack/plugins/code/server/lsp", + "failure": " + TypeError: Cannot read property '0' of undefined + at Object..test (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-intake/node/immutable/kibana/x-pack/plugins/code/server/lsp/abstract_launcher.test.ts:166:10) + ", + "name": "launcher can reconnect if process died", + "time": "7.060", + }, +] +`); + }); + }); + + describe('ftr report', () => { + it('allows relevant tests', async () => { + const failures = await createPromiseFromStreams([ + vfs.src([resolve(__dirname, '__fixtures__/ftr_report.xml')]), + mapXml(), + filterFailures(), + createConcatStream(), + ]); + + expect(console.log.mock.calls).toMatchInlineSnapshot(` +Array [ + Array [ + "Ignoring likely irrelevant failure: Chrome X-Pack UI Functional Tests.x-pack/test/functional/apps/maps - maps app \\"after all\\" hook + + { NoSuchSessionError: This driver instance does not have a valid session ID (did you call WebDriver.quit()?) and may no longer be used. + at promise.finally (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-ciGroup7/node/immutable/kibana/node_modules/selenium-webdriver/lib/webdriver.js:726:38) + at Object.thenFinally [as finally] (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-ciGroup7/node/immutable/kibana/node_modules/selenium-webdriver/lib/promise.js:124:12) + at process._tickCallback (internal/process/next_tick.js:68:7) name: 'NoSuchSessionError', remoteStacktrace: '' } + ", + ], + Array [ + "Found 1 test failures", + ], +] +`); + expect(failures).toMatchInlineSnapshot(` +Array [ + Object { + "classname": "Chrome X-Pack UI Functional Tests.x-pack/test/functional/apps/maps/sample_data·js", + "failure": " + Error: retry.try timeout: TimeoutError: Waiting for element to be located By(css selector, [data-test-subj~=\\"layerTocActionsPanelToggleButtonRoad_Map_-_Bright\\"]) +Wait timed out after 10055ms + at /var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-ciGroup7/node/immutable/kibana/node_modules/selenium-webdriver/lib/webdriver.js:834:17 + at process._tickCallback (internal/process/next_tick.js:68:7) + at lastError (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-ciGroup7/node/immutable/kibana/test/common/services/retry/retry_for_success.ts:28:9) + at onFailure (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-ciGroup7/node/immutable/kibana/test/common/services/retry/retry_for_success.ts:68:13) + ", + "name": "maps app maps loaded from sample data ecommerce \\"before all\\" hook", + "time": "154.378", + }, +] +`); + }); + }); +}); diff --git a/src/dev/jest/babel_transform.js b/src/dev/jest/babel_transform.js index 0796cf859d4ef..c318433137312 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 2c71b354e5202..6d1ac1816b15a 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -22,6 +22,7 @@ import { RESERVED_DIR_JEST_INTEGRATION_TESTS } from '../constants'; export default { rootDir: '../../..', roots: [ + '/src/plugins', '/src/legacy/ui', '/src/core', '/src/legacy/core_plugins', @@ -44,7 +45,12 @@ export default { 'packages/kbn-ui-framework/src/services/**/*.js', '!packages/kbn-ui-framework/src/services/index.js', '!packages/kbn-ui-framework/src/services/**/*/index.js', - 'src/legacy/core_plugins/metrics/**/*.js' + 'src/legacy/core_plugins/**/*.js', + 'src/legacy/core_plugins/**/*.jsx', + 'src/legacy/core_plugins/**/*.ts', + 'src/legacy/core_plugins/**/*.tsx', + '!src/legacy/core_plugins/**/__test__/**/*', + '!src/legacy/core_plugins/**/__snapshots__/**/*', ], moduleNameMapper: { '^plugins/([^\/.]*)/(.*)': '/src/legacy/core_plugins/$1/public/$2', @@ -88,7 +94,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 deleted file mode 100644 index 6c72c918279d7..0000000000000 --- a/src/dev/license_checker/config.js +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -// The following list applies to packages both -// used as dependencies or dev dependencies -export const LICENSE_WHITELIST = [ - 'Elastic-License', - '(BSD-2-Clause OR MIT OR Apache-2.0)', - '(BSD-2-Clause OR MIT)', - '(GPL-2.0 OR MIT)', - '(MIT AND CC-BY-3.0)', - '(MIT AND Zlib)', - '(MIT OR Apache-2.0)', - '(MIT OR GPL-3.0)', - '(WTFPL OR MIT)', - 'AFLv2.1', - 'Apache 2.0', - 'Apache License, v2.0', - 'Apache License, Version 2.0', - 'Apache', - 'Apache*', - 'Apache, Version 2.0', - 'Apache-2.0', - 'BSD 3-Clause', - 'BSD New', - 'BSD', - 'BSD*', - 'BSD-2-Clause', - 'BSD-3-Clause AND MIT', - 'BSD-3-Clause OR MIT', - 'BSD-3-Clause', - '(BSD-3-Clause OR GPL-2.0)', - 'BSD-like', - 'CC0-1.0', - 'CC-BY', - 'CC-BY-3.0', - 'CC-BY-4.0', - 'Eclipse Distribution License - v 1.0', - 'FreeBSD', - 'ISC', - 'ISC*', - 'MIT OR GPL-2.0', - 'MIT', - 'MIT*', - 'MIT/X11', - 'new BSD, and MIT', - 'OFL-1.1 AND MIT', - 'Public Domain', - 'Unlicense', - 'WTFPL OR ISC', - 'WTFPL', -]; - -// The following list only applies to licenses that -// we wanna allow in packages only used as dev dependencies -export const DEV_ONLY_LICENSE_WHITELIST = [ - 'MPL-2.0' -]; - -// Globally overrides a license for a given package@version -export const LICENSE_OVERRIDES = { - 'cycle@1.0.3': ['CC0-1.0'], // conversion to a public-domain like license - 'jsts@1.1.2': ['Eclipse Distribution License - v 1.0'], //cf. https://github.com/bjornharrtell/jsts - '@mapbox/jsonlint-lines-primitives@2.0.2': ['MIT'], //license in readme https://github.com/tmcw/jsonlint - - // TODO can be removed once we upgrade past elasticsearch-browser@14.0.0 - 'elasticsearch-browser@13.0.1': ['Apache-2.0'], - - // TODO can be removed once we upgrade past colors.js@1.0.0 - 'colors@0.5.1': ['MIT'], - - // TODO can be removed once we upgrade past map-stream@0.5.0 - 'map-stream@0.1.0': ['MIT'], - - 'uglify-js@2.2.5': ['BSD'], - 'png-js@0.1.1': ['MIT'], - 'sha.js@2.4.11': ['BSD-3-Clause AND MIT'], - - // TODO can be removed if the ISSUE#239 is accepted on the source - 'xmldom@0.1.19': ['MIT'], - - // TODO can be removed if the PR#9 is accepted on the source - 'pause-stream@0.0.11': ['MIT'], - - // TODO can be removed once we upgrade past or equal pdf-image@2.0.1 - 'pdf-image@1.1.0': ['MIT'], - - // TODO can be removed once we upgrade the use of walk dependency past or equal to v2.3.14 - 'walk@2.3.9': ['MIT'], - - // TODO remove this once we upgrade past or equal to v1.0.2 - 'babel-plugin-mock-imports@1.0.1': ['MIT'] -}; diff --git a/src/dev/license_checker/config.ts b/src/dev/license_checker/config.ts new file mode 100644 index 0000000000000..ec4515ab78416 --- /dev/null +++ b/src/dev/license_checker/config.ts @@ -0,0 +1,108 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// The following list applies to packages both +// used as dependencies or dev dependencies +export const LICENSE_WHITELIST = [ + 'Elastic-License', + '(BSD-2-Clause OR MIT OR Apache-2.0)', + '(BSD-2-Clause OR MIT)', + '(GPL-2.0 OR MIT)', + '(MIT AND CC-BY-3.0)', + '(MIT AND Zlib)', + '(MIT OR Apache-2.0)', + '(MIT OR GPL-3.0)', + '(WTFPL OR MIT)', + 'AFLv2.1', + 'Apache 2.0', + 'Apache License, v2.0', + 'Apache License, Version 2.0', + 'Apache', + 'Apache*', + 'Apache, Version 2.0', + 'Apache-2.0', + 'BSD 3-Clause', + 'BSD New', + 'BSD', + 'BSD*', + 'BSD-2-Clause', + 'BSD-3-Clause AND MIT', + 'BSD-3-Clause OR MIT', + 'BSD-3-Clause', + '(BSD-3-Clause OR GPL-2.0)', + 'BSD-like', + 'CC0-1.0', + 'CC-BY', + 'CC-BY-3.0', + 'CC-BY-4.0', + 'Eclipse Distribution License - v 1.0', + 'FreeBSD', + 'ISC', + 'ISC*', + 'MIT OR GPL-2.0', + '(MIT OR CC0-1.0)', + 'MIT', + 'MIT*', + 'MIT/X11', + 'new BSD, and MIT', + '(OFL-1.1 AND MIT)', + 'Public Domain', + 'Unlicense', + 'WTFPL OR ISC', + 'WTFPL', +]; + +// The following list only applies to licenses that +// we wanna allow in packages only used as dev dependencies +export const DEV_ONLY_LICENSE_WHITELIST = ['MPL-2.0']; + +// Globally overrides a license for a given package@version +export const LICENSE_OVERRIDES = { + 'cycle@1.0.3': ['CC0-1.0'], // conversion to a public-domain like license + 'jsts@1.1.2': ['Eclipse Distribution License - v 1.0'], // cf. https://github.com/bjornharrtell/jsts + '@mapbox/jsonlint-lines-primitives@2.0.2': ['MIT'], // license in readme https://github.com/tmcw/jsonlint + + // TODO can be removed once we upgrade past elasticsearch-browser@14.0.0 + 'elasticsearch-browser@13.0.1': ['Apache-2.0'], + + // TODO can be removed once we upgrade past colors.js@1.0.0 + 'colors@0.5.1': ['MIT'], + + // TODO can be removed once we upgrade past map-stream@0.5.0 + 'map-stream@0.1.0': ['MIT'], + + 'uglify-js@2.2.5': ['BSD'], + 'png-js@0.1.1': ['MIT'], + 'sha.js@2.4.11': ['BSD-3-Clause AND MIT'], + + // TODO can be removed if the ISSUE#239 is accepted on the source + 'xmldom@0.1.19': ['MIT'], + + // TODO can be removed if the PR#9 is accepted on the source + 'pause-stream@0.0.11': ['MIT'], + + // TODO can be removed once we upgrade past or equal pdf-image@2.0.1 + 'pdf-image@1.1.0': ['MIT'], + + // TODO can be removed once we upgrade the use of walk dependency past or equal to v2.3.14 + 'walk@2.3.9': ['MIT'], + + // TODO remove this once we upgrade past or equal to v1.0.2 + 'babel-plugin-mock-imports@1.0.1': ['MIT'], +}; diff --git a/src/dev/license_checker/index.js b/src/dev/license_checker/index.ts similarity index 100% rename from src/dev/license_checker/index.js rename to src/dev/license_checker/index.ts diff --git a/src/dev/license_checker/run_check_licenses_cli.ts b/src/dev/license_checker/run_check_licenses_cli.ts new file mode 100644 index 0000000000000..47b1d0e6c351d --- /dev/null +++ b/src/dev/license_checker/run_check_licenses_cli.ts @@ -0,0 +1,61 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getInstalledPackages } from '../npm'; +import { run } from '../run'; + +import { LICENSE_WHITELIST, DEV_ONLY_LICENSE_WHITELIST, LICENSE_OVERRIDES } from './config'; +import { assertLicensesValid } from './valid'; +import { REPO_ROOT } from '../constants'; + +run( + async ({ log, flags }) => { + const packages = await getInstalledPackages({ + directory: REPO_ROOT, + licenseOverrides: LICENSE_OVERRIDES, + includeDev: !!flags.dev, + }); + + // Assert if the found licenses in the production + // packages are valid + assertLicensesValid({ + packages: packages.filter(pkg => !pkg.isDevOnly), + validLicenses: LICENSE_WHITELIST, + }); + log.success('All production dependency licenses are allowed'); + + // Do the same as above for the packages only used in development + // if the dev flag is found + if (flags.dev) { + assertLicensesValid({ + packages: packages.filter(pkg => pkg.isDevOnly), + validLicenses: LICENSE_WHITELIST.concat(DEV_ONLY_LICENSE_WHITELIST), + }); + log.success('All development dependency licenses are allowed'); + } + }, + { + flags: { + boolean: ['dev'], + help: ` + --dev Also check dev dependencies + `, + }, + } +); diff --git a/src/dev/license_checker/valid.js b/src/dev/license_checker/valid.js deleted file mode 100644 index d11f2d46384d1..0000000000000 --- a/src/dev/license_checker/valid.js +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -const describeInvalidLicenses = getInvalid => pkg => ( - ` - ${pkg.name} - version: ${pkg.version} - all licenses: ${pkg.licenses} - invalid licenses: ${getInvalid(pkg.licenses).join(', ')} - path: ${pkg.relative} -` -); - -/** - * When given a list of packages and the valid license - * options, either throws an error with details about - * violations or returns undefined. - * - * @param {Object} [options={}] - * @property {Array} options.packages List of packages to check, see - * getInstalledPackages() in ../packages - * @property {Array} options.validLicenses - * @return {undefined} - */ -export function assertLicensesValid(options = {}) { - const { - packages, - validLicenses - } = options; - - if (!packages || !validLicenses) { - throw new Error('packages and validLicenses options are required'); - } - - const getInvalid = licenses => ( - licenses.filter(license => !validLicenses.includes(license)) - ); - - const isPackageInvalid = pkg => ( - !pkg.licenses.length || getInvalid(pkg.licenses).length > 0 - ); - - const invalidMsgs = packages - .filter(isPackageInvalid) - .map(describeInvalidLicenses(getInvalid)); - - if (invalidMsgs.length) { - throw new Error(`Non-conforming licenses: ${invalidMsgs.join('')}`); - } -} diff --git a/src/dev/license_checker/valid.ts b/src/dev/license_checker/valid.ts new file mode 100644 index 0000000000000..8c6d0453d13c0 --- /dev/null +++ b/src/dev/license_checker/valid.ts @@ -0,0 +1,67 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import dedent from 'dedent'; +import { createFailError } from '../run'; + +interface Options { + packages: Array<{ + name: string; + version: string; + relative: string; + licenses: string[]; + }>; + validLicenses: string[]; +} + +/** + * When given a list of packages and the valid license + * options, either throws an error with details about + * violations or returns undefined. + */ +export function assertLicensesValid({ packages, validLicenses }: Options) { + const invalidMsgs = packages.reduce( + (acc, pkg) => { + const invalidLicenses = pkg.licenses.filter(license => !validLicenses.includes(license)); + + if (pkg.licenses.length && !invalidLicenses.length) { + return acc; + } + + return acc.concat(dedent` + ${pkg.name} + version: ${pkg.version} + all licenses: ${pkg.licenses} + invalid licenses: ${invalidLicenses.join(', ')} + path: ${pkg.relative} + `); + }, + [] as string[] + ); + + if (invalidMsgs.length) { + throw createFailError( + `Non-conforming licenses:\n${invalidMsgs + .join('\n') + .split('\n') + .map(l => ` ${l}`) + .join('\n')}` + ); + } +} diff --git a/src/dev/npm/__tests__/installed_packages.js b/src/dev/npm/__tests__/installed_packages.js deleted file mode 100644 index 24bad227cbbde..0000000000000 --- a/src/dev/npm/__tests__/installed_packages.js +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { resolve, sep } from 'path'; - -import { uniq } from 'lodash'; -import expect from '@kbn/expect'; - -import { getInstalledPackages } from '../installed_packages'; - -const KIBANA_ROOT = resolve(__dirname, '../../../../'); -const FIXTURE1_ROOT = resolve(__dirname, 'fixtures/fixture1'); - -describe('src/dev/npm/installed_packages', () => { - describe('getInstalledPackages()', function () { - - let kibanaPackages; - let fixture1Packages; - before(async function () { - this.timeout(30 * 1000); - [kibanaPackages, fixture1Packages] = await Promise.all([ - getInstalledPackages({ - directory: KIBANA_ROOT - }), - getInstalledPackages({ - directory: FIXTURE1_ROOT, - dev: true - }), - ]); - }); - - it('requires a directory', async () => { - try { - await getInstalledPackages({}); - throw new Error('expected getInstalledPackages() to reject'); - } catch (err) { - expect(err.message).to.contain('directory'); - } - }); - - it('reads all installed packages of a module', () => { - expect(fixture1Packages).to.eql([ - { - name: 'dep1', - version: '0.0.2', - licenses: [ 'Apache-2.0' ], - repository: 'https://github.com/mycorp/dep1', - directory: resolve(FIXTURE1_ROOT, 'node_modules/dep1'), - relative: ['node_modules', 'dep1'].join(sep), - isDevOnly: false, - }, - { - name: 'privatedep', - version: '0.0.2', - repository: 'https://github.com/mycorp/privatedep', - licenses: [ 'Apache-2.0' ], - directory: resolve(FIXTURE1_ROOT, 'node_modules/privatedep'), - relative: ['node_modules', 'privatedep'].join(sep), - isDevOnly: false, - }, - { - name: 'dep2', - version: '0.0.2', - licenses: [ 'Apache-2.0' ], - repository: 'https://github.com/mycorp/dep2', - directory: resolve(FIXTURE1_ROOT, 'node_modules/dep2'), - relative: ['node_modules', 'dep2'].join(sep), - isDevOnly: true, - } - ]); - }); - - it('returns a single entry for every package/version combo', () => { - const tags = kibanaPackages.map(pkg => `${pkg.name}@${pkg.version}`); - expect(tags).to.eql(uniq(tags)); - }); - - it('does not include root package in the list', async () => { - expect(kibanaPackages.find(pkg => pkg.name === 'kibana')).to.be(undefined); - expect(fixture1Packages.find(pkg => pkg.name === 'fixture1')).to.be(undefined); - }); - }); -}); diff --git a/src/dev/npm/index.js b/src/dev/npm/index.ts similarity index 100% rename from src/dev/npm/index.js rename to src/dev/npm/index.ts diff --git a/src/dev/npm/installed_packages.js b/src/dev/npm/installed_packages.js deleted file mode 100644 index 862fcda4be9f0..0000000000000 --- a/src/dev/npm/installed_packages.js +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { relative, resolve } from 'path'; -import { readFileSync } from 'fs'; - -import { callLicenseChecker } from './license_checker'; - -function resolveLicense(licenseInfo, key, licenseOverrides) { - const { - private: isPrivate, - licenses: detectedLicenses, - realPath, - } = licenseInfo[key]; - - // `license-checker` marks all packages that have `private: true` - // in their `package.json` as "UNLICENSED", so we try to lookup the - // actual license by reading the license field from their package.json - if (isPrivate && detectedLicenses === 'UNLICENSED') { - try { - const pkg = JSON.parse(readFileSync(resolve(realPath, 'package.json'))); - if (!pkg.license) { - throw new Error('no license field'); - } - return [pkg.license]; - } catch (error) { - throw new Error(`Unable to detect license for \`"private": true\` package at ${realPath}: ${error.message}`); - } - } - - return [].concat( - licenseOverrides[key] - ? licenseOverrides[key] - : detectedLicenses - ); -} - -/** - * Get a list of objects with details about each installed - * NPM package. - * - * @param {Object} [options={}] - * @property {String} options.directory root of the project to read - * @property {Boolean} [options.dev=false] should development dependencies be included? - * @property {Object} [options.licenseOverrides] map of `${name}@${version}` to a list of - * license ids to override the automatically - * detected ones - * @return {Array} - */ -export async function getInstalledPackages(options = {}) { - const { - directory, - dev = false, - licenseOverrides = {} - } = options; - - if (!directory) { - throw new Error('You must specify a directory to read installed packages from'); - } - - const licenseInfo = await callLicenseChecker({ directory, dev }); - return Object - .keys(licenseInfo) - .map(key => { - const { realPath, repository, isDevOnly } = licenseInfo[key]; - if (realPath === directory) return; - - const keyParts = key.split('@'); - const name = keyParts.slice(0, -1).join('@'); - const version = keyParts[keyParts.length - 1]; - - const licenses = resolveLicense(licenseInfo, key, licenseOverrides); - - return { - name, - version, - repository, - licenses, - directory: realPath, - relative: relative(directory, realPath), - isDevOnly - }; - }) - .filter(Boolean); -} diff --git a/src/dev/npm/installed_packages.ts b/src/dev/npm/installed_packages.ts new file mode 100644 index 0000000000000..eb024562e8501 --- /dev/null +++ b/src/dev/npm/installed_packages.ts @@ -0,0 +1,124 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { relative, resolve } from 'path'; +import { readFileSync } from 'fs'; +import { promisify } from 'util'; + +import licenseChecker from 'license-checker'; + +export type InstalledPackage = NonNullable>; +interface Options { + directory: string; + includeDev?: boolean; + licenseOverrides?: { [pgkNameAndVersion: string]: string[] }; +} + +const toArray = (input: T | T[]) => ([] as T[]).concat(input); + +function resolveLicenses( + isPrivate: boolean, + realPath: string, + licenses: string[] | string | undefined +) { + // `license-checker` marks all packages that have `private: true` + // in their `package.json` as "UNLICENSED", so we try to lookup the + // actual license by reading the license field from their package.json + if (isPrivate && licenses === 'UNLICENSED') { + try { + const pkg = JSON.parse(readFileSync(resolve(realPath, 'package.json'), 'utf8')); + if (!pkg.license) { + throw new Error('no license field'); + } + return [pkg.license as string]; + } catch (error) { + throw new Error( + `Unable to detect license for \`"private": true\` package at ${realPath}: ${error.message}` + ); + } + } + + return toArray(licenses || []); +} + +function readModuleInfo( + pkgAndVersion: string, + moduleInfo: licenseChecker.ModuleInfo, + dev: boolean, + options: Options +) { + const directory = (moduleInfo as any).realPath as string; + if (directory === options.directory) { + return; + } + + const isPrivate = !!(moduleInfo as any).private as boolean; + const keyParts = pkgAndVersion.split('@'); + const name = keyParts.slice(0, -1).join('@'); + const version = keyParts[keyParts.length - 1]; + const override = options.licenseOverrides && options.licenseOverrides[pkgAndVersion]; + + return { + name, + version, + isDevOnly: dev, + repository: moduleInfo.repository, + directory, + relative: relative(options.directory, directory), + licenses: toArray( + override ? override : resolveLicenses(isPrivate, directory, moduleInfo.licenses) + ), + }; +} + +async function _getInstalledPackages(dev: boolean, options: Options) { + const lcResult = await promisify(licenseChecker.init)({ + start: options.directory, + development: dev, + production: !dev, + json: true, + customFormat: { + realPath: true, + licenseText: false, + licenseFile: false, + }, + } as any); + + const result = []; + + for (const [pkgAndVersion, moduleInfo] of Object.entries(lcResult)) { + const installedPackage = readModuleInfo(pkgAndVersion, moduleInfo, dev, options); + if (installedPackage) { + result.push(installedPackage); + } + } + + return result; +} + +/** + * Get a list of objects with details about each installed + * NPM package. + */ +export async function getInstalledPackages(options: Options) { + return [ + ...(await _getInstalledPackages(false, options)), + ...(options.includeDev ? await _getInstalledPackages(true, options) : []), + ]; +} diff --git a/src/dev/npm/__tests__/fixtures/fixture1/index.js b/src/dev/npm/integration_tests/__fixtures__/fixture1/index.js similarity index 100% rename from src/dev/npm/__tests__/fixtures/fixture1/index.js rename to src/dev/npm/integration_tests/__fixtures__/fixture1/index.js diff --git a/src/dev/npm/__tests__/fixtures/fixture1/node_modules/dep1/index.js b/src/dev/npm/integration_tests/__fixtures__/fixture1/node_modules/dep1/index.js similarity index 100% rename from src/dev/npm/__tests__/fixtures/fixture1/node_modules/dep1/index.js rename to src/dev/npm/integration_tests/__fixtures__/fixture1/node_modules/dep1/index.js diff --git a/src/dev/npm/__tests__/fixtures/fixture1/node_modules/dep1/package.json b/src/dev/npm/integration_tests/__fixtures__/fixture1/node_modules/dep1/package.json similarity index 100% rename from src/dev/npm/__tests__/fixtures/fixture1/node_modules/dep1/package.json rename to src/dev/npm/integration_tests/__fixtures__/fixture1/node_modules/dep1/package.json diff --git a/src/dev/npm/__tests__/fixtures/fixture1/node_modules/dep2/index.js b/src/dev/npm/integration_tests/__fixtures__/fixture1/node_modules/dep2/index.js similarity index 100% rename from src/dev/npm/__tests__/fixtures/fixture1/node_modules/dep2/index.js rename to src/dev/npm/integration_tests/__fixtures__/fixture1/node_modules/dep2/index.js diff --git a/src/dev/npm/__tests__/fixtures/fixture1/node_modules/dep2/package.json b/src/dev/npm/integration_tests/__fixtures__/fixture1/node_modules/dep2/package.json similarity index 100% rename from src/dev/npm/__tests__/fixtures/fixture1/node_modules/dep2/package.json rename to src/dev/npm/integration_tests/__fixtures__/fixture1/node_modules/dep2/package.json diff --git a/src/dev/npm/__tests__/fixtures/fixture1/node_modules/privatedep/index.js b/src/dev/npm/integration_tests/__fixtures__/fixture1/node_modules/privatedep/index.js similarity index 100% rename from src/dev/npm/__tests__/fixtures/fixture1/node_modules/privatedep/index.js rename to src/dev/npm/integration_tests/__fixtures__/fixture1/node_modules/privatedep/index.js diff --git a/src/dev/npm/__tests__/fixtures/fixture1/node_modules/privatedep/package.json b/src/dev/npm/integration_tests/__fixtures__/fixture1/node_modules/privatedep/package.json similarity index 100% rename from src/dev/npm/__tests__/fixtures/fixture1/node_modules/privatedep/package.json rename to src/dev/npm/integration_tests/__fixtures__/fixture1/node_modules/privatedep/package.json diff --git a/src/dev/npm/__tests__/fixtures/fixture1/package.json b/src/dev/npm/integration_tests/__fixtures__/fixture1/package.json similarity index 100% rename from src/dev/npm/__tests__/fixtures/fixture1/package.json rename to src/dev/npm/integration_tests/__fixtures__/fixture1/package.json diff --git a/src/dev/npm/integration_tests/installed_packages.test.ts b/src/dev/npm/integration_tests/installed_packages.test.ts new file mode 100644 index 0000000000000..35bfe015f4e7f --- /dev/null +++ b/src/dev/npm/integration_tests/installed_packages.test.ts @@ -0,0 +1,93 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { resolve, sep } from 'path'; + +import { uniq } from 'lodash'; + +import { getInstalledPackages, InstalledPackage } from '../installed_packages'; +import { REPO_ROOT } from '../../constants'; + +const FIXTURE1_ROOT = resolve(__dirname, '__fixtures__/fixture1'); + +describe('src/dev/npm/installed_packages', () => { + describe('getInstalledPackages()', function() { + let kibanaPackages: InstalledPackage[]; + let fixture1Packages: InstalledPackage[]; + + beforeAll(async function() { + [kibanaPackages, fixture1Packages] = await Promise.all([ + getInstalledPackages({ + directory: REPO_ROOT, + }), + getInstalledPackages({ + directory: FIXTURE1_ROOT, + includeDev: true, + }), + ]); + }, 30 * 1000); + + it('reads all installed packages of a module', () => { + expect(fixture1Packages).toEqual([ + { + name: 'dep1', + version: '0.0.2', + licenses: ['Apache-2.0'], + repository: 'https://github.com/mycorp/dep1', + directory: resolve(FIXTURE1_ROOT, 'node_modules/dep1'), + relative: ['node_modules', 'dep1'].join(sep), + isDevOnly: false, + }, + { + name: 'privatedep', + version: '0.0.2', + repository: 'https://github.com/mycorp/privatedep', + licenses: ['Apache-2.0'], + directory: resolve(FIXTURE1_ROOT, 'node_modules/privatedep'), + relative: ['node_modules', 'privatedep'].join(sep), + isDevOnly: false, + }, + { + name: 'dep2', + version: '0.0.2', + licenses: ['Apache-2.0'], + repository: 'https://github.com/mycorp/dep2', + directory: resolve(FIXTURE1_ROOT, 'node_modules/dep2'), + relative: ['node_modules', 'dep2'].join(sep), + isDevOnly: true, + }, + ]); + }); + + it('returns a single entry for every package/version combo', () => { + const tags = kibanaPackages.map(pkg => `${pkg.name}@${pkg.version}`); + expect(tags).toEqual(uniq(tags)); + }); + + it('does not include root package in the list', async () => { + if (kibanaPackages.find(pkg => pkg.name === 'kibana')) { + throw new Error('Expected getInstalledPackages(kibana) to not include kibana pkg'); + } + + if (fixture1Packages.find(pkg => pkg.name === 'fixture1')) { + throw new Error('Expected getInstalledPackages(fixture1) to not include fixture1 pkg'); + } + }); + }); +}); diff --git a/src/dev/npm/license_checker.js b/src/dev/npm/license_checker.js deleted file mode 100644 index 697ff633cdd8a..0000000000000 --- a/src/dev/npm/license_checker.js +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import licenseChecker from 'license-checker'; - -async function runLicenseChecker(directory, dev) { - return new Promise((resolve, reject) => { - licenseChecker.init({ - start: directory, - development: dev, - production: !dev, - json: true, - customFormat: { - realPath: true, - licenseText: false, - licenseFile: false - } - }, (err, licenseInfo) => { - if (err) reject(err); - else { - resolve( - // Extend original licenseInfo object with a new attribute - // stating whether a license was found in a package used - // only as a dev dependency or not - Object.keys(licenseInfo).reduce(function (result, key) { - result[key] = Object.assign(licenseInfo[key], { isDevOnly: dev }); - return result; - }, {}) - ); - } - }); - }); -} - -export async function callLicenseChecker(options = {}) { - const { - directory, - dev = false - } = options; - - if (!directory) { - throw new Error('You must specify the directory where license checker should start'); - } - - return new Promise(async (resolve, reject) => { - try { - // Run license checker for prod only packages - const prodOnlyLicenses = await runLicenseChecker(directory, false); - - if (!dev) { - resolve(prodOnlyLicenses); - return; - } - - // In case we have the dev option - // also run the license checker for the - // dev only packages and build a final object - // merging the previous results too - const devOnlyLicenses = await runLicenseChecker(directory, true); - resolve(Object.assign(prodOnlyLicenses, devOnlyLicenses)); - } catch (e) { - reject(e); - } - }); -} diff --git a/src/dev/prs/github_api.ts b/src/dev/prs/github_api.ts new file mode 100644 index 0000000000000..c0a4a03897775 --- /dev/null +++ b/src/dev/prs/github_api.ts @@ -0,0 +1,81 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import axios, { AxiosError, AxiosResponse } from 'axios'; + +import { createFailError } from '../run'; + +interface ResponseError extends AxiosError { + request: any; + response: AxiosResponse; +} +const isResponseError = (error: any): error is ResponseError => + error && error.response && error.response.status; + +const isRateLimitError = (error: any) => + isResponseError(error) && + error.response.status === 403 && + `${error.response.headers['X-RateLimit-Remaining']}` === '0'; + +export class GithubApi { + private api = axios.create({ + baseURL: 'https://api.github.com/', + headers: { + Accept: 'application/vnd.github.v3+json', + 'User-Agent': 'kibana/update_prs_cli', + ...(this.accessToken ? { Authorization: `token ${this.accessToken} ` } : {}), + }, + }); + + constructor(private accessToken?: string) {} + + async getPrInfo(prNumber: number) { + try { + const resp = await this.api.get(`repos/elastic/kibana/pulls/${prNumber}`); + const targetRef: string = resp.data.base && resp.data.base.ref; + if (!targetRef) { + throw new Error('unable to read base ref from pr info'); + } + + const owner: string = resp.data.head && resp.data.head.user && resp.data.head.user.login; + if (!owner) { + throw new Error('unable to read owner info from pr info'); + } + + const sourceBranch: string = resp.data.head.ref; + if (!sourceBranch) { + throw new Error('unable to read source branch name from pr info'); + } + + return { + targetRef, + owner, + sourceBranch, + }; + } catch (error) { + if (!isRateLimitError(error)) { + throw error; + } + + throw createFailError( + 'github rate limit exceeded, please specify the `--access-token` command line flag and try again' + ); + } + } +} diff --git a/src/dev/prs/helpers.ts b/src/dev/prs/helpers.ts new file mode 100644 index 0000000000000..d25db1a79a1b9 --- /dev/null +++ b/src/dev/prs/helpers.ts @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Readable } from 'stream'; +import * as Rx from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +/** + * Convert a Readable stream to an observable of lines + */ +export const getLine$ = (stream: Readable) => { + return new Rx.Observable(subscriber => { + let buffer = ''; + return Rx.fromEvent(stream, 'data') + .pipe(takeUntil(Rx.fromEvent(stream, 'close'))) + .subscribe({ + next(chunk) { + buffer += chunk; + while (true) { + const i = buffer.indexOf('\n'); + if (i === -1) { + break; + } + + subscriber.next(buffer.slice(0, i)); + buffer = buffer.slice(i + 1); + } + }, + error(error) { + subscriber.error(error); + }, + complete() { + if (buffer.length) { + subscriber.next(buffer); + buffer = ''; + } + + subscriber.complete(); + }, + }); + }); +}; diff --git a/src/dev/prs/pr.ts b/src/dev/prs/pr.ts new file mode 100644 index 0000000000000..7b86b1229e83f --- /dev/null +++ b/src/dev/prs/pr.ts @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createFlagError } from '../run'; + +const isNum = (input: string) => { + return /^\d+$/.test(input); +}; + +export class Pr { + static parseInput(input: string) { + if (!isNum(input)) { + throw createFlagError(`invalid pr number [${input}], expected a number`); + } + + return parseInt(input, 10); + } + + public readonly remoteRef = `pull/${this.number}/head`; + + constructor( + public readonly number: number, + public readonly targetRef: string, + public readonly owner: string, + public readonly sourceBranch: string + ) {} +} diff --git a/src/dev/prs/run_update_prs_cli.ts b/src/dev/prs/run_update_prs_cli.ts new file mode 100644 index 0000000000000..e626dcee6d340 --- /dev/null +++ b/src/dev/prs/run_update_prs_cli.ts @@ -0,0 +1,179 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { resolve } from 'path'; + +import * as Rx from 'rxjs'; +import execa from 'execa'; +import chalk from 'chalk'; +import { first, tap } from 'rxjs/operators'; +import dedent from 'dedent'; + +import { getLine$ } from './helpers'; +import { run, createFlagError } from '../run'; +import { Pr } from './pr'; +import { GithubApi } from './github_api'; + +const UPSTREAM_URL = 'git@github.com:elastic/kibana.git'; + +run( + async ({ flags, log }) => { + /** + * Start off by consuming the necessary flags so that errors from invalid + * flags can be thrown before anything serious is done + */ + const accessToken = flags['access-token']; + if (typeof accessToken !== 'string' && accessToken !== undefined) { + throw createFlagError('invalid --access-token, expected a single string'); + } + + const repoDir = flags['repo-dir']; + if (typeof repoDir !== 'string') { + throw createFlagError('invalid --repo-dir, expected a single string'); + } + + const prNumbers = flags._.map(arg => Pr.parseInput(arg)); + + /** + * Call the Gitub API once for each PR to get the targetRef so we know which branch to pull + * into that pr + */ + const api = new GithubApi(accessToken); + const prs = await Promise.all( + prNumbers.map(async prNumber => { + const { targetRef, owner, sourceBranch } = await api.getPrInfo(prNumber); + return new Pr(prNumber, targetRef, owner, sourceBranch); + }) + ); + + const execInDir = async (cmd: string, args: string[]) => { + log.debug(`$ ${cmd} ${args.join(' ')}`); + + const proc = execa(cmd, args, { + cwd: repoDir, + stdio: ['inherit', 'pipe', 'pipe'], + } as any); + + await Promise.all([ + proc.then(() => log.debug(` - ${cmd} exited with 0`)), + Rx.merge(getLine$(proc.stdout), getLine$(proc.stderr)) + .pipe(tap(line => log.debug(line))) + .toPromise(), + ]); + }; + + const init = async () => { + // ensure local repo is initialized + await execa('git', ['init', repoDir]); + + try { + // attempt to init upstream remote + await execInDir('git', ['remote', 'add', 'upstream', UPSTREAM_URL]); + } catch (error) { + if (error.code !== 128) { + throw error; + } + + // remote already exists, update its url + await execInDir('git', ['remote', 'set-url', 'upstream', UPSTREAM_URL]); + } + }; + + const updatePr = async (pr: Pr) => { + log.info('Fetching...'); + await execInDir('git', [ + 'fetch', + 'upstream', + '-fun', + `pull/${pr.number}/head:${pr.sourceBranch}`, + ]); + await execInDir('git', ['reset', '--hard']); + await execInDir('git', ['clean', '-fd']); + + log.info('Checking out %s:%s locally', pr.owner, pr.sourceBranch); + await execInDir('git', ['checkout', pr.sourceBranch]); + + try { + log.info('Pulling in changes from elastic:%s', pr.targetRef); + await execInDir('git', ['pull', 'upstream', pr.targetRef, '--no-edit']); + } catch (error) { + if (!error.stdout.includes('Automatic merge failed;')) { + throw error; + } + + const resolveConflicts = async () => { + log.error(chalk.red('Conflict resolution required')); + log.info( + dedent(chalk` + Please resolve the merge conflicts in ${repoDir} in another terminal window. + Once the conflicts are resolved run the following in the other window: + + git commit --no-edit + + {bold hit the enter key when complete} + `) + '\n' + ); + + await getLine$(process.stdin) + .pipe(first()) + .toPromise(); + + try { + await execInDir('git', ['diff-index', '--quiet', 'HEAD', '--']); + } catch (_) { + log.error(`Uncommitted changes in ${repoDir}`); + await resolveConflicts(); + } + }; + + await resolveConflicts(); + } + + log.info('Pushing changes to %s:%s', pr.owner, pr.sourceBranch); + await execInDir('git', [ + 'push', + `git@github.com:${pr.owner}/kibana.git`, + `HEAD:${pr.sourceBranch}`, + ]); + + log.success('updated'); + }; + + await init(); + for (const pr of prs) { + log.info('pr #%s', pr.number); + log.indent(4); + try { + await updatePr(pr); + } finally { + log.indent(-4); + } + } + }, + { + description: 'Update github PRs with the latest changes from their base branch', + usage: 'node scripts/update_prs number [...numbers]', + flags: { + string: ['repo-dir', 'access-token'], + default: { + 'repo-dir': resolve(__dirname, '../../../data/.update_prs'), + }, + }, + } +); diff --git a/src/dev/register_git_hook/register_git_hook.js b/src/dev/register_git_hook/register_git_hook.js index 82616795c978a..c24627b4adb46 100644 --- a/src/dev/register_git_hook/register_git_hook.js +++ b/src/dev/register_git_hook/register_git_hook.js @@ -48,58 +48,58 @@ function getKbnPrecommitGitHookScript(rootPath, nodeHome, platform) { # # ** THIS IS AN AUTO-GENERATED FILE ** # ** PLEASE DO NOT CHANGE IT MANUALLY ** - # + # # GENERATED BY ${__dirname} # IF YOU WANNA CHANGE SOMETHING INTO THIS SCRIPT # PLEASE RE-RUN 'yarn kbn bootstrap' or 'node scripts/register_git_hook' IN THE ROOT # OF THE CURRENT PROJECT ${rootPath} - + set -euo pipefail - + # Export Git hook params export GIT_PARAMS="$*" - + has_node() { command -v node >/dev/null 2>&1 } - + has_nvm() { command -v nvm >/dev/null 2>&1 } - + try_load_node_from_nvm_paths () { # If nvm is not loaded, load it has_node || { NVM_SH="${nodeHome}/.nvm/nvm.sh" - + if [ "${platform}" == "darwin" ] && [ -s "$(brew --prefix nvm)/nvm.sh" ]; then NVM_SH="$(brew --prefix nvm)/nvm.sh" fi - + export NVM_DIR=${nodeHome}/.nvm - + [ -s "$NVM_SH" ] && \. "$NVM_SH" - + # If nvm has been loaded correctly, use project .nvmrc has_nvm && nvm use } } - + extend_user_path() { if [ "${platform}" == "win32" ]; then export PATH="$PATH:/c/Program Files/nodejs" - else + else export PATH="$PATH:/usr/local/bin:/usr/local" try_load_node_from_nvm_paths fi } - + # Extend path with common path locations for node # in order to make the hook working on git GUI apps extend_user_path - + # Check if we have node js bin in path - has_node || { + has_node || { echo "Can't found node bin in the PATH. Please update the PATH to proceed." echo "If your PATH already has the node bin, maybe you are using some git GUI app." echo "Can't found node bin in the PATH. Please update the PATH to proceed." @@ -107,12 +107,16 @@ function getKbnPrecommitGitHookScript(rootPath, nodeHome, platform) { echo "In order to proceed, you need to config the PATH used by the application that are launching your git GUI app." echo "If you are running macOS, you can do that using:" echo "'sudo launchctl config user path /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin'" - + exit 1 } - - npm run --silent precommit || { echo "Pre-commit hook failed (add --no-verify to bypass)"; exit 1; } - + + node scripts/precommit_hook || { + echo "Pre-commit hook failed (add --no-verify to bypass)"; + echo ' For eslint failures you can try running \`node scripts/precommit_hook --fix\`'; + exit 1; + } + exit 0 `); } diff --git a/src/dev/renovate/config.ts b/src/dev/renovate/config.ts new file mode 100644 index 0000000000000..588c82b208e28 --- /dev/null +++ b/src/dev/renovate/config.ts @@ -0,0 +1,119 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { RENOVATE_PACKAGE_GROUPS } from './package_groups'; +import { PACKAGE_GLOBS } from './package_globs'; +import { wordRegExp, maybeFlatMap, maybeMap, getTypePackageName } from './utils'; + +const DEFAULT_LABELS = ['release_note:skip', 'renovate', 'v8.0.0', 'v7.3.0']; + +export const RENOVATE_CONFIG = { + extends: ['config:base'], + + includePaths: PACKAGE_GLOBS, + + /** + * Only submit PRs to these branches, we will manually backport PRs for now + */ + baseBranches: ['master'], + + /** + * Labels added to PRs opened by renovate + */ + labels: DEFAULT_LABELS, + + /** + * Config customizations for major version upgrades + */ + major: { + labels: [...DEFAULT_LABELS, 'renovate:major'], + }, + + /** + * Enable creation of a "Master Issue" within the repository. This + * Master Issue is akin to a mini dashboard and contains a list of all + * PRs pending, open, closed (unmerged) or in error. + */ + masterIssue: true, + + /** + * Whether updates should require manual approval from within the + * Master Issue before creation. + * + * We can turn this off once we've gotten through the backlog of + * outdated packages. + */ + masterIssueApproval: true, + + /** + * Policy for how to modify/update existing ranges + * pin = convert ranges to exact versions, e.g. ^1.0.0 -> 1.1.0 + */ + rangeStrategy: 'replace', + + npm: { + /** + * This deletes and re-creates the lock file, which we will only want + * to turn on once we've updated all our deps and enabled version pinning + */ + lockFileMaintenance: { enabled: false }, + + /** + * Define groups of packages that should be updated/configured together + */ + packageRules: [ + ...RENOVATE_PACKAGE_GROUPS.map(group => ({ + groupSlug: group.name, + groupName: `${group.name} related packages`, + packagePatterns: maybeMap(group.packageWords, word => wordRegExp(word).source), + packageNames: maybeFlatMap(group.packageNames, name => [name, getTypePackageName(name)]), + labels: group.extraLabels && [...DEFAULT_LABELS, ...group.extraLabels], + enabled: group.enabled === false ? false : undefined, + })), + + // internal/local packages + { + packagePatterns: ['^@kbn/.*'], + enabled: false, + }, + ], + }, + + /** + * Limit the number of active PRs renovate will allow + */ + prConcurrentLimit: 6, + + /** + * Disable vulnerability alert handling, we handle that separately + */ + vulnerabilityAlerts: { + enabled: false, + }, + + /** + * Disable automatic rebase on each change to base branch + */ + rebaseStalePrs: false, + + /** + * Disable semantic commit formating + */ + semanticCommits: false, +}; diff --git a/src/dev/renovate/package_globs.ts b/src/dev/renovate/package_globs.ts new file mode 100644 index 0000000000000..cd77065e1211e --- /dev/null +++ b/src/dev/renovate/package_globs.ts @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { readFileSync } from 'fs'; + +import globby from 'globby'; + +import { REPO_ROOT } from '../constants'; + +export const PACKAGE_GLOBS = [ + 'package.json', + 'x-pack/package.json', + 'x-pack/plugins/*/package.json', + 'packages/*/package.json', + 'test/plugin_functional/plugins/*/package.json', + 'test/interpreter_functional/plugins/*/package.json', +]; + +export function getAllDepNames() { + const depNames = new Set(); + + for (const glob of PACKAGE_GLOBS) { + const files = globby.sync(glob, { + cwd: REPO_ROOT, + absolute: true, + }); + + for (const path of files) { + const pkg = JSON.parse(readFileSync(path, 'utf8')); + const deps = [ + ...Object.keys(pkg.dependencies || {}), + ...Object.keys(pkg.devDependencies || {}), + ]; + + for (const dep of deps) { + depNames.add(dep); + } + } + } + + return depNames; +} diff --git a/src/dev/renovate/package_groups.ts b/src/dev/renovate/package_groups.ts new file mode 100644 index 0000000000000..ad43621473d78 --- /dev/null +++ b/src/dev/renovate/package_groups.ts @@ -0,0 +1,168 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getAllDepNames } from './package_globs'; +import { wordRegExp, unwrapTypesPackage } from './utils'; + +interface PackageGroup { + /** + * The group name, will be used for the branch name and in pr titles + */ + readonly name: string; + + /** + * Specific words that, when found in the package name, identify it as part of this group + */ + readonly packageWords?: string[]; + + /** + * Exact package names that should be included in this group + */ + readonly packageNames?: string[]; + + /** + * Extra labels to apply to PRs created for packages in this group + */ + readonly extraLabels?: string[]; + + /** + * A flag that will prevent renovatebot from telling us when there + * are updates. This should only be used in very special cases, like + * when we intend to never update a package. To just prevent a version + * upgrade consider support for the `allowedVersions` config, or just + * closing PRs to communicate to renovate that the specific upgrade + * should be ignored. + */ + readonly enabled?: false; +} + +export const RENOVATE_PACKAGE_GROUPS: PackageGroup[] = [ + { + name: 'eslint', + packageWords: ['eslint'], + }, + + { + name: 'babel', + packageWords: ['babel'], + packageNames: ['core-js'], + }, + + { + name: 'jest', + packageWords: ['jest'], + }, + + { + name: 'mocha', + packageWords: ['mocha'], + }, + + { + name: 'karma', + packageWords: ['karma'], + }, + + { + name: 'gulp', + packageWords: ['gulp'], + }, + + { + name: 'grunt', + packageWords: ['grunt'], + }, + + { + name: 'angular', + packageWords: ['angular'], + }, + + { + name: 'd3', + packageWords: ['d3'], + }, + + { + name: 'react', + packageWords: ['react', 'redux', 'enzyme'], + packageNames: ['ngreact', 'recompose', 'prop-types', 'typescript-fsa-reducers', 'reselect'], + }, + + { + name: 'graphql', + packageWords: ['graphql'], + }, + + { + name: 'webpack', + packageWords: ['webpack', 'loader'], + packageNames: ['mini-css-extract-plugin', 'chokidar'], + }, + + { + name: 'language server', + packageNames: ['vscode-jsonrpc', 'vscode-languageserver', 'vscode-languageserver-types'], + }, + + { + name: 'hapi', + packageWords: ['hapi'], + packageNames: ['hapi', 'joi', 'boom', 'hoek', 'h2o2', '@elastic/good', 'good-squeeze', 'inert'], + }, + + { + name: 'dragselect', + packageNames: ['dragselect'], + extraLabels: [':ml'], + }, + + { + name: 'api-documenter', + packageNames: ['@microsoft/api-documenter', '@microsoft/api-extractor'], + enabled: false, + }, +]; + +/** + * Auto-define package groups for any `@types/*` deps that are not already in a group + */ +for (const dep of getAllDepNames()) { + const typesFor = unwrapTypesPackage(dep); + if (!typesFor) { + continue; + } + + // determine if one of the existing groups has typesFor in its + // packageNames or if any of the packageWords is in typesFor + const existing = RENOVATE_PACKAGE_GROUPS.some( + group => + (group.packageNames || []).includes(typesFor) || + (group.packageWords || []).some(word => wordRegExp(word).test(typesFor)) + ); + + if (existing) { + continue; + } + + RENOVATE_PACKAGE_GROUPS.push({ + name: typesFor, + packageNames: [typesFor], + }); +} diff --git a/src/dev/renovate/run_build_renovate_config_cli.ts b/src/dev/renovate/run_build_renovate_config_cli.ts new file mode 100644 index 0000000000000..699b6f155adcb --- /dev/null +++ b/src/dev/renovate/run_build_renovate_config_cli.ts @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { writeFileSync } from 'fs'; +import { resolve } from 'path'; +import json5 from 'json5'; +import dedent from 'dedent'; + +import { run } from '../run'; +import { REPO_ROOT } from '../constants'; +import { RENOVATE_CONFIG } from './config'; + +run( + async () => { + const genInfo = dedent` + /** + * PLEASE DO NOT MODIFY + * + * This file is automatically generated by running \`node scripts/build_renovate_config\` + * + */ + `; + + writeFileSync( + resolve(REPO_ROOT, 'renovate.json5'), + `${genInfo}\n${json5.stringify(RENOVATE_CONFIG, null, 2)}\n` + ); + }, + { + description: + 'Regenerate the renovate.json5 file at the root of the repo based on the config in src/dev/renovate', + } +); diff --git a/src/dev/renovate/utils.ts b/src/dev/renovate/utils.ts new file mode 100644 index 0000000000000..a3c7e1b56d7b7 --- /dev/null +++ b/src/dev/renovate/utils.ts @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const maybeMap = (input: T[] | undefined, fn: (i: T) => T2) => + input ? input.map(fn) : undefined; + +export const maybeFlatMap = (input: T[] | undefined, fn: (i: T) => T2[]) => + input ? input.reduce((acc, i) => [...acc, ...fn(i)], [] as T2[]) : undefined; + +export const wordRegExp = (word: string) => new RegExp(`(\\b|_)${word}(\\b|_)`); + +export const getTypePackageName = (pkgName: string) => { + const scopedPkgRe = /^@(.+?)\/(.+?)$/; + const match = pkgName.match(scopedPkgRe); + return `@types/${match ? `${match[1]}__${match[2]}` : pkgName}`; +}; + +export const unwrapTypesPackage = (pkgName: string) => { + if (!pkgName.startsWith('@types')) { + return; + } + + const typesFor = pkgName.slice('@types/'.length); + + if (!typesFor.includes('__')) { + return typesFor; + } + + // @types packages use a convention for scoped packages, @types/org__name + const [org, name] = typesFor.split('__'); + return `@${org}/${name}`; +}; diff --git a/src/dev/run_precommit_hook.js b/src/dev/run_precommit_hook.js index 1b925beb577cb..14526ba98b28e 100644 --- a/src/dev/run_precommit_hook.js +++ b/src/dev/run_precommit_hook.js @@ -22,7 +22,7 @@ import * as Eslint from './eslint'; import * as Sasslint from './sasslint'; import { getFilesForCommit, checkFileCasing } from './precommit_hook'; -run(async ({ log }) => { +run(async ({ log, flags }) => { const files = await getFilesForCommit(); const errors = []; @@ -36,7 +36,9 @@ run(async ({ log }) => { const filesToLint = Linter.pickFilesToLint(log, files); if (filesToLint.length > 0) { try { - await Linter.lintFiles(log, filesToLint); + await Linter.lintFiles(log, filesToLint, { + fix: flags.fix + }); } catch (error) { errors.push(error); } @@ -46,4 +48,17 @@ run(async ({ log }) => { if (errors.length) { throw combineErrors(errors); } +}, { + description: ` + Run checks on files that are staged for commit + `, + flags: { + boolean: ['fix'], + default: { + fix: false + }, + help: ` + --fix Execute eslint in --fix mode + ` + }, }); diff --git a/src/es_archiver/lib/indices/kibana_index.js b/src/es_archiver/lib/indices/kibana_index.js index 77c82b230dc65..1ab3c59e48376 100644 --- a/src/es_archiver/lib/indices/kibana_index.js +++ b/src/es_archiver/lib/indices/kibana_index.js @@ -188,7 +188,6 @@ export async function cleanKibanaIndices({ client, stats, log, kibanaUrl }) { bool: { must_not: { ids: { - type: '_doc', values: ['space:default'], }, }, diff --git a/src/fixtures/stubbed_logstash_index_pattern.js b/src/fixtures/stubbed_logstash_index_pattern.js index b35a153faf1a7..88dc5d708dc5f 100644 --- a/src/fixtures/stubbed_logstash_index_pattern.js +++ b/src/fixtures/stubbed_logstash_index_pattern.js @@ -18,12 +18,12 @@ */ import TestUtilsStubIndexPatternProvider from 'test_utils/stub_index_pattern'; -import FixturesLogstashFieldsProvider from 'fixtures/logstash_fields'; +import stubbedLogstashFields from 'fixtures/logstash_fields'; import { getKbnFieldType } from '../legacy/utils'; export default function stubbedLogstashIndexPatternService(Private) { const StubIndexPattern = Private(TestUtilsStubIndexPatternProvider); - const mockLogstashFields = Private(FixturesLogstashFieldsProvider); + const mockLogstashFields = stubbedLogstashFields(); const fields = mockLogstashFields.map(function (field) { const kbnType = getKbnFieldType(field.type); diff --git a/src/fixtures/stubbed_saved_object_index_pattern.js b/src/fixtures/stubbed_saved_object_index_pattern.js index fa3575e1e4fff..0b7cc6f05e509 100644 --- a/src/fixtures/stubbed_saved_object_index_pattern.js +++ b/src/fixtures/stubbed_saved_object_index_pattern.js @@ -17,11 +17,11 @@ * under the License. */ -import FixturesLogstashFieldsProvider from 'fixtures/logstash_fields'; +import stubbedLogstashFields from 'fixtures/logstash_fields'; import { SimpleSavedObject } from 'ui/saved_objects'; -export function FixturesStubbedSavedObjectIndexPatternProvider(Private) { - const mockLogstashFields = Private(FixturesLogstashFieldsProvider); +export function FixturesStubbedSavedObjectIndexPatternProvider() { + const mockLogstashFields = stubbedLogstashFields(); return function (id) { return new SimpleSavedObject(undefined, { diff --git a/src/functional_test_runner/lib/config/config.ts b/src/functional_test_runner/lib/config/config.ts index 9819f23a656bc..03eb048a125bb 100644 --- a/src/functional_test_runner/lib/config/config.ts +++ b/src/functional_test_runner/lib/config/config.ts @@ -120,4 +120,12 @@ export class Config { } }); } + + public getAll() { + return cloneDeep(this[$values], v => { + if (typeof v === 'function') { + return v; + } + }); + } } diff --git a/src/functional_test_runner/lib/config/schema.ts b/src/functional_test_runner/lib/config/schema.ts index 57c0b1135c3af..8362292412102 100644 --- a/src/functional_test_runner/lib/config/schema.ts +++ b/src/functional_test_runner/lib/config/schema.ts @@ -131,6 +131,14 @@ export const schema = Joi.object() updateBaselines: Joi.boolean().default(false), + browser: Joi.object() + .keys({ + type: Joi.string() + .valid('chrome', 'firefox') + .default('chrome'), + }) + .default(), + junit: Joi.object() .keys({ enabled: Joi.boolean().default(!!process.env.CI), @@ -166,6 +174,7 @@ export const schema = Joi.object() license: Joi.string().default('oss'), from: Joi.string().default('snapshot'), serverArgs: Joi.array(), + serverEnvVars: Joi.object(), dataArchive: Joi.string(), }) .default(), diff --git a/src/legacy/core_plugins/console/api_server/spec/overrides/snapshot.create_repository.json b/src/legacy/core_plugins/console/api_server/spec/overrides/snapshot.create_repository.json index 64a5d0188a1db..c513292f2bd59 100644 --- a/src/legacy/core_plugins/console/api_server/spec/overrides/snapshot.create_repository.json +++ b/src/legacy/core_plugins/console/api_server/spec/overrides/snapshot.create_repository.json @@ -4,7 +4,7 @@ "__template": { "type": "" }, "type": { - "__one_of": ["fs", "url", "s3", "hdfs"] + "__one_of": ["fs", "url", "s3", "hdfs", "azure"] }, "settings": { "__one_of": [{ @@ -59,6 +59,21 @@ "concurrent_streams": 5, "compress": { "__one_of": [true, false] }, "chunk_size": "10m" + }, + { + "__condition": { + "lines_regex": "type[\"']\\s*:\\s*[\"']azure" + }, + "__template": { + "path": "" + }, + "container": "", + "base_path": "", + "client": "default", + "location_mode": { "__one_of": ["primary_only", "secondary_only"] }, + "readonly": { "__one_of": [true, false] }, + "compress": { "__one_of": [true, false] }, + "chunk_size": "10m" } ] } diff --git a/src/legacy/core_plugins/console/public/src/controllers/sense_controller.js b/src/legacy/core_plugins/console/public/src/controllers/sense_controller.js index ba38a6eddddc0..5c4f09b0086e1 100644 --- a/src/legacy/core_plugins/console/public/src/controllers/sense_controller.js +++ b/src/legacy/core_plugins/console/public/src/controllers/sense_controller.js @@ -29,7 +29,7 @@ import { DOC_LINK_VERSION } from 'ui/documentation_links'; const module = require('ui/modules').get('app/sense'); -module.run(function (Private, $rootScope) { +module.run(function ($rootScope) { module.setupResizeCheckerForRootEditors = ($el, ...editors) => { return applyResizeCheckerToEditors($rootScope, $el, ...editors); }; @@ -41,6 +41,11 @@ module.controller('SenseController', function SenseController(Private, $scope, $ $scope.topNavController = Private(SenseTopNavController); + // Since we pass this callback via reactDirective into a react component, which has the function defined as required + // in it's prop types, we should set this initially (before it's set in the $timeout below). Without this line + // the component we pass this in will throw an propType validation error. + $scope.getRequestsAsCURL = () => ''; + // We need to wait for these elements to be rendered before we can select them with jQuery // and then initialize this app let input; diff --git a/src/legacy/core_plugins/console/public/src/controllers/sense_top_nav_controller.js b/src/legacy/core_plugins/console/public/src/controllers/sense_top_nav_controller.js index b7aaf20d45ec5..e3c1a1f1c80e4 100644 --- a/src/legacy/core_plugins/console/public/src/controllers/sense_top_nav_controller.js +++ b/src/legacy/core_plugins/console/public/src/controllers/sense_top_nav_controller.js @@ -18,15 +18,16 @@ */ import { KbnTopNavControllerProvider } from 'ui/kbn_top_nav/kbn_top_nav_controller'; +import { i18n } from '@kbn/i18n'; import storage from '../storage'; -export function SenseTopNavController(Private, i18n) { +export function SenseTopNavController(Private) { const KbnTopNavController = Private(KbnTopNavControllerProvider); const controller = new KbnTopNavController([ { key: 'welcome', - label: i18n('console.topNav.welcomeTabLabel', { + label: i18n.translate('console.topNav.welcomeTabLabel', { defaultMessage: 'Welcome' }), hideButton: true, @@ -35,10 +36,10 @@ export function SenseTopNavController(Private, i18n) { }, { key: 'history', - label: i18n('console.topNav.historyTabLabel', { + label: i18n.translate('console.topNav.historyTabLabel', { defaultMessage: 'History' }), - description: i18n('console.topNav.historyTabDescription', { + description: i18n.translate('console.topNav.historyTabDescription', { defaultMessage: 'History', }), template: ``, @@ -46,10 +47,10 @@ export function SenseTopNavController(Private, i18n) { }, { key: 'settings', - label: i18n('console.topNav.settingsTabLabel', { + label: i18n.translate('console.topNav.settingsTabLabel', { defaultMessage: 'Settings' }), - description: i18n('console.topNav.settingsTabDescription', { + description: i18n.translate('console.topNav.settingsTabDescription', { defaultMessage: 'Settings', }), template: ``, @@ -57,10 +58,10 @@ export function SenseTopNavController(Private, i18n) { }, { key: 'help', - label: i18n('console.topNav.helpTabLabel', { + label: i18n.translate('console.topNav.helpTabLabel', { defaultMessage: 'Help' }), - description: i18n('console.topNav.helpTabDescription', { + description: i18n.translate('console.topNav.helpTabDescription', { defaultMessage: 'Help', }), template: ``, diff --git a/src/legacy/core_plugins/console/public/src/directives/console_menu_directive.js b/src/legacy/core_plugins/console/public/src/directives/console_menu_directive.js index 32c1bcd278072..b5a1476e8f9ce 100644 --- a/src/legacy/core_plugins/console/public/src/directives/console_menu_directive.js +++ b/src/legacy/core_plugins/console/public/src/directives/console_menu_directive.js @@ -26,9 +26,5 @@ const module = uiModules.get('apps/sense', ['react']); import { ConsoleMenu } from '../console_menu'; module.directive('consoleMenu', function (reactDirective) { - return reactDirective( - wrapInI18nContext(ConsoleMenu), - undefined, - { restrict: 'E' } - ); + return reactDirective(wrapInI18nContext(ConsoleMenu)); }); diff --git a/src/legacy/core_plugins/console/public/src/directives/sense_help.js b/src/legacy/core_plugins/console/public/src/directives/sense_help.js index e244399e2d833..32149aacac388 100644 --- a/src/legacy/core_plugins/console/public/src/directives/sense_help.js +++ b/src/legacy/core_plugins/console/public/src/directives/sense_help.js @@ -18,12 +18,13 @@ */ require('./sense_help_example'); +import template from './help.html'; require('ui/modules') .get('app/sense') .directive('senseHelp', function () { return { restrict: 'E', - template: require('./help.html') + template }; }); diff --git a/src/legacy/core_plugins/console/public/src/directives/sense_help_example.js b/src/legacy/core_plugins/console/public/src/directives/sense_help_example.js index d4e4632a45dfb..d27c4b39041ed 100644 --- a/src/legacy/core_plugins/console/public/src/directives/sense_help_example.js +++ b/src/legacy/core_plugins/console/public/src/directives/sense_help_example.js @@ -18,7 +18,7 @@ */ const SenseEditor = require('../sense_editor/editor'); -const exampleText = require('raw-loader!./helpExample.txt').trim(); +import exampleText from 'raw-loader!./helpExample.txt'; import { applyResizeCheckerToEditors } from '../sense_editor_resize'; require('ui/modules') @@ -27,7 +27,7 @@ require('ui/modules') return { restrict: 'E', link: function ($scope, $el) { - $el.text(exampleText); + $el.text(exampleText.trim()); $scope.editor = new SenseEditor($el); applyResizeCheckerToEditors($scope, $el, $scope.editor); $scope.editor.setReadOnly(true); diff --git a/src/legacy/core_plugins/console/public/src/directives/sense_history.js b/src/legacy/core_plugins/console/public/src/directives/sense_history.js index 39e4bcf8e7835..178fe6e7c5dc7 100644 --- a/src/legacy/core_plugins/console/public/src/directives/sense_history.js +++ b/src/legacy/core_plugins/console/public/src/directives/sense_history.js @@ -18,6 +18,7 @@ */ import { keyCodes } from '@elastic/eui'; +import template from './history.html'; const { memoize } = require('lodash'); const moment = require('moment'); @@ -30,7 +31,7 @@ require('ui/modules') .directive('senseHistory', function () { return { restrict: 'E', - template: require('./history.html'), + template, controllerAs: 'history', controller: function ($scope, $element) { this.reqs = history.getHistory(); diff --git a/src/legacy/core_plugins/console/public/src/directives/sense_history_viewer.js b/src/legacy/core_plugins/console/public/src/directives/sense_history_viewer.js index d7ad736335f52..e066605a2ca83 100644 --- a/src/legacy/core_plugins/console/public/src/directives/sense_history_viewer.js +++ b/src/legacy/core_plugins/console/public/src/directives/sense_history_viewer.js @@ -19,11 +19,13 @@ const SenseEditor = require('../sense_editor/editor'); +import { i18n } from '@kbn/i18n'; + import { applyResizeCheckerToEditors } from '../sense_editor_resize'; require('ui/modules') .get('app/sense') - .directive('senseHistoryViewer', function (i18n) { + .directive('senseHistoryViewer', function () { return { restrict: 'E', scope: { @@ -43,7 +45,7 @@ require('ui/modules') viewer.clearSelection(); } else { viewer.getSession().setValue( - i18n('console.historyPage.noHistoryTextMessage', { defaultMessage: 'No history available' }) + i18n.translate('console.historyPage.noHistoryTextMessage', { defaultMessage: 'No history available' }) ); } }); diff --git a/src/legacy/core_plugins/console/public/src/directives/sense_settings.js b/src/legacy/core_plugins/console/public/src/directives/sense_settings.js index 6fe38b072ebcd..8405990af460d 100644 --- a/src/legacy/core_plugins/console/public/src/directives/sense_settings.js +++ b/src/legacy/core_plugins/console/public/src/directives/sense_settings.js @@ -19,6 +19,7 @@ require('ui/directives/input_focus'); +import template from './settings.html'; const mappings = require('../mappings'); require('ui/modules') @@ -26,7 +27,7 @@ require('ui/modules') .directive('senseSettings', function () { return { restrict: 'E', - template: require('./settings.html'), + template, controllerAs: 'settings', controller: function ($scope, $element) { const settings = require('../settings'); diff --git a/src/legacy/core_plugins/console/public/src/directives/sense_welcome.js b/src/legacy/core_plugins/console/public/src/directives/sense_welcome.js index 28834f907a90d..4689012645308 100644 --- a/src/legacy/core_plugins/console/public/src/directives/sense_welcome.js +++ b/src/legacy/core_plugins/console/public/src/directives/sense_welcome.js @@ -19,19 +19,22 @@ require('./sense_help_example'); +import { i18n } from '@kbn/i18n'; +import template from './welcome.html'; + const storage = require('../storage'); require('ui/modules') .get('app/sense') - .directive('senseWelcome', function (i18n) { + .directive('senseWelcome', function () { return { restrict: 'E', - template: require('./welcome.html'), + template, link: function ($scope) { $scope.$on('$destroy', function () { storage.set('version_welcome_shown', '@@SENSE_REVISION'); }); - $scope.asWellAsFragmentText = i18n('console.welcomePage.supportedRequestFormatDescription.asWellAsFragmentText', { + $scope.asWellAsFragmentText = i18n.translate('console.welcomePage.supportedRequestFormatDescription.asWellAsFragmentText', { defaultMessage: 'as well as' }); }, diff --git a/src/legacy/core_plugins/console/public/src/sense_editor/mode/worker/index.js b/src/legacy/core_plugins/console/public/src/sense_editor/mode/worker/index.js index 010d32fb0c863..3d5a719b9fd1f 100644 --- a/src/legacy/core_plugins/console/public/src/sense_editor/mode/worker/index.js +++ b/src/legacy/core_plugins/console/public/src/sense_editor/mode/worker/index.js @@ -17,7 +17,9 @@ * under the License. */ +import src from '!!raw-loader!./worker.js'; + export const workerModule = { id: 'sense_editor/mode/worker', - src: require('!!raw-loader!./worker.js') + src }; diff --git a/src/legacy/core_plugins/data/public/expressions/expression_renderer.tsx b/src/legacy/core_plugins/data/public/expressions/expression_renderer.tsx new file mode 100644 index 0000000000000..78fb43772504c --- /dev/null +++ b/src/legacy/core_plugins/data/public/expressions/expression_renderer.tsx @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { useRef, useEffect } from 'react'; +import React from 'react'; +import { Ast } from '@kbn/interpreter/common'; + +import { ExpressionRunnerOptions, ExpressionRunner } from './expression_runner'; +import { Result } from './expressions_service'; + +// Accept all options of the runner as props except for the +// dom element which is provided by the component itself +export type ExpressionRendererProps = Pick< + ExpressionRunnerOptions, + Exclude +> & { + expression: string | Ast; + /** + * If an element is specified, but the response of the expression run can't be rendered + * because it isn't a valid response or the specified renderer isn't available, + * this callback is called with the given result. + */ + onRenderFailure?: (result: Result) => void; +}; + +export type ExpressionRenderer = React.FC; + +export const createRenderer = (run: ExpressionRunner): ExpressionRenderer => ({ + expression, + onRenderFailure, + ...options +}: ExpressionRendererProps) => { + const mountpoint: React.MutableRefObject = useRef(null); + + useEffect( + () => { + if (mountpoint.current) { + run(expression, { ...options, element: mountpoint.current }).catch(result => { + if (onRenderFailure) { + onRenderFailure(result); + } + }); + } + }, + [expression, mountpoint.current] + ); + + return ( +
{ + mountpoint.current = el; + }} + /> + ); +}; diff --git a/src/legacy/core_plugins/data/public/expressions/expression_runner.ts b/src/legacy/core_plugins/data/public/expressions/expression_runner.ts new file mode 100644 index 0000000000000..bfad401ae8620 --- /dev/null +++ b/src/legacy/core_plugins/data/public/expressions/expression_runner.ts @@ -0,0 +1,75 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Ast, fromExpression } from '@kbn/interpreter/common'; + +import { RequestAdapter, DataAdapter } from 'ui/inspector/adapters'; +import { RenderFunctionsRegistry, Interpreter, Result } from './expressions_service'; + +export interface ExpressionRunnerOptions { + // TODO use the real types here once they are ready + context?: object; + getInitialContext?: () => object; + element?: Element; +} + +export type ExpressionRunner = ( + expression: string | Ast, + options: ExpressionRunnerOptions +) => Promise; + +export const createRunFn = ( + renderersRegistry: RenderFunctionsRegistry, + interpreterPromise: Promise +): ExpressionRunner => async (expressionOrAst, { element, context, getInitialContext }) => { + // TODO: make interpreter initialization synchronous to avoid this + const interpreter = await interpreterPromise; + const ast = + typeof expressionOrAst === 'string' ? fromExpression(expressionOrAst) : expressionOrAst; + + const response = await interpreter.interpretAst(ast, context || { type: 'null' }, { + getInitialContext: getInitialContext || (() => ({})), + inspectorAdapters: { + // TODO connect real adapters + requests: new RequestAdapter(), + data: new DataAdapter(), + }, + }); + + if (response.type === 'error') { + throw response; + } + + if (element) { + if (response.type === 'render' && response.as && renderersRegistry.get(response.as) !== null) { + renderersRegistry.get(response.as).render(element, response.value, { + onDestroy: fn => { + // TODO implement + }, + done: () => { + // TODO implement + }, + }); + } else { + throw response; + } + } + + return response; +}; diff --git a/src/legacy/core_plugins/data/public/expressions/expressions_service.test.tsx b/src/legacy/core_plugins/data/public/expressions/expressions_service.test.tsx new file mode 100644 index 0000000000000..fdd0d73763681 --- /dev/null +++ b/src/legacy/core_plugins/data/public/expressions/expressions_service.test.tsx @@ -0,0 +1,276 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { fromExpression, Ast } from '@kbn/interpreter/common'; + +import { + ExpressionsService, + RenderFunctionsRegistry, + RenderFunction, + Interpreter, + ExpressionsServiceDependencies, + Result, + ExpressionsSetup, +} from './expressions_service'; +import { mount } from 'enzyme'; +import React from 'react'; + +const waitForInterpreterRun = async () => { + // Wait for two ticks with empty callback queues + // This makes sure the runFn promise and actual interpretAst + // promise have been resolved and processed + await new Promise(resolve => setTimeout(resolve)); + await new Promise(resolve => setTimeout(resolve)); +}; + +const RENDERER_ID = 'mockId'; + +describe('expressions_service', () => { + let interpretAstMock: jest.Mocked['interpretAst']; + let interpreterMock: jest.Mocked; + let renderFunctionMock: jest.Mocked; + let setupPluginsMock: ExpressionsServiceDependencies; + const expressionResult: Result = { type: 'render', as: RENDERER_ID, value: {} }; + + let api: ExpressionsSetup; + let testExpression: string; + let testAst: Ast; + + beforeEach(() => { + interpretAstMock = jest.fn(_ => Promise.resolve(expressionResult)); + interpreterMock = { interpretAst: interpretAstMock }; + renderFunctionMock = ({ + render: jest.fn(), + } as unknown) as jest.Mocked; + setupPluginsMock = { + interpreter: { + getInterpreter: () => Promise.resolve({ interpreter: interpreterMock }), + renderersRegistry: ({ + get: (id: string) => (id === RENDERER_ID ? renderFunctionMock : null), + } as unknown) as RenderFunctionsRegistry, + }, + }; + api = new ExpressionsService().setup(setupPluginsMock); + testExpression = 'test | expression'; + testAst = fromExpression(testExpression); + }); + + describe('expression_runner', () => { + it('should return run function', () => { + expect(typeof api.run).toBe('function'); + }); + + it('should call the interpreter with parsed expression', async () => { + await api.run(testExpression, { element: document.createElement('div') }); + expect(interpreterMock.interpretAst).toHaveBeenCalledWith( + testAst, + expect.anything(), + expect.anything() + ); + }); + + it('should call the interpreter with given context and getInitialContext functions', async () => { + const getInitialContext = () => ({}); + const context = {}; + + await api.run(testExpression, { getInitialContext, context }); + const interpretCall = interpreterMock.interpretAst.mock.calls[0]; + + expect(interpretCall[1]).toBe(context); + expect(interpretCall[2].getInitialContext).toBe(getInitialContext); + }); + + it('should call the interpreter with passed in ast', async () => { + await api.run(testAst, { element: document.createElement('div') }); + expect(interpreterMock.interpretAst).toHaveBeenCalledWith( + testAst, + expect.anything(), + expect.anything() + ); + }); + + it('should return the result of the interpreter run', async () => { + const response = await api.run(testAst, {}); + expect(response).toBe(expressionResult); + }); + + it('should reject the promise if the response is not renderable but an element is passed', async () => { + const unexpectedResult = { type: 'datatable', value: {} }; + interpretAstMock.mockReturnValue(Promise.resolve(unexpectedResult)); + expect( + api.run(testAst, { + element: document.createElement('div'), + }) + ).rejects.toBe(unexpectedResult); + }); + + it('should reject the promise if the renderer is not known', async () => { + const unexpectedResult = { type: 'render', as: 'unknown_id' }; + interpretAstMock.mockReturnValue(Promise.resolve(unexpectedResult)); + expect( + api.run(testAst, { + element: document.createElement('div'), + }) + ).rejects.toBe(unexpectedResult); + }); + + it('should not reject the promise on unknown renderer if the runner is not rendering', async () => { + const unexpectedResult = { type: 'render', as: 'unknown_id' }; + interpretAstMock.mockReturnValue(Promise.resolve(unexpectedResult)); + expect(api.run(testAst, {})).resolves.toBe(unexpectedResult); + }); + + it('should reject the promise if the response is an error', async () => { + const errorResult = { type: 'error', error: {} }; + interpretAstMock.mockReturnValue(Promise.resolve(errorResult)); + expect(api.run(testAst, {})).rejects.toBe(errorResult); + }); + + it('should reject the promise if there are syntax errors', async () => { + expect(api.run('|||', {})).rejects.toBeInstanceOf(Error); + }); + + it('should call the render function with the result and element', async () => { + const element = document.createElement('div'); + + await api.run(testAst, { element }); + expect(renderFunctionMock.render).toHaveBeenCalledWith( + element, + expressionResult.value, + expect.anything() + ); + expect(interpreterMock.interpretAst).toHaveBeenCalledWith( + testAst, + expect.anything(), + expect.anything() + ); + }); + }); + + describe('expression_renderer', () => { + it('should call interpreter and render function when called through react component', async () => { + const ExpressionRenderer = api.ExpressionRenderer; + + mount(); + + await waitForInterpreterRun(); + + expect(renderFunctionMock.render).toHaveBeenCalledWith( + expect.any(Element), + expressionResult.value, + expect.anything() + ); + expect(interpreterMock.interpretAst).toHaveBeenCalledWith( + testAst, + expect.anything(), + expect.anything() + ); + }); + + it('should call the interpreter with given context and getInitialContext functions', async () => { + const getInitialContext = () => ({}); + const context = {}; + + const ExpressionRenderer = api.ExpressionRenderer; + + mount( + + ); + + await waitForInterpreterRun(); + + const interpretCall = interpreterMock.interpretAst.mock.calls[0]; + + expect(interpretCall[1]).toBe(context); + expect(interpretCall[2].getInitialContext).toBe(getInitialContext); + }); + + it('should call interpreter and render function again if expression changes', async () => { + const ExpressionRenderer = api.ExpressionRenderer; + + const instance = mount(); + + await waitForInterpreterRun(); + + expect(renderFunctionMock.render).toHaveBeenCalledWith( + expect.any(Element), + expressionResult.value, + expect.anything() + ); + expect(interpreterMock.interpretAst).toHaveBeenCalledWith( + testAst, + expect.anything(), + expect.anything() + ); + + instance.setProps({ expression: 'supertest | expression ' }); + + await waitForInterpreterRun(); + + expect(renderFunctionMock.render).toHaveBeenCalledTimes(2); + expect(interpreterMock.interpretAst).toHaveBeenCalledTimes(2); + }); + + it('should not call interpreter and render function again if expression does not change', async () => { + const ast = fromExpression(testExpression); + + const ExpressionRenderer = api.ExpressionRenderer; + + const instance = mount(); + + await waitForInterpreterRun(); + + expect(renderFunctionMock.render).toHaveBeenCalledWith( + expect.any(Element), + expressionResult.value, + expect.anything() + ); + expect(interpreterMock.interpretAst).toHaveBeenCalledWith( + ast, + expect.anything(), + expect.anything() + ); + + instance.update(); + + await waitForInterpreterRun(); + + expect(renderFunctionMock.render).toHaveBeenCalledTimes(1); + expect(interpreterMock.interpretAst).toHaveBeenCalledTimes(1); + }); + + it('should call onRenderFailure if the result can not be rendered', async () => { + const errorResult = { type: 'error', error: {} }; + interpretAstMock.mockReturnValue(Promise.resolve(errorResult)); + const renderFailureSpy = jest.fn(); + + const ExpressionRenderer = api.ExpressionRenderer; + + mount(); + + await waitForInterpreterRun(); + + expect(renderFailureSpy).toHaveBeenCalledWith(errorResult); + }); + }); +}); diff --git a/src/legacy/core_plugins/data/public/expressions/expressions_service.ts b/src/legacy/core_plugins/data/public/expressions/expressions_service.ts new file mode 100644 index 0000000000000..f22caf3d43ece --- /dev/null +++ b/src/legacy/core_plugins/data/public/expressions/expressions_service.ts @@ -0,0 +1,127 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Ast } from '@kbn/interpreter/common'; + +// TODO: +// this type import and the types below them should be switched to the types of +// the interpreter plugin itself once they are ready +import { Registry } from '@kbn/interpreter/common'; +import { Adapters } from 'ui/inspector'; +import { Query, Filters, TimeRange } from 'ui/embeddable'; +import { createRenderer } from './expression_renderer'; +import { createRunFn } from './expression_runner'; + +export interface InitialContextObject { + timeRange?: TimeRange; + filters?: Filters; + query?: Query; +} + +export type getInitialContextFunction = () => InitialContextObject; + +export interface Handlers { + getInitialContext: getInitialContextFunction; + inspectorAdapters?: Adapters; +} + +type Context = object; +export interface Result { + type: string; + as?: string; + value?: unknown; + error?: unknown; +} + +interface RenderHandlers { + done: () => void; + onDestroy: (fn: () => void) => void; +} + +export interface RenderFunction { + name: string; + displayName: string; + help: string; + validate: () => void; + reuseDomNode: boolean; + render: (domNode: Element, data: unknown, handlers: RenderHandlers) => void; +} + +export type RenderFunctionsRegistry = Registry; + +export interface Interpreter { + interpretAst(ast: Ast, context: Context, handlers: Handlers): Promise; +} + +type InterpreterGetter = () => Promise<{ interpreter: Interpreter }>; + +export interface ExpressionsServiceDependencies { + interpreter: { + renderersRegistry: RenderFunctionsRegistry; + getInterpreter: InterpreterGetter; + }; +} + +/** + * Expressions Service + * @internal + */ +export class ExpressionsService { + public setup({ + interpreter: { renderersRegistry, getInterpreter }, + }: ExpressionsServiceDependencies) { + const run = createRunFn( + renderersRegistry, + getInterpreter().then(({ interpreter }) => interpreter) + ); + + return { + /** + * **experimential** This API is experimential and might be removed in the future + * without notice + * + * Executes the given expression string or ast and renders the result into the + * given DOM element. + * + * + * @param expressionOrAst + * @param element + */ + run, + /** + * **experimential** This API is experimential and might be removed in the future + * without notice + * + * Component which executes and renders the given expression in a div element. + * The expression is re-executed on updating the props. + * + * This is a React bridge of the `run` method + * @param props + */ + ExpressionRenderer: createRenderer(run), + }; + } + + public stop() { + // nothing to do here yet + } +} + +/** @public */ +export type ExpressionsSetup = ReturnType; diff --git a/src/legacy/core_plugins/data/public/expressions/index.ts b/src/legacy/core_plugins/data/public/expressions/index.ts new file mode 100644 index 0000000000000..fceefce44f81f --- /dev/null +++ b/src/legacy/core_plugins/data/public/expressions/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { ExpressionsService, ExpressionsSetup } from './expressions_service'; +export { ExpressionRenderer, ExpressionRendererProps } from './expression_renderer'; +export { ExpressionRunner } from './expression_runner'; diff --git a/src/legacy/ui/public/apply_filters/apply_filters_popover.tsx b/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filters_popover.tsx similarity index 94% rename from src/legacy/ui/public/apply_filters/apply_filters_popover.tsx rename to src/legacy/core_plugins/data/public/filter/apply_filters/apply_filters_popover.tsx index 3b02eacdc6d25..e8a03473f1215 100644 --- a/src/legacy/ui/public/apply_filters/apply_filters_popover.tsx +++ b/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filters_popover.tsx @@ -82,7 +82,7 @@ export class ApplyFiltersPopover extends Component { @@ -93,13 +93,13 @@ export class ApplyFiltersPopover extends Component { diff --git a/src/legacy/ui/public/apply_filters/directive.html b/src/legacy/core_plugins/data/public/filter/apply_filters/directive.html similarity index 100% rename from src/legacy/ui/public/apply_filters/directive.html rename to src/legacy/core_plugins/data/public/filter/apply_filters/directive.html diff --git a/src/legacy/core_plugins/data/public/filter/apply_filters/directive.js b/src/legacy/core_plugins/data/public/filter/apply_filters/directive.js new file mode 100644 index 0000000000000..a16d17519eb73 --- /dev/null +++ b/src/legacy/core_plugins/data/public/filter/apply_filters/directive.js @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import 'ngreact'; +import { uiModules } from 'ui/modules'; +import template from './directive.html'; +import { ApplyFiltersPopover } from './apply_filters_popover'; +import { mapAndFlattenFilters } from 'ui/filter_manager/lib/map_and_flatten_filters'; +import { wrapInI18nContext } from 'ui/i18n'; + +const app = uiModules.get('app/data', ['react']); + +export function setupDirective() { + app.directive('applyFiltersPopoverComponent', (reactDirective) => { + return reactDirective(wrapInI18nContext(ApplyFiltersPopover)); + }); + + app.directive('applyFiltersPopover', (indexPatterns) => { + return { + template, + restrict: 'E', + scope: { + filters: '=', + onCancel: '=', + onSubmit: '=', + }, + link: function ($scope) { + $scope.state = {}; + + // Each time the new filters change we want to rebuild (not just re-render) the "apply filters" + // popover, because it has to reset its state whenever the new filters change. Setting a `key` + // property on the component accomplishes this due to how React handles the `key` property. + $scope.$watch('filters', filters => { + mapAndFlattenFilters(indexPatterns, filters).then(mappedFilters => { + $scope.state = { + filters: mappedFilters, + key: Date.now(), + }; + }); + }); + } + }; + }); +} diff --git a/src/legacy/core_plugins/data/public/filter/apply_filters/index.ts b/src/legacy/core_plugins/data/public/filter/apply_filters/index.ts new file mode 100644 index 0000000000000..36d2501e1d9fb --- /dev/null +++ b/src/legacy/core_plugins/data/public/filter/apply_filters/index.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { ApplyFiltersPopover } from './apply_filters_popover'; + +// @ts-ignore +export { setupDirective } from './directive'; diff --git a/src/legacy/ui/public/filter_bar/_global_filter_group.scss b/src/legacy/core_plugins/data/public/filter/filter_bar/_global_filter_group.scss similarity index 100% rename from src/legacy/ui/public/filter_bar/_global_filter_group.scss rename to src/legacy/core_plugins/data/public/filter/filter_bar/_global_filter_group.scss diff --git a/src/legacy/ui/public/filter_bar/_global_filter_item.scss b/src/legacy/core_plugins/data/public/filter/filter_bar/_global_filter_item.scss similarity index 100% rename from src/legacy/ui/public/filter_bar/_global_filter_item.scss rename to src/legacy/core_plugins/data/public/filter/filter_bar/_global_filter_item.scss diff --git a/src/legacy/ui/public/filter_bar/_index.scss b/src/legacy/core_plugins/data/public/filter/filter_bar/_index.scss similarity index 100% rename from src/legacy/ui/public/filter_bar/_index.scss rename to src/legacy/core_plugins/data/public/filter/filter_bar/_index.scss diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/directive.js b/src/legacy/core_plugins/data/public/filter/filter_bar/directive.js new file mode 100644 index 0000000000000..50559cca3ffaf --- /dev/null +++ b/src/legacy/core_plugins/data/public/filter/filter_bar/directive.js @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import 'ngreact'; +import { wrapInI18nContext } from 'ui/i18n'; +import { uiModules } from 'ui/modules'; +import { FilterBar } from './filter_bar'; + +const app = uiModules.get('app/kibana', ['react']); + +export function setupDirective() { + app.directive('filterBar', reactDirective => { + return reactDirective(wrapInI18nContext(FilterBar)); + }); +} diff --git a/src/legacy/ui/public/filter_bar/filter_bar.less b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.less similarity index 100% rename from src/legacy/ui/public/filter_bar/filter_bar.less rename to src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.less diff --git a/src/legacy/ui/public/filter_bar/filter_bar.tsx b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.tsx similarity index 99% rename from src/legacy/ui/public/filter_bar/filter_bar.tsx rename to src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.tsx index 1d16b8ab9f61f..af0933326a052 100644 --- a/src/legacy/ui/public/filter_bar/filter_bar.tsx +++ b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.tsx @@ -118,7 +118,7 @@ class FilterBarUI extends Component { +{' '} diff --git a/src/legacy/ui/public/filter_bar/filter_editor/generic_combo_box.tsx b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/generic_combo_box.tsx similarity index 100% rename from src/legacy/ui/public/filter_bar/filter_editor/generic_combo_box.tsx rename to src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/generic_combo_box.tsx diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/index.tsx b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/index.tsx new file mode 100644 index 0000000000000..6ca27c31bd357 --- /dev/null +++ b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/index.tsx @@ -0,0 +1,472 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + EuiButton, + EuiButtonEmpty, + // @ts-ignore + EuiCodeEditor, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiPopoverTitle, + EuiSpacer, + EuiSwitch, +} from '@elastic/eui'; +import { FieldFilter, Filter } from '@kbn/es-query'; +import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; +import { get } from 'lodash'; +import React, { Component } from 'react'; +import { Field, IndexPattern } from 'ui/index_patterns'; +import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box'; +import { + buildCustomFilter, + buildFilter, + getFieldFromFilter, + getFilterableFields, + getFilterParams, + getIndexPatternFromFilter, + getOperatorFromFilter, + getOperatorOptions, + getQueryDslFromFilter, + isFilterValid, +} from './lib/filter_editor_utils'; +import { Operator } from './lib/filter_operators'; +import { PhraseValueInput } from './phrase_value_input'; +import { PhrasesValuesInput } from './phrases_values_input'; +import { RangeValueInput } from './range_value_input'; + +interface Props { + filter: Filter; + indexPatterns: IndexPattern[]; + onSubmit: (filter: Filter) => void; + onCancel: () => void; + intl: InjectedIntl; +} + +interface State { + selectedIndexPattern?: IndexPattern; + selectedField?: Field; + selectedOperator?: Operator; + params: any; + useCustomLabel: boolean; + customLabel: string | null; + queryDsl: string; + isCustomEditorOpen: boolean; +} + +class FilterEditorUI extends Component { + public constructor(props: Props) { + super(props); + this.state = { + selectedIndexPattern: this.getIndexPatternFromFilter(), + selectedField: this.getFieldFromFilter(), + selectedOperator: this.getSelectedOperator(), + params: getFilterParams(props.filter), + useCustomLabel: props.filter.meta.alias !== null, + customLabel: props.filter.meta.alias, + queryDsl: JSON.stringify(getQueryDslFromFilter(props.filter), null, 2), + isCustomEditorOpen: this.isUnknownFilterType(), + }; + } + + public render() { + return ( +
+ + + + + + + + {this.state.isCustomEditorOpen ? ( + + ) : ( + + )} + + + + + +
+ + {this.renderIndexPatternInput()} + + {this.state.isCustomEditorOpen ? this.renderCustomEditor() : this.renderRegularEditor()} + + + + + + {this.state.useCustomLabel && ( +
+ + + + +
+ )} + + + + + + + + + + + + + + + + +
+
+
+ ); + } + + private renderIndexPatternInput() { + if (this.props.indexPatterns.length <= 1) { + return ''; + } + const { selectedIndexPattern } = this.state; + return ( + + + + indexPattern.title} + onChange={this.onIndexPatternChange} + singleSelection={{ asPlainText: true }} + isClearable={false} + data-test-subj="filterIndexPatternsSelect" + /> + + + + ); + } + + private renderRegularEditor() { + return ( +
+ + {this.renderFieldInput()} + {this.renderOperatorInput()} + + +
{this.renderParamsEditor()}
+
+ ); + } + + private renderFieldInput() { + const { selectedIndexPattern, selectedField } = this.state; + const fields = selectedIndexPattern ? getFilterableFields(selectedIndexPattern) : []; + return ( + + field.name} + onChange={this.onFieldChange} + singleSelection={{ asPlainText: true }} + isClearable={false} + data-test-subj="filterFieldSuggestionList" + /> + + ); + } + + private renderOperatorInput() { + const { selectedField, selectedOperator } = this.state; + const operators = selectedField ? getOperatorOptions(selectedField) : []; + return ( + + message} + onChange={this.onOperatorChange} + singleSelection={{ asPlainText: true }} + isClearable={false} + data-test-subj="filterOperatorList" + /> + + ); + } + + private renderCustomEditor() { + return ( + + + + ); + } + + private renderParamsEditor() { + const indexPattern = this.state.selectedIndexPattern; + if (!indexPattern || !this.state.selectedOperator) { + return ''; + } + + switch (this.state.selectedOperator.type) { + case 'exists': + return ''; + case 'phrase': + return ( + + ); + case 'phrases': + return ( + + ); + case 'range': + return ( + + ); + } + } + + private toggleCustomEditor = () => { + const isCustomEditorOpen = !this.state.isCustomEditorOpen; + this.setState({ isCustomEditorOpen }); + }; + + private isUnknownFilterType() { + const { type } = this.props.filter.meta; + return !!type && !['phrase', 'phrases', 'range', 'exists'].includes(type); + } + + private getIndexPatternFromFilter() { + return getIndexPatternFromFilter(this.props.filter, this.props.indexPatterns); + } + + private getFieldFromFilter() { + const indexPattern = this.getIndexPatternFromFilter(); + return indexPattern && getFieldFromFilter(this.props.filter as FieldFilter, indexPattern); + } + + private getSelectedOperator() { + return getOperatorFromFilter(this.props.filter); + } + + private isFilterValid() { + const { + isCustomEditorOpen, + queryDsl, + selectedIndexPattern: indexPattern, + selectedField: field, + selectedOperator: operator, + params, + } = this.state; + + if (isCustomEditorOpen) { + try { + return Boolean(JSON.parse(queryDsl)); + } catch (e) { + return false; + } + } + + return isFilterValid(indexPattern, field, operator, params); + } + + private onIndexPatternChange = ([selectedIndexPattern]: IndexPattern[]) => { + const selectedField = undefined; + const selectedOperator = undefined; + const params = undefined; + this.setState({ selectedIndexPattern, selectedField, selectedOperator, params }); + }; + + private onFieldChange = ([selectedField]: Field[]) => { + const selectedOperator = undefined; + const params = undefined; + this.setState({ selectedField, selectedOperator, params }); + }; + + private onOperatorChange = ([selectedOperator]: Operator[]) => { + // Only reset params when the operator type changes + const params = + get(this.state.selectedOperator, 'type') === get(selectedOperator, 'type') + ? this.state.params + : undefined; + this.setState({ selectedOperator, params }); + }; + + private onCustomLabelSwitchChange = (event: React.ChangeEvent) => { + const useCustomLabel = event.target.checked; + const customLabel = event.target.checked ? '' : null; + this.setState({ useCustomLabel, customLabel }); + }; + + private onCustomLabelChange = (event: React.ChangeEvent) => { + const customLabel = event.target.value; + this.setState({ customLabel }); + }; + + private onParamsChange = (params: any) => { + this.setState({ params }); + }; + + private onQueryDslChange = (queryDsl: string) => { + this.setState({ queryDsl }); + }; + + private onSubmit = () => { + const { + selectedIndexPattern: indexPattern, + selectedField: field, + selectedOperator: operator, + params, + useCustomLabel, + customLabel, + isCustomEditorOpen, + queryDsl, + } = this.state; + + const { store } = this.props.filter.$state; + const alias = useCustomLabel ? customLabel : null; + + if (isCustomEditorOpen) { + const { index, disabled, negate } = this.props.filter.meta; + const newIndex = index || this.props.indexPatterns[0].id; + const body = JSON.parse(queryDsl); + const filter = buildCustomFilter(newIndex, body, disabled, negate, alias, store); + this.props.onSubmit(filter); + } else if (indexPattern && field && operator) { + const filter = buildFilter(indexPattern, field, operator, params, alias, store); + this.props.onSubmit(filter); + } + }; +} + +function IndexPatternComboBox(props: GenericComboBoxProps) { + return GenericComboBox(props); +} + +function FieldComboBox(props: GenericComboBoxProps) { + return GenericComboBox(props); +} + +function OperatorComboBox(props: GenericComboBoxProps) { + return GenericComboBox(props); +} + +export const FilterEditor = injectI18n(FilterEditorUI); diff --git a/src/legacy/ui/public/filter_bar/filter_editor/lib/filter_editor_utils.test.ts b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.test.ts similarity index 100% rename from src/legacy/ui/public/filter_bar/filter_editor/lib/filter_editor_utils.test.ts rename to src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.test.ts diff --git a/src/legacy/ui/public/filter_bar/filter_editor/lib/filter_editor_utils.ts b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.ts similarity index 100% rename from src/legacy/ui/public/filter_bar/filter_editor/lib/filter_editor_utils.ts rename to src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.ts diff --git a/src/legacy/ui/public/filter_bar/filter_editor/lib/filter_operators.ts b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_operators.ts similarity index 79% rename from src/legacy/ui/public/filter_bar/filter_editor/lib/filter_operators.ts rename to src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_operators.ts index b97f4a73901b6..469f5355df106 100644 --- a/src/legacy/ui/public/filter_bar/filter_editor/lib/filter_operators.ts +++ b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_operators.ts @@ -27,7 +27,7 @@ export interface Operator { } export const isOperator = { - message: i18n.translate('common.ui.filterEditor.isOperatorOptionLabel', { + message: i18n.translate('data.filter.filterEditor.isOperatorOptionLabel', { defaultMessage: 'is', }), type: 'phrase', @@ -35,7 +35,7 @@ export const isOperator = { }; export const isNotOperator = { - message: i18n.translate('common.ui.filterEditor.isNotOperatorOptionLabel', { + message: i18n.translate('data.filter.filterEditor.isNotOperatorOptionLabel', { defaultMessage: 'is not', }), type: 'phrase', @@ -43,7 +43,7 @@ export const isNotOperator = { }; export const isOneOfOperator = { - message: i18n.translate('common.ui.filterEditor.isOneOfOperatorOptionLabel', { + message: i18n.translate('data.filter.filterEditor.isOneOfOperatorOptionLabel', { defaultMessage: 'is one of', }), type: 'phrases', @@ -52,7 +52,7 @@ export const isOneOfOperator = { }; export const isNotOneOfOperator = { - message: i18n.translate('common.ui.filterEditor.isNotOneOfOperatorOptionLabel', { + message: i18n.translate('data.filter.filterEditor.isNotOneOfOperatorOptionLabel', { defaultMessage: 'is not one of', }), type: 'phrases', @@ -61,7 +61,7 @@ export const isNotOneOfOperator = { }; export const isBetweenOperator = { - message: i18n.translate('common.ui.filterEditor.isBetweenOperatorOptionLabel', { + message: i18n.translate('data.filter.filterEditor.isBetweenOperatorOptionLabel', { defaultMessage: 'is between', }), type: 'range', @@ -70,7 +70,7 @@ export const isBetweenOperator = { }; export const isNotBetweenOperator = { - message: i18n.translate('common.ui.filterEditor.isNotBetweenOperatorOptionLabel', { + message: i18n.translate('data.filter.filterEditor.isNotBetweenOperatorOptionLabel', { defaultMessage: 'is not between', }), type: 'range', @@ -79,7 +79,7 @@ export const isNotBetweenOperator = { }; export const existsOperator = { - message: i18n.translate('common.ui.filterEditor.existsOperatorOptionLabel', { + message: i18n.translate('data.filter.filterEditor.existsOperatorOptionLabel', { defaultMessage: 'exists', }), type: 'exists', @@ -87,7 +87,7 @@ export const existsOperator = { }; export const doesNotExistOperator = { - message: i18n.translate('common.ui.filterEditor.doesNotExistOperatorOptionLabel', { + message: i18n.translate('data.filter.filterEditor.doesNotExistOperatorOptionLabel', { defaultMessage: 'does not exist', }), type: 'exists', diff --git a/src/legacy/ui/public/filter_bar/filter_editor/lib/fixtures/exists_filter.ts b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/fixtures/exists_filter.ts similarity index 100% rename from src/legacy/ui/public/filter_bar/filter_editor/lib/fixtures/exists_filter.ts rename to src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/fixtures/exists_filter.ts diff --git a/src/legacy/ui/public/filter_bar/filter_editor/lib/fixtures/phrase_filter.ts b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/fixtures/phrase_filter.ts similarity index 100% rename from src/legacy/ui/public/filter_bar/filter_editor/lib/fixtures/phrase_filter.ts rename to src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/fixtures/phrase_filter.ts diff --git a/src/legacy/ui/public/filter_bar/filter_editor/lib/fixtures/phrases_filter.ts b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/fixtures/phrases_filter.ts similarity index 100% rename from src/legacy/ui/public/filter_bar/filter_editor/lib/fixtures/phrases_filter.ts rename to src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/fixtures/phrases_filter.ts diff --git a/src/legacy/ui/public/filter_bar/filter_editor/lib/fixtures/range_filter.ts b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/fixtures/range_filter.ts similarity index 100% rename from src/legacy/ui/public/filter_bar/filter_editor/lib/fixtures/range_filter.ts rename to src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/fixtures/range_filter.ts diff --git a/src/legacy/ui/public/filter_bar/filter_editor/phrase_suggestor.tsx b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrase_suggestor.tsx similarity index 100% rename from src/legacy/ui/public/filter_bar/filter_editor/phrase_suggestor.tsx rename to src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrase_suggestor.tsx diff --git a/src/legacy/ui/public/filter_bar/filter_editor/phrase_value_input.tsx b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrase_value_input.tsx similarity index 93% rename from src/legacy/ui/public/filter_bar/filter_editor/phrase_value_input.tsx rename to src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrase_value_input.tsx index 06b826d681d0c..f33ce3d7486fc 100644 --- a/src/legacy/ui/public/filter_bar/filter_editor/phrase_value_input.tsx +++ b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrase_value_input.tsx @@ -36,7 +36,7 @@ class PhraseValueInputUI extends PhraseSuggestor { return ( @@ -45,7 +45,7 @@ class PhraseValueInputUI extends PhraseSuggestor { ) : ( { return ( { return ( { @@ -63,7 +63,7 @@ class RangeValueInputUI extends Component { value={this.props.value ? this.props.value.from : undefined} onChange={this.onFromChange} placeholder={this.props.intl.formatMessage({ - id: 'common.ui.filterEditor.rangeStartInputPlaceholder', + id: 'data.filter.filterEditor.rangeStartInputPlaceholder', defaultMessage: 'Start of the range', })} /> @@ -72,7 +72,7 @@ class RangeValueInputUI extends Component { @@ -81,7 +81,7 @@ class RangeValueInputUI extends Component { value={this.props.value ? this.props.value.to : undefined} onChange={this.onToChange} placeholder={this.props.intl.formatMessage({ - id: 'common.ui.filterEditor.rangeEndInputPlaceholder', + id: 'data.filter.filterEditor.rangeEndInputPlaceholder', defaultMessage: 'End of the range', })} /> @@ -91,7 +91,7 @@ class RangeValueInputUI extends Component { {type === 'date' ? ( {' '} diff --git a/src/legacy/ui/public/filter_bar/filter_editor/value_input_type.tsx b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/value_input_type.tsx similarity index 96% rename from src/legacy/ui/public/filter_bar/filter_editor/value_input_type.tsx rename to src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/value_input_type.tsx index 0a573c88eae70..8792bad88ad16 100644 --- a/src/legacy/ui/public/filter_bar/filter_editor/value_input_type.tsx +++ b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/value_input_type.tsx @@ -82,14 +82,14 @@ class ValueInputTypeUI extends Component { { value: 'true', text: this.props.intl.formatMessage({ - id: 'common.ui.filterEditor.trueOptionLabel', + id: 'data.filter.filterEditor.trueOptionLabel', defaultMessage: 'true', }), }, { value: 'false', text: this.props.intl.formatMessage({ - id: 'common.ui.filterEditor.falseOptionLabel', + id: 'data.filter.filterEditor.falseOptionLabel', defaultMessage: 'false', }), }, diff --git a/src/legacy/ui/public/filter_bar/filter_item.tsx b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_item.tsx similarity index 91% rename from src/legacy/ui/public/filter_bar/filter_item.tsx rename to src/legacy/core_plugins/data/public/filter/filter_bar/filter_item.tsx index 4fda77d44a91f..d4fd894b93b23 100644 --- a/src/legacy/ui/public/filter_bar/filter_item.tsx +++ b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_item.tsx @@ -88,11 +88,11 @@ class FilterItemUI extends Component { { name: isFilterPinned(filter) ? this.props.intl.formatMessage({ - id: 'common.ui.filterBar.unpinFilterButtonLabel', + id: 'data.filter.filterBar.unpinFilterButtonLabel', defaultMessage: 'Unpin', }) : this.props.intl.formatMessage({ - id: 'common.ui.filterBar.pinFilterButtonLabel', + id: 'data.filter.filterBar.pinFilterButtonLabel', defaultMessage: 'Pin across all apps', }), icon: 'pin', @@ -104,7 +104,7 @@ class FilterItemUI extends Component { }, { name: this.props.intl.formatMessage({ - id: 'common.ui.filterBar.editFilterButtonLabel', + id: 'data.filter.filterBar.editFilterButtonLabel', defaultMessage: 'Edit filter', }), icon: 'pencil', @@ -114,11 +114,11 @@ class FilterItemUI extends Component { { name: negate ? this.props.intl.formatMessage({ - id: 'common.ui.filterBar.includeFilterButtonLabel', + id: 'data.filter.filterBar.includeFilterButtonLabel', defaultMessage: 'Include results', }) : this.props.intl.formatMessage({ - id: 'common.ui.filterBar.excludeFilterButtonLabel', + id: 'data.filter.filterBar.excludeFilterButtonLabel', defaultMessage: 'Exclude results', }), icon: negate ? 'plusInCircle' : 'minusInCircle', @@ -131,11 +131,11 @@ class FilterItemUI extends Component { { name: disabled ? this.props.intl.formatMessage({ - id: 'common.ui.filterBar.enableFilterButtonLabel', + id: 'data.filter.filterBar.enableFilterButtonLabel', defaultMessage: 'Re-enable', }) : this.props.intl.formatMessage({ - id: 'common.ui.filterBar.disableFilterButtonLabel', + id: 'data.filter.filterBar.disableFilterButtonLabel', defaultMessage: 'Temporarily disable', }), icon: `${disabled ? 'eye' : 'eyeClosed'}`, @@ -147,7 +147,7 @@ class FilterItemUI extends Component { }, { name: this.props.intl.formatMessage({ - id: 'common.ui.filterBar.deleteFilterButtonLabel', + id: 'data.filter.filterBar.deleteFilterButtonLabel', defaultMessage: 'Delete', }), icon: 'trash', diff --git a/src/legacy/ui/public/filter_bar/filter_options.tsx b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_options.tsx similarity index 88% rename from src/legacy/ui/public/filter_bar/filter_options.tsx rename to src/legacy/core_plugins/data/public/filter/filter_bar/filter_options.tsx index 1f5a3208dadfa..f1d4519b43d19 100644 --- a/src/legacy/ui/public/filter_bar/filter_options.tsx +++ b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_options.tsx @@ -58,7 +58,7 @@ class FilterOptionsUI extends Component { items: [ { name: this.props.intl.formatMessage({ - id: 'common.ui.searchBar.enableAllFiltersButtonLabel', + id: 'data.filter.options.enableAllFiltersButtonLabel', defaultMessage: 'Enable all', }), icon: 'eye', @@ -70,7 +70,7 @@ class FilterOptionsUI extends Component { }, { name: this.props.intl.formatMessage({ - id: 'common.ui.searchBar.disableAllFiltersButtonLabel', + id: 'data.filter.options.disableAllFiltersButtonLabel', defaultMessage: 'Disable all', }), icon: 'eyeClosed', @@ -82,7 +82,7 @@ class FilterOptionsUI extends Component { }, { name: this.props.intl.formatMessage({ - id: 'common.ui.searchBar.pinAllFiltersButtonLabel', + id: 'data.filter.options.pinAllFiltersButtonLabel', defaultMessage: 'Pin all', }), icon: 'pin', @@ -94,7 +94,7 @@ class FilterOptionsUI extends Component { }, { name: this.props.intl.formatMessage({ - id: 'common.ui.searchBar.unpinAllFiltersButtonLabel', + id: 'data.filter.options.unpinAllFiltersButtonLabel', defaultMessage: 'Unpin all', }), icon: 'pin', @@ -106,7 +106,7 @@ class FilterOptionsUI extends Component { }, { name: this.props.intl.formatMessage({ - id: 'common.ui.searchBar.invertNegatedFiltersButtonLabel', + id: 'data.filter.options.invertNegatedFiltersButtonLabel', defaultMessage: 'Invert inclusion', }), icon: 'invert', @@ -118,7 +118,7 @@ class FilterOptionsUI extends Component { }, { name: this.props.intl.formatMessage({ - id: 'common.ui.searchBar.invertDisabledFiltersButtonLabel', + id: 'data.filter.options.invertDisabledFiltersButtonLabel', defaultMessage: 'Invert enabled/disabled', }), icon: 'eye', @@ -130,7 +130,7 @@ class FilterOptionsUI extends Component { }, { name: this.props.intl.formatMessage({ - id: 'common.ui.searchBar.deleteAllFiltersButtonLabel', + id: 'data.filter.options.deleteAllFiltersButtonLabel', defaultMessage: 'Remove all', }), icon: 'trash', @@ -155,11 +155,11 @@ class FilterOptionsUI extends Component { color="text" iconType="gear" aria-label={this.props.intl.formatMessage({ - id: 'common.ui.searchBar.changeAllFiltersButtonLabel', + id: 'data.filter.options.changeAllFiltersButtonLabel', defaultMessage: 'Change all filters', })} title={this.props.intl.formatMessage({ - id: 'common.ui.searchBar.changeAllFiltersButtonLabel', + id: 'data.filter.options.changeAllFiltersButtonLabel', defaultMessage: 'Change all filters', })} data-test-subj="showFilterActions" @@ -171,7 +171,7 @@ class FilterOptionsUI extends Component { > diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_view/index.tsx b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_view/index.tsx new file mode 100644 index 0000000000000..b905bd3a732ed --- /dev/null +++ b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_view/index.tsx @@ -0,0 +1,103 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { EuiBadge } from '@elastic/eui'; +import { Filter, isFilterPinned } from '@kbn/es-query'; +import { i18n } from '@kbn/i18n'; +import React, { SFC } from 'react'; +import { existsOperator, isOneOfOperator } from '../filter_editor/lib/filter_operators'; + +interface Props { + filter: Filter; + [propName: string]: any; +} + +export const FilterView: SFC = ({ filter, ...rest }: Props) => { + let title = `Filter: ${getFilterDisplayText(filter)}. ${i18n.translate( + 'data.filter.filterBar.moreFilterActionsMessage', + { + defaultMessage: 'Select for more filter actions.', + } + )}`; + + if (isFilterPinned(filter)) { + title = `${i18n.translate('data.filter.filterBar.pinnedFilterPrefix', { + defaultMessage: 'Pinned', + })} ${title}`; + } + if (filter.meta.disabled) { + title = `${i18n.translate('data.filter.filterBar.disabledFilterPrefix', { + defaultMessage: 'Disabled', + })} ${title}`; + } + + return ( + + {getFilterDisplayText(filter)} + + ); +}; + +export function getFilterDisplayText(filter: Filter) { + const prefix = filter.meta.negate + ? ` ${i18n.translate('data.filter.filterBar.negatedFilterPrefix', { + defaultMessage: 'NOT ', + })}` + : ''; + + if (filter.meta.alias !== null) { + return `${prefix}${filter.meta.alias}`; + } + + switch (filter.meta.type) { + case 'exists': + return `${prefix}${filter.meta.key} ${existsOperator.message}`; + case 'geo_bounding_box': + return `${prefix}${filter.meta.key}: ${filter.meta.value}`; + case 'geo_polygon': + return `${prefix}${filter.meta.key}: ${filter.meta.value}`; + case 'phrase': + return `${prefix}${filter.meta.key}: ${filter.meta.value}`; + case 'phrases': + return `${prefix}${filter.meta.key} ${isOneOfOperator.message} ${filter.meta.value}`; + case 'query_string': + return `${prefix}${filter.meta.value}`; + case 'range': + return `${prefix}${filter.meta.key}: ${filter.meta.value}`; + default: + return `${prefix}${JSON.stringify(filter.query)}`; + } +} diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/index.ts b/src/legacy/core_plugins/data/public/filter/filter_bar/index.ts new file mode 100644 index 0000000000000..d0786734e42dd --- /dev/null +++ b/src/legacy/core_plugins/data/public/filter/filter_bar/index.ts @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import './directive'; + +export { FilterBar } from './filter_bar'; + +// @ts-ignore +export { setupDirective } from './directive'; diff --git a/src/legacy/core_plugins/data/public/filter/filter_service.ts b/src/legacy/core_plugins/data/public/filter/filter_service.ts new file mode 100644 index 0000000000000..0e1870ea598ac --- /dev/null +++ b/src/legacy/core_plugins/data/public/filter/filter_service.ts @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { once } from 'lodash'; +import { FilterBar, setupDirective as setupFilterBarDirective } from './filter_bar'; +import { ApplyFiltersPopover, setupDirective as setupApplyFiltersDirective } from './apply_filters'; + +/** + * FilterSearch Service + * @internal + */ +export class FilterService { + public setup() { + return { + ui: { + ApplyFiltersPopover, + FilterBar, + }, + loadLegacyDirectives: once(() => { + setupFilterBarDirective(); + setupApplyFiltersDirective(); + }), + }; + } + + public stop() { + // nothing to do here yet + } +} + +/** @public */ +export type FilterSetup = ReturnType; diff --git a/src/legacy/core_plugins/data/public/filter/index.tsx b/src/legacy/core_plugins/data/public/filter/index.tsx new file mode 100644 index 0000000000000..997dae3854b4c --- /dev/null +++ b/src/legacy/core_plugins/data/public/filter/index.tsx @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { FilterService, FilterSetup } from './filter_service'; diff --git a/src/legacy/core_plugins/data/public/index.scss b/src/legacy/core_plugins/data/public/index.scss index 1b2b614a1d647..993e52665defa 100644 --- a/src/legacy/core_plugins/data/public/index.scss +++ b/src/legacy/core_plugins/data/public/index.scss @@ -1,4 +1,6 @@ @import 'src/legacy/ui/public/styles/styling_constants'; -@import './query_bar/index'; +@import './query/query_bar/index'; + +@import './filter/filter_bar/index'; diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index dc3092fd286d6..a00e01804723c 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -17,33 +17,57 @@ * under the License. */ -import { SearchBarService } from './search_bar'; -import { QueryBarService } from './query_bar'; +// TODO these are imports from the old plugin world. +// Once the new platform is ready, they can get removed +// and handled by the platform itself in the setup method +// of the ExpressionExectorService +// @ts-ignore +import { getInterpreter } from 'plugins/interpreter/interpreter'; +// @ts-ignore +import { renderersRegistry } from 'plugins/interpreter/registries'; +import { ExpressionsService, ExpressionsSetup } from './expressions'; +import { SearchService, SearchSetup } from './search'; +import { QueryService, QuerySetup } from './query'; +import { FilterService, FilterSetup } from './filter'; import { IndexPatternsService, IndexPatternsSetup } from './index_patterns'; class DataPlugin { + // Exposed services, sorted alphabetically + private readonly expressions: ExpressionsService; + private readonly filter: FilterService; private readonly indexPatterns: IndexPatternsService; - private readonly searchBar: SearchBarService; - private readonly queryBar: QueryBarService; + private readonly search: SearchService; + private readonly query: QueryService; constructor() { this.indexPatterns = new IndexPatternsService(); - this.queryBar = new QueryBarService(); - this.searchBar = new SearchBarService(); + this.filter = new FilterService(); + this.query = new QueryService(); + this.search = new SearchService(); + this.expressions = new ExpressionsService(); } - public setup() { + public setup(): DataSetup { return { + expressions: this.expressions.setup({ + interpreter: { + getInterpreter, + renderersRegistry, + }, + }), indexPatterns: this.indexPatterns.setup(), - search: this.searchBar.setup(), - query: this.queryBar.setup(), + filter: this.filter.setup(), + search: this.search.setup(), + query: this.query.setup(), }; } public stop() { + this.expressions.stop(); this.indexPatterns.stop(); - this.searchBar.stop(); - this.queryBar.stop(); + this.filter.stop(); + this.search.stop(); + this.query.stop(); } } @@ -56,8 +80,16 @@ export const data = new DataPlugin().setup(); /** @public */ export interface DataSetup { + expressions: ExpressionsSetup; indexPatterns: IndexPatternsSetup; + filter: FilterSetup; + search: SearchSetup; + query: QuerySetup; } +/** @public types */ +export { ExpressionRenderer, ExpressionRendererProps, ExpressionRunner } from './expressions'; + /** @public types */ export { IndexPattern, StaticIndexPattern, StaticIndexPatternField, Field } from './index_patterns'; +export { Query } from './query'; diff --git a/src/legacy/core_plugins/data/public/query/index.ts b/src/legacy/core_plugins/data/public/query/index.ts new file mode 100644 index 0000000000000..9a4d1b4f50c10 --- /dev/null +++ b/src/legacy/core_plugins/data/public/query/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { QueryService, QuerySetup, Query } from './query_service'; diff --git a/src/legacy/core_plugins/data/public/query_bar/_index.scss b/src/legacy/core_plugins/data/public/query/query_bar/_index.scss similarity index 100% rename from src/legacy/core_plugins/data/public/query_bar/_index.scss rename to src/legacy/core_plugins/data/public/query/query_bar/_index.scss diff --git a/src/legacy/core_plugins/data/public/query_bar/components/__snapshots__/language_switcher.test.tsx.snap b/src/legacy/core_plugins/data/public/query/query_bar/components/__snapshots__/language_switcher.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/data/public/query_bar/components/__snapshots__/language_switcher.test.tsx.snap rename to src/legacy/core_plugins/data/public/query/query_bar/components/__snapshots__/language_switcher.test.tsx.snap diff --git a/src/legacy/core_plugins/data/public/query_bar/components/__snapshots__/query_bar.test.tsx.snap b/src/legacy/core_plugins/data/public/query/query_bar/components/__snapshots__/query_bar.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/data/public/query_bar/components/__snapshots__/query_bar.test.tsx.snap rename to src/legacy/core_plugins/data/public/query/query_bar/components/__snapshots__/query_bar.test.tsx.snap diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/__snapshots__/query_bar_input.test.tsx.snap b/src/legacy/core_plugins/data/public/query/query_bar/components/__snapshots__/query_bar_input.test.tsx.snap new file mode 100644 index 0000000000000..130e303fda58a --- /dev/null +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/__snapshots__/query_bar_input.test.tsx.snap @@ -0,0 +1,1040 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`QueryBarInput Should disable autoFocus on EuiFieldText when disableAutoFocus prop is true 1`] = ` + + +
+
+
+ + } + aria-activedescendant="" + aria-autocomplete="list" + aria-controls="kbnTypeahead__items" + aria-label="You are on search box of Another Screen page. Start typing to search and filter the discover" + autoComplete="off" + autoFocus={false} + compressed={false} + data-test-subj="queryInput" + fullWidth={true} + inputRef={[Function]} + isLoading={false} + onChange={[Function]} + onClick={[Function]} + onKeyDown={[Function]} + onKeyUp={[Function]} + placeholder="Search" + role="textbox" + spellCheck={false} + type="text" + value="response:200" + > + + } + compressed={false} + fullWidth={true} + isLoading={false} + > +
+
+ + + + +
+ + + + + } + className="eui-displayBlock" + closePopover={[Function]} + hasArrow={true} + id="popover" + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + withTitle={true} + > + +
+
+ + + +
+
+
+
+
+
+
+
+
+
+ +
+
+
+`; + +exports[`QueryBarInput Should pass the query language to the language switcher 1`] = ` + + +
+
+
+ + } + aria-activedescendant="" + aria-autocomplete="list" + aria-controls="kbnTypeahead__items" + aria-label="You are on search box of Another Screen page. Start typing to search and filter the discover" + autoComplete="off" + autoFocus={true} + compressed={false} + data-test-subj="queryInput" + fullWidth={true} + inputRef={[Function]} + isLoading={false} + onChange={[Function]} + onClick={[Function]} + onKeyDown={[Function]} + onKeyUp={[Function]} + placeholder="Search" + role="textbox" + spellCheck={false} + type="text" + value="response:200" + > + + } + compressed={false} + fullWidth={true} + isLoading={false} + > +
+
+ + + + +
+ + + + + } + className="eui-displayBlock" + closePopover={[Function]} + hasArrow={true} + id="popover" + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + withTitle={true} + > + +
+
+ + + +
+
+
+
+
+
+
+
+
+
+ +
+
+
+`; + +exports[`QueryBarInput Should render the given query 1`] = ` + + +
+
+
+ + } + aria-activedescendant="" + aria-autocomplete="list" + aria-controls="kbnTypeahead__items" + aria-label="You are on search box of Another Screen page. Start typing to search and filter the discover" + autoComplete="off" + autoFocus={true} + compressed={false} + data-test-subj="queryInput" + fullWidth={true} + inputRef={[Function]} + isLoading={false} + onChange={[Function]} + onClick={[Function]} + onKeyDown={[Function]} + onKeyUp={[Function]} + placeholder="Search" + role="textbox" + spellCheck={false} + type="text" + value="response:200" + > + + } + compressed={false} + fullWidth={true} + isLoading={false} + > +
+
+ + + + +
+ + + + + } + className="eui-displayBlock" + closePopover={[Function]} + hasArrow={true} + id="popover" + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + withTitle={true} + > + +
+
+ + + +
+
+
+
+
+
+
+
+
+
+ +
+
+
+`; diff --git a/src/legacy/core_plugins/data/public/query_bar/components/_index.scss b/src/legacy/core_plugins/data/public/query/query_bar/components/_index.scss similarity index 100% rename from src/legacy/core_plugins/data/public/query_bar/components/_index.scss rename to src/legacy/core_plugins/data/public/query/query_bar/components/_index.scss diff --git a/src/legacy/core_plugins/data/public/query_bar/components/_query_bar.scss b/src/legacy/core_plugins/data/public/query/query_bar/components/_query_bar.scss similarity index 100% rename from src/legacy/core_plugins/data/public/query_bar/components/_query_bar.scss rename to src/legacy/core_plugins/data/public/query/query_bar/components/_query_bar.scss diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/index.ts b/src/legacy/core_plugins/data/public/query/query_bar/components/index.ts new file mode 100644 index 0000000000000..12b1f254895c3 --- /dev/null +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { QueryBar } from './query_bar'; +export { QueryBarInput } from './query_bar_input'; diff --git a/src/legacy/core_plugins/data/public/query_bar/components/language_switcher.test.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/language_switcher.test.tsx similarity index 100% rename from src/legacy/core_plugins/data/public/query_bar/components/language_switcher.test.tsx rename to src/legacy/core_plugins/data/public/query/query_bar/components/language_switcher.test.tsx diff --git a/src/legacy/core_plugins/data/public/query_bar/components/language_switcher.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/language_switcher.tsx similarity index 97% rename from src/legacy/core_plugins/data/public/query_bar/components/language_switcher.tsx rename to src/legacy/core_plugins/data/public/query/query_bar/components/language_switcher.tsx index 0c12f6db3a5cb..8806b5b648962 100644 --- a/src/legacy/core_plugins/data/public/query_bar/components/language_switcher.tsx +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/language_switcher.tsx @@ -27,6 +27,7 @@ import { EuiSpacer, EuiSwitch, EuiText, + PopoverAnchorPosition, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component } from 'react'; @@ -41,6 +42,7 @@ interface State { interface Props { language: string; onSelectLanguage: (newLanguage: string) => void; + anchorPosition?: PopoverAnchorPosition; } export class QueryLanguageSwitcher extends Component { @@ -73,7 +75,7 @@ export class QueryLanguageSwitcher extends Component { id="popover" className="eui-displayBlock" ownFocus - anchorPosition="downRight" + anchorPosition={this.props.anchorPosition || 'downRight'} button={button} isOpen={this.state.isPopoverOpen} closePopover={this.closePopover} diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar.test.mocks.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar.test.mocks.tsx new file mode 100644 index 0000000000000..585fad0e058b7 --- /dev/null +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar.test.mocks.tsx @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + fatalErrorsServiceMock, + notificationServiceMock, +} from '../../../../../../../core/public/mocks'; + +jest.doMock('ui/new_platform', () => ({ + npSetup: { + core: { + fatalErrors: fatalErrorsServiceMock.createSetupContract(), + notifications: notificationServiceMock.createSetupContract(), + }, + }, +})); diff --git a/src/legacy/core_plugins/data/public/query_bar/components/query_bar.test.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar.test.tsx similarity index 98% rename from src/legacy/core_plugins/data/public/query_bar/components/query_bar.test.tsx rename to src/legacy/core_plugins/data/public/query/query_bar/components/query_bar.test.tsx index 04352bd4c6a62..2be00deb16d9e 100644 --- a/src/legacy/core_plugins/data/public/query_bar/components/query_bar.test.tsx +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar.test.tsx @@ -21,6 +21,7 @@ import { mockPersistedLogFactory } from './query_bar_input.test.mocks'; import React from 'react'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import './query_bar.test.mocks'; import { QueryBar } from './query_bar'; const noop = () => { diff --git a/src/legacy/core_plugins/data/public/query_bar/components/query_bar.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar.tsx similarity index 99% rename from src/legacy/core_plugins/data/public/query_bar/components/query_bar.tsx rename to src/legacy/core_plugins/data/public/query/query_bar/components/query_bar.tsx index 573124a6302b1..e1df6286bd120 100644 --- a/src/legacy/core_plugins/data/public/query_bar/components/query_bar.tsx +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar.tsx @@ -40,14 +40,10 @@ import { PersistedLog } from 'ui/persisted_log'; import { QueryBarInput } from './query_bar_input'; import { getQueryLog } from '../lib/get_query_log'; +import { Query } from '../index'; const config = chrome.getUiSettingsClient(); -interface Query { - query: string; - language: string; -} - interface DateRange { from: string; to: string; @@ -59,7 +55,7 @@ interface Props { disableAutoFocus?: boolean; appName: string; screenTitle: string; - indexPatterns: IndexPattern[]; + indexPatterns: Array; store: Storage; intl: InjectedIntl; prepend?: any; @@ -338,6 +334,7 @@ export class QueryBarUI extends Component { const { query, language } = this.state.query; if ( language === 'kuery' && + typeof query === 'string' && !store.get('kibana.luceneSyntaxWarningOptOut') && doesKueryExpressionHaveLuceneSyntaxError(query) ) { diff --git a/src/legacy/core_plugins/data/public/query_bar/components/query_bar_input.test.mocks.ts b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.test.mocks.ts similarity index 82% rename from src/legacy/core_plugins/data/public/query_bar/components/query_bar_input.test.mocks.ts rename to src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.test.mocks.ts index 0f0d5e1591b17..01f8b193ae396 100644 --- a/src/legacy/core_plugins/data/public/query_bar/components/query_bar_input.test.mocks.ts +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.test.mocks.ts @@ -18,7 +18,22 @@ */ import { createKfetch } from 'ui/kfetch/kfetch'; -import { setup } from '../../../../../../test_utils/public/kfetch_test_setup'; +import { setup } from 'test_utils/http_test_setup'; + +const mockIndexPattern = { + id: '1234', + title: 'logstash-*', + fields: [ + { + name: 'response', + type: 'number', + esTypes: ['integer'], + aggregatable: true, + filterable: true, + searchable: true, + }, + ], +}; const mockChromeFactory = jest.fn(() => { return { @@ -52,6 +67,10 @@ const mockAutocompleteProvider = jest.fn(() => mockGetAutocompleteSuggestions); export const mockGetAutocompleteProvider = jest.fn(() => mockAutocompleteProvider); const mockKfetch = jest.fn(() => createKfetch(setup().http)); +export const mockFetchIndexPatterns = jest + .fn() + .mockReturnValue(Promise.resolve([mockIndexPattern])); + jest.mock('ui/chrome', () => mockChromeFactory()); jest.mock('ui/kfetch', () => ({ kfetch: () => {}, @@ -68,10 +87,13 @@ jest.mock('ui/autocomplete_providers', () => ({ getAutocompleteProvider: mockGetAutocompleteProvider, })); jest.mock('ui/kfetch', () => ({ - __newPlatformSetup__: jest.fn(), kfetch: mockKfetch, })); +jest.mock('../lib/fetch_index_patterns', () => ({ + fetchIndexPatterns: mockFetchIndexPatterns, +})); + import _ from 'lodash'; // Using doMock to avoid hoisting so that I can override only the debounce method in lodash jest.doMock('lodash', () => ({ diff --git a/src/legacy/core_plugins/data/public/query_bar/components/query_bar_input.test.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.test.tsx similarity index 95% rename from src/legacy/core_plugins/data/public/query_bar/components/query_bar_input.test.tsx rename to src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.test.tsx index bcd007d4a601e..d51dda1e4f5d3 100644 --- a/src/legacy/core_plugins/data/public/query_bar/components/query_bar_input.test.tsx +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.test.tsx @@ -18,8 +18,7 @@ */ import { - mockGetAutocompleteProvider, - mockGetAutocompleteSuggestions, + mockFetchIndexPatterns, mockPersistedLog, mockPersistedLogFactory, } from './query_bar_input.test.mocks'; @@ -131,6 +130,8 @@ describe('QueryBarInput', () => { }); it('Should create a unique PersistedLog based on the appName and query language', () => { + mockPersistedLogFactory.mockClear(); + mountWithIntl( { expect(mockPersistedLog.get).toHaveBeenCalled(); }); - it('Should get suggestions from the autocomplete provider for the current language', () => { + it('Should accept index pattern strings and fetch the full object', () => { + mockFetchIndexPatterns.mockClear(); + mountWithIntl( ); - - expect(mockGetAutocompleteProvider).toHaveBeenCalledWith('kuery'); - expect(mockGetAutocompleteSuggestions).toHaveBeenCalled(); + expect(mockFetchIndexPatterns).toHaveBeenCalledWith(['logstash-*']); }); }); diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.tsx new file mode 100644 index 0000000000000..3722c3a572e10 --- /dev/null +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.tsx @@ -0,0 +1,526 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Component } from 'react'; +import React from 'react'; + +import { EuiFieldText, EuiOutsideClickDetector, PopoverAnchorPosition } from '@elastic/eui'; + +import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; +import { + AutocompleteSuggestion, + AutocompleteSuggestionType, + getAutocompleteProvider, +} from 'ui/autocomplete_providers'; +import { debounce, compact, isEqual, omit } from 'lodash'; +import { IndexPattern, StaticIndexPattern } from 'ui/index_patterns'; +import { PersistedLog } from 'ui/persisted_log'; +import chrome from 'ui/chrome'; +import { kfetch } from 'ui/kfetch'; +import { Storage } from 'ui/storage'; +import { localStorage } from 'ui/storage/storage_service'; +import { Query } from '../index'; +import { fromUser, matchPairs, toUser } from '../lib'; +import { QueryLanguageSwitcher } from './language_switcher'; +import { SuggestionsComponent } from './typeahead/suggestions_component'; +import { getQueryLog } from '../lib/get_query_log'; +import { fetchIndexPatterns } from '../lib/fetch_index_patterns'; + +interface Props { + indexPatterns: Array; + intl: InjectedIntl; + query: Query; + appName: string; + disableAutoFocus?: boolean; + screenTitle?: string; + prepend?: any; + store?: Storage; + persistedLog?: PersistedLog; + bubbleSubmitEvent?: boolean; + languageSwitcherPopoverAnchorPosition?: PopoverAnchorPosition; + onChange?: (query: Query) => void; + onSubmit?: (query: Query) => void; +} + +interface State { + isSuggestionsVisible: boolean; + index: number | null; + suggestions: AutocompleteSuggestion[]; + suggestionLimit: number; + selectionStart: number | null; + selectionEnd: number | null; + indexPatterns: StaticIndexPattern[]; +} + +const KEY_CODES = { + LEFT: 37, + UP: 38, + RIGHT: 39, + DOWN: 40, + ENTER: 13, + ESC: 27, + TAB: 9, + HOME: 36, + END: 35, +}; + +const config = chrome.getUiSettingsClient(); +const recentSearchType: AutocompleteSuggestionType = 'recentSearch'; + +export class QueryBarInputUI extends Component { + public state = { + isSuggestionsVisible: false, + index: null, + suggestions: [], + suggestionLimit: 50, + selectionStart: null, + selectionEnd: null, + indexPatterns: [], + }; + + public inputRef: HTMLInputElement | null = null; + + private persistedLog: PersistedLog | undefined; + private componentIsUnmounting = false; + + private getQueryString = () => { + return toUser(this.props.query.query); + }; + + private fetchIndexPatterns = async () => { + const stringPatterns = this.props.indexPatterns.filter( + indexPattern => typeof indexPattern === 'string' + ) as string[]; + const objectPatterns = this.props.indexPatterns.filter( + indexPattern => typeof indexPattern !== 'string' + ) as IndexPattern[]; + + const objectPatternsFromStrings = await fetchIndexPatterns(stringPatterns); + + this.setState({ + indexPatterns: [...objectPatterns, ...objectPatternsFromStrings], + }); + }; + + private getSuggestions = async () => { + if (!this.inputRef) { + return; + } + + const language = this.props.query.language; + const queryString = this.getQueryString(); + + const recentSearchSuggestions = this.getRecentSearchSuggestions(queryString); + + const autocompleteProvider = getAutocompleteProvider(language); + if ( + !autocompleteProvider || + !Array.isArray(this.state.indexPatterns) || + compact(this.state.indexPatterns).length === 0 + ) { + return recentSearchSuggestions; + } + + const indexPatterns = this.state.indexPatterns; + const getAutocompleteSuggestions = autocompleteProvider({ config, indexPatterns }); + + const { selectionStart, selectionEnd } = this.inputRef; + if (selectionStart === null || selectionEnd === null) { + return; + } + + const suggestions: AutocompleteSuggestion[] = await getAutocompleteSuggestions({ + query: queryString, + selectionStart, + selectionEnd, + }); + return [...suggestions, ...recentSearchSuggestions]; + }; + + private getRecentSearchSuggestions = (query: string) => { + if (!this.persistedLog) { + return []; + } + const recentSearches = this.persistedLog.get(); + const matchingRecentSearches = recentSearches.filter(recentQuery => { + const recentQueryString = typeof recentQuery === 'object' ? toUser(recentQuery) : recentQuery; + return recentQueryString.includes(query); + }); + return matchingRecentSearches.map(recentSearch => { + const text = toUser(recentSearch); + const start = 0; + const end = query.length; + return { type: recentSearchType, text, start, end }; + }); + }; + + private updateSuggestions = debounce(async () => { + const suggestions = (await this.getSuggestions()) || []; + if (!this.componentIsUnmounting) { + this.setState({ suggestions }); + } + }, 100); + + private onSubmit = (query: Query) => { + if (this.props.onSubmit) { + if (this.persistedLog) { + this.persistedLog.add(query.query); + } + + this.props.onSubmit({ query: fromUser(query.query), language: query.language }); + } + }; + + private onChange = (query: Query) => { + this.updateSuggestions(); + + if (this.props.onChange) { + this.props.onChange({ query: fromUser(query.query), language: query.language }); + } + }; + + private onQueryStringChange = (value: string) => { + const hasValue = Boolean(value.trim()); + + this.setState({ + isSuggestionsVisible: hasValue, + index: null, + suggestionLimit: 50, + }); + + this.onChange({ query: value, language: this.props.query.language }); + }; + + private onInputChange = (event: React.ChangeEvent) => { + this.onQueryStringChange(event.target.value); + }; + + private onClickInput = (event: React.MouseEvent) => { + if (event.target instanceof HTMLInputElement) { + this.onQueryStringChange(event.target.value); + } + }; + + private onKeyUp = (event: React.KeyboardEvent) => { + if ([KEY_CODES.LEFT, KEY_CODES.RIGHT, KEY_CODES.HOME, KEY_CODES.END].includes(event.keyCode)) { + this.setState({ isSuggestionsVisible: true }); + if (event.target instanceof HTMLInputElement) { + this.onQueryStringChange(event.target.value); + } + } + }; + + private onKeyDown = (event: React.KeyboardEvent) => { + if (event.target instanceof HTMLInputElement) { + const { isSuggestionsVisible, index } = this.state; + const preventDefault = event.preventDefault.bind(event); + const { target, key, metaKey } = event; + const { value, selectionStart, selectionEnd } = target; + const updateQuery = (query: string, newSelectionStart: number, newSelectionEnd: number) => { + this.onQueryStringChange(query); + this.setState({ + selectionStart: newSelectionStart, + selectionEnd: newSelectionEnd, + }); + }; + + switch (event.keyCode) { + case KEY_CODES.DOWN: + event.preventDefault(); + if (isSuggestionsVisible && index !== null) { + this.incrementIndex(index); + } else { + this.setState({ isSuggestionsVisible: true, index: 0 }); + } + break; + case KEY_CODES.UP: + event.preventDefault(); + if (isSuggestionsVisible && index !== null) { + this.decrementIndex(index); + } + break; + case KEY_CODES.ENTER: + if (!this.props.bubbleSubmitEvent) { + event.preventDefault(); + } + if (isSuggestionsVisible && index !== null && this.state.suggestions[index]) { + event.preventDefault(); + this.selectSuggestion(this.state.suggestions[index]); + } else { + this.onSubmit(this.props.query); + this.setState({ + isSuggestionsVisible: false, + }); + } + break; + case KEY_CODES.ESC: + event.preventDefault(); + this.setState({ isSuggestionsVisible: false, index: null }); + break; + case KEY_CODES.TAB: + this.setState({ isSuggestionsVisible: false, index: null }); + break; + default: + if (selectionStart !== null && selectionEnd !== null) { + matchPairs({ + value, + selectionStart, + selectionEnd, + key, + metaKey, + updateQuery, + preventDefault, + }); + } + + break; + } + } + }; + + private selectSuggestion = ({ + type, + text, + start, + end, + }: { + type: AutocompleteSuggestionType; + text: string; + start: number; + end: number; + }) => { + if (!this.inputRef) { + return; + } + + const query = this.getQueryString(); + const { selectionStart, selectionEnd } = this.inputRef; + if (selectionStart === null || selectionEnd === null) { + return; + } + + const value = query.substr(0, selectionStart) + query.substr(selectionEnd); + const newQueryString = value.substr(0, start) + text + value.substr(end); + + this.onQueryStringChange(newQueryString); + + if (type === recentSearchType) { + this.setState({ isSuggestionsVisible: false, index: null }); + this.onSubmit({ query: newQueryString, language: this.props.query.language }); + } + }; + + private increaseLimit = () => { + this.setState({ + suggestionLimit: this.state.suggestionLimit + 50, + }); + }; + + private incrementIndex = (currentIndex: number) => { + let nextIndex = currentIndex + 1; + if (currentIndex === null || nextIndex >= this.state.suggestions.length) { + nextIndex = 0; + } + this.setState({ index: nextIndex }); + }; + + private decrementIndex = (currentIndex: number) => { + const previousIndex = currentIndex - 1; + if (previousIndex < 0) { + this.setState({ index: this.state.suggestions.length - 1 }); + } else { + this.setState({ index: previousIndex }); + } + }; + + private onSelectLanguage = (language: string) => { + // Send telemetry info every time the user opts in or out of kuery + // As a result it is important this function only ever gets called in the + // UI component's change handler. + kfetch({ + pathname: '/api/kibana/kql_opt_in_telemetry', + method: 'POST', + body: JSON.stringify({ opt_in: language === 'kuery' }), + }); + + if (this.props.store) { + this.props.store.set('kibana.userQueryLanguage', language); + } else { + localStorage.set('kibana.userQueryLanguage', language); + } + + const newQuery = { query: '', language }; + this.onChange(newQuery); + this.onSubmit(newQuery); + }; + + private onOutsideClick = () => { + if (this.state.isSuggestionsVisible) { + this.setState({ isSuggestionsVisible: false, index: null }); + } + }; + + private onClickSuggestion = (suggestion: AutocompleteSuggestion) => { + if (!this.inputRef) { + return; + } + this.selectSuggestion(suggestion); + this.inputRef.focus(); + }; + + public onMouseEnterSuggestion = (index: number) => { + this.setState({ index }); + }; + + public componentDidMount() { + this.persistedLog = this.props.persistedLog + ? this.props.persistedLog + : getQueryLog(this.props.appName, this.props.query.language); + + this.fetchIndexPatterns().then(this.updateSuggestions); + } + + public componentDidUpdate(prevProps: Props) { + this.persistedLog = this.props.persistedLog + ? this.props.persistedLog + : getQueryLog(this.props.appName, this.props.query.language); + + if (!isEqual(prevProps.indexPatterns, this.props.indexPatterns)) { + this.fetchIndexPatterns().then(this.updateSuggestions); + } else if (!isEqual(prevProps.query, this.props.query)) { + this.updateSuggestions(); + } + + if (this.state.selectionStart !== null && this.state.selectionEnd !== null) { + if (this.inputRef) { + // For some reason the type guard above does not make the compiler happy + // @ts-ignore + this.inputRef.setSelectionRange(this.state.selectionStart, this.state.selectionEnd); + } + this.setState({ + selectionStart: null, + selectionEnd: null, + }); + } + } + + public componentWillUnmount() { + this.updateSuggestions.cancel(); + this.componentIsUnmounting = true; + } + + public render() { + const rest = omit(this.props, [ + 'indexPatterns', + 'intl', + 'query', + 'appName', + 'disableAutoFocus', + 'screenTitle', + 'prepend', + 'store', + 'persistedLog', + 'bubbleSubmitEvent', + 'languageSwitcherPopoverAnchorPosition', + 'onChange', + 'onSubmit', + ]); + + return ( + +
+
+
+ { + if (node) { + this.inputRef = node; + } + }} + autoComplete="off" + spellCheck={false} + aria-label={ + this.props.screenTitle + ? this.props.intl.formatMessage( + { + id: 'data.query.queryBar.searchInputAriaLabel', + defaultMessage: + 'You are on search box of {previouslyTranslatedPageTitle} page. Start typing to search and filter the {pageType}', + }, + { + previouslyTranslatedPageTitle: this.props.screenTitle, + pageType: this.props.appName, + } + ) + : undefined + } + type="text" + data-test-subj="queryInput" + aria-autocomplete="list" + aria-controls="kbnTypeahead__items" + aria-activedescendant={ + this.state.isSuggestionsVisible ? 'suggestion-' + this.state.index : '' + } + role="textbox" + prepend={this.props.prepend} + append={ + + } + {...rest} + /> +
+
+ + +
+
+ ); + } +} + +export const QueryBarInput = injectI18n(QueryBarInputUI); diff --git a/src/legacy/core_plugins/data/public/query_bar/components/typeahead/__snapshots__/suggestion_component.test.tsx.snap b/src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/__snapshots__/suggestion_component.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/data/public/query_bar/components/typeahead/__snapshots__/suggestion_component.test.tsx.snap rename to src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/__snapshots__/suggestion_component.test.tsx.snap diff --git a/src/legacy/core_plugins/data/public/query_bar/components/typeahead/__snapshots__/suggestions_component.test.tsx.snap b/src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/__snapshots__/suggestions_component.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/data/public/query_bar/components/typeahead/__snapshots__/suggestions_component.test.tsx.snap rename to src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/__snapshots__/suggestions_component.test.tsx.snap diff --git a/src/legacy/core_plugins/data/public/query_bar/components/typeahead/_index.scss b/src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/_index.scss similarity index 100% rename from src/legacy/core_plugins/data/public/query_bar/components/typeahead/_index.scss rename to src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/_index.scss diff --git a/src/legacy/core_plugins/data/public/query_bar/components/typeahead/_suggestion.scss b/src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/_suggestion.scss similarity index 100% rename from src/legacy/core_plugins/data/public/query_bar/components/typeahead/_suggestion.scss rename to src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/_suggestion.scss diff --git a/src/legacy/core_plugins/data/public/query_bar/components/typeahead/suggestion_component.test.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/suggestion_component.test.tsx similarity index 100% rename from src/legacy/core_plugins/data/public/query_bar/components/typeahead/suggestion_component.test.tsx rename to src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/suggestion_component.test.tsx diff --git a/src/legacy/core_plugins/data/public/query_bar/components/typeahead/suggestion_component.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/suggestion_component.tsx similarity index 100% rename from src/legacy/core_plugins/data/public/query_bar/components/typeahead/suggestion_component.tsx rename to src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/suggestion_component.tsx diff --git a/src/legacy/core_plugins/data/public/query_bar/components/typeahead/suggestions_component.test.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/suggestions_component.test.tsx similarity index 100% rename from src/legacy/core_plugins/data/public/query_bar/components/typeahead/suggestions_component.test.tsx rename to src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/suggestions_component.test.tsx diff --git a/src/legacy/core_plugins/data/public/query_bar/components/typeahead/suggestions_component.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/suggestions_component.tsx similarity index 100% rename from src/legacy/core_plugins/data/public/query_bar/components/typeahead/suggestions_component.tsx rename to src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/suggestions_component.tsx diff --git a/src/legacy/core_plugins/data/public/query/query_bar/directive/index.js b/src/legacy/core_plugins/data/public/query/query_bar/directive/index.js new file mode 100644 index 0000000000000..8d388d12ece74 --- /dev/null +++ b/src/legacy/core_plugins/data/public/query/query_bar/directive/index.js @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import 'ngreact'; +import { wrapInI18nContext } from 'ui/i18n'; +import { uiModules } from 'ui/modules'; +import { QueryBar } from '../components'; + +const app = uiModules.get('app/data', ['react']); + +export function setupDirective() { + app.directive('queryBar', (reactDirective, localStorage) => { + return reactDirective( + wrapInI18nContext(QueryBar), + undefined, + {}, + { + store: localStorage, + } + ); + }); +} diff --git a/src/legacy/core_plugins/data/public/query/query_bar/index.ts b/src/legacy/core_plugins/data/public/query/query_bar/index.ts new file mode 100644 index 0000000000000..38865e50e8897 --- /dev/null +++ b/src/legacy/core_plugins/data/public/query/query_bar/index.ts @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { QueryBar, QueryBarInput } from './components'; +export { fromUser } from './lib/from_user'; +export { toUser } from './lib/to_user'; +export { getQueryLog } from './lib/get_query_log'; + +// @ts-ignore +export { setupDirective } from './directive'; + +export interface Query { + query: string | { [key: string]: any }; + language: string; +} diff --git a/src/legacy/core_plugins/data/public/query_bar/lib/__tests__/lib_user_input.js b/src/legacy/core_plugins/data/public/query/query_bar/lib/__tests__/lib_user_input.js similarity index 100% rename from src/legacy/core_plugins/data/public/query_bar/lib/__tests__/lib_user_input.js rename to src/legacy/core_plugins/data/public/query/query_bar/lib/__tests__/lib_user_input.js diff --git a/src/legacy/core_plugins/data/public/query/query_bar/lib/fetch_index_patterns.ts b/src/legacy/core_plugins/data/public/query/query_bar/lib/fetch_index_patterns.ts new file mode 100644 index 0000000000000..091c7b74b6efa --- /dev/null +++ b/src/legacy/core_plugins/data/public/query/query_bar/lib/fetch_index_patterns.ts @@ -0,0 +1,54 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import chrome from 'ui/chrome'; +import { getFromSavedObject } from 'ui/index_patterns/static_utils'; +import { isEmpty } from 'lodash'; + +const config = chrome.getUiSettingsClient(); + +export async function fetchIndexPatterns(indexPatternStrings: string[]) { + if (!indexPatternStrings || isEmpty(indexPatternStrings)) { + return []; + } + + const searchString = indexPatternStrings.map(string => `"${string}"`).join(' | '); + const indexPatternsFromSavedObjects = await chrome.getSavedObjectsClient().find({ + type: 'index-pattern', + fields: ['title', 'fields'], + search: searchString, + searchFields: ['title'], + }); + + const exactMatches = indexPatternsFromSavedObjects.savedObjects.filter(savedObject => { + return indexPatternStrings.includes(savedObject.attributes.title as string); + }); + + const allMatches = + exactMatches.length === indexPatternStrings.length + ? exactMatches + : [...exactMatches, await fetchDefaultIndexPattern()]; + + return allMatches.map(getFromSavedObject); +} + +const fetchDefaultIndexPattern = async () => { + const savedObjectsClient = chrome.getSavedObjectsClient(); + return await savedObjectsClient.get('index-pattern', config.get('defaultIndex')); +}; diff --git a/src/legacy/core_plugins/data/public/query_bar/lib/from_user.ts b/src/legacy/core_plugins/data/public/query/query_bar/lib/from_user.ts similarity index 100% rename from src/legacy/core_plugins/data/public/query_bar/lib/from_user.ts rename to src/legacy/core_plugins/data/public/query/query_bar/lib/from_user.ts diff --git a/src/legacy/core_plugins/data/public/query_bar/lib/get_query_log.ts b/src/legacy/core_plugins/data/public/query/query_bar/lib/get_query_log.ts similarity index 100% rename from src/legacy/core_plugins/data/public/query_bar/lib/get_query_log.ts rename to src/legacy/core_plugins/data/public/query/query_bar/lib/get_query_log.ts diff --git a/src/legacy/core_plugins/data/public/query_bar/lib/index.ts b/src/legacy/core_plugins/data/public/query/query_bar/lib/index.ts similarity index 100% rename from src/legacy/core_plugins/data/public/query_bar/lib/index.ts rename to src/legacy/core_plugins/data/public/query/query_bar/lib/index.ts diff --git a/src/legacy/core_plugins/data/public/query_bar/lib/match_pairs.ts b/src/legacy/core_plugins/data/public/query/query_bar/lib/match_pairs.ts similarity index 100% rename from src/legacy/core_plugins/data/public/query_bar/lib/match_pairs.ts rename to src/legacy/core_plugins/data/public/query/query_bar/lib/match_pairs.ts diff --git a/src/legacy/core_plugins/data/public/query_bar/lib/to_user.ts b/src/legacy/core_plugins/data/public/query/query_bar/lib/to_user.ts similarity index 84% rename from src/legacy/core_plugins/data/public/query_bar/lib/to_user.ts rename to src/legacy/core_plugins/data/public/query/query_bar/lib/to_user.ts index dfae965d64344..1eb054ba40514 100644 --- a/src/legacy/core_plugins/data/public/query_bar/lib/to_user.ts +++ b/src/legacy/core_plugins/data/public/query/query_bar/lib/to_user.ts @@ -21,10 +21,10 @@ import angular from 'angular'; /** * Take text from the model and present it to the user as a string - * @param {text} model value + * @param text model value * @returns {string} */ -export function toUser(text: ToUserQuery | string): string { +export function toUser(text: { [key: string]: any } | string): string { if (text == null) { return ''; } @@ -39,12 +39,3 @@ export function toUser(text: ToUserQuery | string): string { } return '' + text; } - -interface ToUserQuery { - match_all: object; - query_string: ToUserQueryString; -} - -interface ToUserQueryString { - query: string; -} diff --git a/src/legacy/core_plugins/data/public/query/query_service.ts b/src/legacy/core_plugins/data/public/query/query_service.ts new file mode 100644 index 0000000000000..be678776f7526 --- /dev/null +++ b/src/legacy/core_plugins/data/public/query/query_service.ts @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { once } from 'lodash'; +import { + QueryBar, + QueryBarInput, + fromUser, + toUser, + getQueryLog, + setupDirective as setupQueryBarDirective, +} from './query_bar'; + +/** + * Query Service + * + * @internal + */ +export class QueryService { + public setup() { + return { + loadLegacyDirectives: once(setupQueryBarDirective), + helpers: { + fromUser, + toUser, + getQueryLog, + }, + ui: { + QueryBar, + QueryBarInput, + }, + }; + } + + public stop() { + // nothing to do here yet + } +} + +/** @public */ +export type QuerySetup = ReturnType; + +export { Query } from './query_bar'; diff --git a/src/legacy/core_plugins/data/public/query_bar/components/__snapshots__/query_bar_input.test.tsx.snap b/src/legacy/core_plugins/data/public/query_bar/components/__snapshots__/query_bar_input.test.tsx.snap deleted file mode 100644 index 58d409c227397..0000000000000 --- a/src/legacy/core_plugins/data/public/query_bar/components/__snapshots__/query_bar_input.test.tsx.snap +++ /dev/null @@ -1,1052 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`QueryBarInput Should disable autoFocus on EuiFieldText when disableAutoFocus prop is true 1`] = ` - - -
-
-
-
- - } - aria-activedescendant="" - aria-autocomplete="list" - aria-controls="kbnTypeahead__items" - aria-label="You are on search box of Another Screen page. Start typing to search and filter the discover" - autoComplete="off" - autoFocus={false} - compressed={false} - data-test-subj="queryInput" - fullWidth={true} - inputRef={[Function]} - isLoading={false} - onChange={[Function]} - onClick={[Function]} - onKeyDown={[Function]} - onKeyUp={[Function]} - placeholder="Search" - role="textbox" - spellCheck={false} - type="text" - value="response:200" - > - - } - compressed={false} - fullWidth={true} - isLoading={false} - > -
-
- - - - -
- - - - - } - className="eui-displayBlock" - closePopover={[Function]} - hasArrow={true} - id="popover" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - withTitle={true} - > - -
-
- - - -
-
-
-
-
-
-
-
-
-
-
- -
-
-
-`; - -exports[`QueryBarInput Should pass the query language to the language switcher 1`] = ` - - -
-
-
-
- - } - aria-activedescendant="" - aria-autocomplete="list" - aria-controls="kbnTypeahead__items" - aria-label="You are on search box of Another Screen page. Start typing to search and filter the discover" - autoComplete="off" - autoFocus={true} - compressed={false} - data-test-subj="queryInput" - fullWidth={true} - inputRef={[Function]} - isLoading={false} - onChange={[Function]} - onClick={[Function]} - onKeyDown={[Function]} - onKeyUp={[Function]} - placeholder="Search" - role="textbox" - spellCheck={false} - type="text" - value="response:200" - > - - } - compressed={false} - fullWidth={true} - isLoading={false} - > -
-
- - - - -
- - - - - } - className="eui-displayBlock" - closePopover={[Function]} - hasArrow={true} - id="popover" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - withTitle={true} - > - -
-
- - - -
-
-
-
-
-
-
-
-
-
-
- -
-
-
-`; - -exports[`QueryBarInput Should render the given query 1`] = ` - - -
-
-
-
- - } - aria-activedescendant="" - aria-autocomplete="list" - aria-controls="kbnTypeahead__items" - aria-label="You are on search box of Another Screen page. Start typing to search and filter the discover" - autoComplete="off" - autoFocus={true} - compressed={false} - data-test-subj="queryInput" - fullWidth={true} - inputRef={[Function]} - isLoading={false} - onChange={[Function]} - onClick={[Function]} - onKeyDown={[Function]} - onKeyUp={[Function]} - placeholder="Search" - role="textbox" - spellCheck={false} - type="text" - value="response:200" - > - - } - compressed={false} - fullWidth={true} - isLoading={false} - > -
-
- - - - -
- - - - - } - className="eui-displayBlock" - closePopover={[Function]} - hasArrow={true} - id="popover" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - withTitle={true} - > - -
-
- - - -
-
-
-
-
-
-
-
-
-
-
- -
-
-
-`; diff --git a/src/legacy/core_plugins/data/public/query_bar/components/index.ts b/src/legacy/core_plugins/data/public/query_bar/components/index.ts deleted file mode 100644 index ed4266589478e..0000000000000 --- a/src/legacy/core_plugins/data/public/query_bar/components/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { QueryBar } from './query_bar'; diff --git a/src/legacy/core_plugins/data/public/query_bar/components/query_bar_input.tsx b/src/legacy/core_plugins/data/public/query_bar/components/query_bar_input.tsx deleted file mode 100644 index 42bf972889e4d..0000000000000 --- a/src/legacy/core_plugins/data/public/query_bar/components/query_bar_input.tsx +++ /dev/null @@ -1,476 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Component } from 'react'; -import React from 'react'; - -import { EuiFieldText, EuiOutsideClickDetector } from '@elastic/eui'; - -import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import { - AutocompleteSuggestion, - AutocompleteSuggestionType, - getAutocompleteProvider, -} from 'ui/autocomplete_providers'; -import { debounce, compact } from 'lodash'; -import { IndexPattern } from 'ui/index_patterns'; -import { PersistedLog } from 'ui/persisted_log'; -import chrome from 'ui/chrome'; -import { kfetch } from 'ui/kfetch'; -import { Storage } from 'ui/storage'; -import { fromUser, matchPairs, toUser } from '../lib'; -import { QueryLanguageSwitcher } from './language_switcher'; -import { SuggestionsComponent } from './typeahead/suggestions_component'; -import { getQueryLog } from '../lib/get_query_log'; - -interface Query { - query: string; - language: string; -} - -interface Props { - indexPatterns: IndexPattern[]; - intl: InjectedIntl; - query: Query; - appName: string; - disableAutoFocus?: boolean; - screenTitle: string; - prepend?: any; - store: Storage; - persistedLog?: PersistedLog; - onChange?: (query: Query) => void; - onSubmit?: (query: Query) => void; -} - -interface State { - isSuggestionsVisible: boolean; - index: number | null; - suggestions: AutocompleteSuggestion[]; - suggestionLimit: number; - selectionStart: number | null; - selectionEnd: number | null; -} - -const KEY_CODES = { - LEFT: 37, - UP: 38, - RIGHT: 39, - DOWN: 40, - ENTER: 13, - ESC: 27, - TAB: 9, - HOME: 36, - END: 35, -}; - -const config = chrome.getUiSettingsClient(); -const recentSearchType: AutocompleteSuggestionType = 'recentSearch'; - -export class QueryBarInputUI extends Component { - public state = { - isSuggestionsVisible: false, - index: null, - suggestions: [], - suggestionLimit: 50, - selectionStart: null, - selectionEnd: null, - }; - - public inputRef: HTMLInputElement | null = null; - - private persistedLog: PersistedLog | undefined; - private componentIsUnmounting = false; - - private getQueryString = () => { - return toUser(this.props.query.query); - }; - - private getSuggestions = async () => { - if (!this.inputRef) { - return; - } - - const { - query: { query, language }, - } = this.props; - const recentSearchSuggestions = this.getRecentSearchSuggestions(query); - - const autocompleteProvider = getAutocompleteProvider(language); - if ( - !autocompleteProvider || - !Array.isArray(this.props.indexPatterns) || - compact(this.props.indexPatterns).length === 0 - ) { - return recentSearchSuggestions; - } - - const indexPatterns = this.props.indexPatterns; - const getAutocompleteSuggestions = autocompleteProvider({ config, indexPatterns }); - - const { selectionStart, selectionEnd } = this.inputRef; - if (selectionStart === null || selectionEnd === null) { - return; - } - - const suggestions: AutocompleteSuggestion[] = await getAutocompleteSuggestions({ - query, - selectionStart, - selectionEnd, - }); - return [...suggestions, ...recentSearchSuggestions]; - }; - - private getRecentSearchSuggestions = (query: string) => { - if (!this.persistedLog) { - return []; - } - const recentSearches = this.persistedLog.get(); - const matchingRecentSearches = recentSearches.filter(recentQuery => { - const recentQueryString = typeof recentQuery === 'object' ? toUser(recentQuery) : recentQuery; - return recentQueryString.includes(query); - }); - return matchingRecentSearches.map(recentSearch => { - const text = toUser(recentSearch); - const start = 0; - const end = query.length; - return { type: recentSearchType, text, start, end }; - }); - }; - - private updateSuggestions = debounce(async () => { - const suggestions = (await this.getSuggestions()) || []; - if (!this.componentIsUnmounting) { - this.setState({ suggestions }); - } - }, 100); - - private onSubmit = (query: Query) => { - if (this.props.onSubmit) { - if (this.persistedLog) { - this.persistedLog.add(query.query); - } - - this.props.onSubmit({ query: fromUser(query.query), language: query.language }); - } - }; - - private onChange = (query: Query) => { - this.updateSuggestions(); - - if (this.props.onChange) { - this.props.onChange({ query: fromUser(query.query), language: query.language }); - } - }; - - private onQueryStringChange = (value: string) => { - const hasValue = Boolean(value.trim()); - - this.setState({ - isSuggestionsVisible: hasValue, - index: null, - suggestionLimit: 50, - }); - - this.onChange({ query: value, language: this.props.query.language }); - }; - - private onInputChange = (event: React.ChangeEvent) => { - this.onQueryStringChange(event.target.value); - }; - - private onClickInput = (event: React.MouseEvent) => { - if (event.target instanceof HTMLInputElement) { - this.onQueryStringChange(event.target.value); - } - }; - - private onKeyUp = (event: React.KeyboardEvent) => { - if ([KEY_CODES.LEFT, KEY_CODES.RIGHT, KEY_CODES.HOME, KEY_CODES.END].includes(event.keyCode)) { - this.setState({ isSuggestionsVisible: true }); - if (event.target instanceof HTMLInputElement) { - this.onQueryStringChange(event.target.value); - } - } - }; - - private onKeyDown = (event: React.KeyboardEvent) => { - if (event.target instanceof HTMLInputElement) { - const { isSuggestionsVisible, index } = this.state; - const preventDefault = event.preventDefault.bind(event); - const { target, key, metaKey } = event; - const { value, selectionStart, selectionEnd } = target; - const updateQuery = (query: string, newSelectionStart: number, newSelectionEnd: number) => { - this.onQueryStringChange(query); - this.setState({ - selectionStart: newSelectionStart, - selectionEnd: newSelectionEnd, - }); - }; - - switch (event.keyCode) { - case KEY_CODES.DOWN: - event.preventDefault(); - if (isSuggestionsVisible && index !== null) { - this.incrementIndex(index); - } else { - this.setState({ isSuggestionsVisible: true, index: 0 }); - } - break; - case KEY_CODES.UP: - event.preventDefault(); - if (isSuggestionsVisible && index !== null) { - this.decrementIndex(index); - } - break; - case KEY_CODES.ENTER: - event.preventDefault(); - if (isSuggestionsVisible && index !== null && this.state.suggestions[index]) { - this.selectSuggestion(this.state.suggestions[index]); - } else { - this.onSubmit(this.props.query); - this.setState({ - isSuggestionsVisible: false, - }); - } - break; - case KEY_CODES.ESC: - event.preventDefault(); - this.setState({ isSuggestionsVisible: false, index: null }); - break; - case KEY_CODES.TAB: - this.setState({ isSuggestionsVisible: false, index: null }); - break; - default: - if (selectionStart !== null && selectionEnd !== null) { - matchPairs({ - value, - selectionStart, - selectionEnd, - key, - metaKey, - updateQuery, - preventDefault, - }); - } - - break; - } - } - }; - - private selectSuggestion = ({ - type, - text, - start, - end, - }: { - type: AutocompleteSuggestionType; - text: string; - start: number; - end: number; - }) => { - if (!this.inputRef) { - return; - } - - const query = this.getQueryString(); - const { selectionStart, selectionEnd } = this.inputRef; - if (selectionStart === null || selectionEnd === null) { - return; - } - - const value = query.substr(0, selectionStart) + query.substr(selectionEnd); - const newQueryString = value.substr(0, start) + text + value.substr(end); - - this.onQueryStringChange(newQueryString); - - if (type === recentSearchType) { - this.setState({ isSuggestionsVisible: false, index: null }); - this.onSubmit({ query: newQueryString, language: this.props.query.language }); - } - }; - - private increaseLimit = () => { - this.setState({ - suggestionLimit: this.state.suggestionLimit + 50, - }); - }; - - private incrementIndex = (currentIndex: number) => { - let nextIndex = currentIndex + 1; - if (currentIndex === null || nextIndex >= this.state.suggestions.length) { - nextIndex = 0; - } - this.setState({ index: nextIndex }); - }; - - private decrementIndex = (currentIndex: number) => { - const previousIndex = currentIndex - 1; - if (previousIndex < 0) { - this.setState({ index: this.state.suggestions.length - 1 }); - } else { - this.setState({ index: previousIndex }); - } - }; - - private onSelectLanguage = (language: string) => { - // Send telemetry info every time the user opts in or out of kuery - // As a result it is important this function only ever gets called in the - // UI component's change handler. - kfetch({ - pathname: '/api/kibana/kql_opt_in_telemetry', - method: 'POST', - body: JSON.stringify({ opt_in: language === 'kuery' }), - }); - - this.props.store.set('kibana.userQueryLanguage', language); - - const newQuery = { query: '', language }; - this.onChange(newQuery); - this.onSubmit(newQuery); - }; - - private onOutsideClick = () => { - if (this.state.isSuggestionsVisible) { - this.setState({ isSuggestionsVisible: false, index: null }); - } - }; - - private onClickSuggestion = (suggestion: AutocompleteSuggestion) => { - if (!this.inputRef) { - return; - } - this.selectSuggestion(suggestion); - this.inputRef.focus(); - }; - - public onMouseEnterSuggestion = (index: number) => { - this.setState({ index }); - }; - - public componentDidMount() { - this.persistedLog = this.props.persistedLog - ? this.props.persistedLog - : getQueryLog(this.props.appName, this.props.query.language); - this.updateSuggestions(); - } - - public componentDidUpdate(prevProps: Props) { - this.persistedLog = this.props.persistedLog - ? this.props.persistedLog - : getQueryLog(this.props.appName, this.props.query.language); - this.updateSuggestions(); - - if (this.state.selectionStart !== null && this.state.selectionEnd !== null) { - if (this.inputRef) { - // For some reason the type guard above does not make the compiler happy - // @ts-ignore - this.inputRef.setSelectionRange(this.state.selectionStart, this.state.selectionEnd); - } - this.setState({ - selectionStart: null, - selectionEnd: null, - }); - } - } - - public componentWillUnmount() { - this.updateSuggestions.cancel(); - this.componentIsUnmounting = true; - } - - public render() { - return ( - -
-
-
-
- { - if (node) { - this.inputRef = node; - } - }} - autoComplete="off" - spellCheck={false} - aria-label={this.props.intl.formatMessage( - { - id: 'data.query.queryBar.searchInputAriaLabel', - defaultMessage: - 'You are on search box of {previouslyTranslatedPageTitle} page. Start typing to search and filter the {pageType}', - }, - { - previouslyTranslatedPageTitle: this.props.screenTitle, - pageType: this.props.appName, - } - )} - type="text" - data-test-subj="queryInput" - aria-autocomplete="list" - aria-controls="kbnTypeahead__items" - aria-activedescendant={ - this.state.isSuggestionsVisible ? 'suggestion-' + this.state.index : '' - } - role="textbox" - prepend={this.props.prepend} - append={ - - } - /> -
-
-
- - -
-
- ); - } -} - -export const QueryBarInput = injectI18n(QueryBarInputUI); diff --git a/src/legacy/core_plugins/data/public/query_bar/directive/index.js b/src/legacy/core_plugins/data/public/query_bar/directive/index.js deleted file mode 100644 index 61b319ff19c9b..0000000000000 --- a/src/legacy/core_plugins/data/public/query_bar/directive/index.js +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - - - -import 'ngreact'; -import { wrapInI18nContext } from 'ui/i18n'; -import { uiModules } from 'ui/modules'; -import { QueryBar } from '../components'; - -const app = uiModules.get('app/data', ['react']); - -export function setupDirective() { - app.directive('queryBar', (reactDirective, localStorage) => { - return reactDirective( - wrapInI18nContext(QueryBar), - undefined, - {}, - { - store: localStorage, - } - ); - }); -} diff --git a/src/legacy/core_plugins/data/public/query_bar/index.ts b/src/legacy/core_plugins/data/public/query_bar/index.ts deleted file mode 100644 index 2737e79651d5a..0000000000000 --- a/src/legacy/core_plugins/data/public/query_bar/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { QueryBarService } from './query_bar_service'; - -export { QueryBar } from './components'; diff --git a/src/legacy/core_plugins/data/public/query_bar/query_bar_service.ts b/src/legacy/core_plugins/data/public/query_bar/query_bar_service.ts deleted file mode 100644 index 7195f982137d3..0000000000000 --- a/src/legacy/core_plugins/data/public/query_bar/query_bar_service.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { once } from 'lodash'; -import { QueryBar } from './components/query_bar'; -import { fromUser } from './lib/from_user'; -import { toUser } from './lib/to_user'; - -// @ts-ignore -import { setupDirective } from './directive'; - -/** - * Query Bar Service - * - * @internal - */ -export class QueryBarService { - public setup() { - return { - loadLegacyDirectives: once(setupDirective), - helpers: { - fromUser, - toUser, - }, - ui: { - QueryBar, - }, - }; - } - - public stop() { - // nothing to do here yet - } -} - -/** @public */ -export type QueryBarSetup = ReturnType; diff --git a/src/legacy/core_plugins/data/public/search/index.tsx b/src/legacy/core_plugins/data/public/search/index.tsx new file mode 100644 index 0000000000000..b0687ea3c6bcd --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/index.tsx @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { SearchService, SearchSetup } from './search_service'; diff --git a/src/legacy/core_plugins/data/public/search_bar/components/index.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/index.tsx similarity index 100% rename from src/legacy/core_plugins/data/public/search_bar/components/index.tsx rename to src/legacy/core_plugins/data/public/search/search_bar/components/index.tsx diff --git a/src/legacy/core_plugins/data/public/search_bar/components/search_bar.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx similarity index 96% rename from src/legacy/core_plugins/data/public/search_bar/components/search_bar.tsx rename to src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx index 133a68c3b4dbb..26a641b7bc153 100644 --- a/src/legacy/core_plugins/data/public/search_bar/components/search_bar.tsx +++ b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx @@ -24,16 +24,11 @@ import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import classNames from 'classnames'; import React, { Component } from 'react'; import ResizeObserver from 'resize-observer-polyfill'; -import { FilterBar } from 'ui/filter_bar'; import { IndexPattern } from 'ui/index_patterns'; import { Storage } from 'ui/storage'; -import { QueryBar } from '../../query_bar'; - -interface Query { - query: string; - language: string; -} +import { Query, QueryBar } from '../../../query/query_bar'; +import { FilterBar } from '../../../filter/filter_bar'; interface DateRange { from: string; @@ -45,10 +40,7 @@ interface DateRange { * See [search_bar\directive\index.js] file */ interface Props { - query: { - query: string; - language: string; - }; + query: Query; onQuerySubmit: (payload: { dateRange: DateRange; query: Query }) => void; disableAutoFocus?: boolean; appName: string; diff --git a/src/legacy/core_plugins/data/public/search_bar/directive/index.js b/src/legacy/core_plugins/data/public/search/search_bar/directive/index.js similarity index 100% rename from src/legacy/core_plugins/data/public/search_bar/directive/index.js rename to src/legacy/core_plugins/data/public/search/search_bar/directive/index.js diff --git a/src/legacy/core_plugins/data/public/search/search_bar/index.tsx b/src/legacy/core_plugins/data/public/search/search_bar/index.tsx new file mode 100644 index 0000000000000..b181b43e0e527 --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/search_bar/index.tsx @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { SearchBar } from './components'; + +// @ts-ignore +export { setupDirective } from './directive'; diff --git a/src/legacy/core_plugins/data/public/search/search_service.ts b/src/legacy/core_plugins/data/public/search/search_service.ts new file mode 100644 index 0000000000000..2e65280d3bcff --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/search_service.ts @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { once } from 'lodash'; +import { SearchBar, setupDirective as setupSearchBarDirective } from './search_bar'; + +/** + * Search Service + * @internal + */ +export class SearchService { + public setup() { + return { + ui: { + SearchBar, + }, + loadLegacyDirectives: once(setupSearchBarDirective), + }; + } + + public stop() { + // nothing to do here yet + } +} + +/** @public */ +export type SearchSetup = ReturnType; diff --git a/src/legacy/core_plugins/data/public/search_bar/index.tsx b/src/legacy/core_plugins/data/public/search_bar/index.tsx deleted file mode 100644 index c2e9b794415e3..0000000000000 --- a/src/legacy/core_plugins/data/public/search_bar/index.tsx +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { SearchBarService } from './search_bar_service'; diff --git a/src/legacy/core_plugins/data/public/search_bar/search_bar_service.ts b/src/legacy/core_plugins/data/public/search_bar/search_bar_service.ts deleted file mode 100644 index e5e010df85cc9..0000000000000 --- a/src/legacy/core_plugins/data/public/search_bar/search_bar_service.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { once } from 'lodash'; -import { SearchBar } from './components/search_bar'; - -// @ts-ignore -import { setupDirective } from './directive'; - -/** - * Search Bar Service - * @internal - */ -export class SearchBarService { - public setup() { - return { - ui: { - SearchBar, - }, - loadLegacyDirectives: once(setupDirective), - }; - } - - public stop() { - // nothing to do here yet - } -} - -/** @public */ -export type SearchBarSetup = ReturnType; diff --git a/src/legacy/core_plugins/embeddable_api/README.md b/src/legacy/core_plugins/embeddable_api/README.md new file mode 100644 index 0000000000000..8e099138c6013 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/README.md @@ -0,0 +1,31 @@ +# The Embeddable API V2 + +The Embeddable API's main goal is to have documented and standardized ways to share and exchange information and functionality across applications and plugins. + +There are three main pieces of this infrastructure: + - Embeddables & Containers + - Actions + - Triggers + +## Embeddables & Containers + +Embeddables are isolated, serializable, renderable widgets. A developer can hard code an embeddable inside their +application, or they can use some built in actions to allow users to dynamically add them to *containers*. + +Containers are a special type of embeddable that can contain nested embeddables. + +## Actions + +Actions are pluggable pieces of functionality exposed to the user that take an embeddable as context, plus an optional action context. + +## Triggers + +Triggers are the way actions are connected to a user action. We ship with two default triggers, `CONTEXT_MENU_TRIGGER` and `APPLY_FILTER`. + +Actions attached to the `CONTEXT_MENU_TRIGGER` will be displayed in supported embeddables context menu to the user. Actions attached to the `APPLY_FILTER` trigger will show up when any embeddable emits this trigger. + +A developer can register new triggers that their embeddables, or external components, can emit (as long as they have an embeddable to pass along as context). + +## Examples + +Many examples can be viewed in the functionally tested `kbn_tp_embeddable_explorer` plugin, as well as the jest tested classes inside the `embeddable_api/public/test_samples` folder. \ No newline at end of file diff --git a/src/legacy/core_plugins/embeddable_api/index.ts b/src/legacy/core_plugins/embeddable_api/index.ts new file mode 100644 index 0000000000000..f3ea5a4d99c88 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/index.ts @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { resolve } from 'path'; +import { LegacyPluginApi, LegacyPluginSpec, ArrayOrItem } from 'src/legacy/plugin_discovery/types'; + +// eslint-disable-next-line import/no-default-export +export default function(kibana: LegacyPluginApi): ArrayOrItem { + return new kibana.Plugin({ + uiExports: { + styleSheetPaths: resolve(__dirname, 'public/index.scss'), + embeddableActions: ['plugins/embeddable_api/actions/apply_filter_action'], + }, + }); +} diff --git a/src/legacy/core_plugins/embeddable_api/package.json b/src/legacy/core_plugins/embeddable_api/package.json new file mode 100644 index 0000000000000..f625408fe4c6c --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/package.json @@ -0,0 +1,4 @@ +{ + "name": "embeddable_api", + "version": "kibana" +} diff --git a/src/legacy/core_plugins/embeddable_api/public/_variables.scss b/src/legacy/core_plugins/embeddable_api/public/_variables.scss new file mode 100644 index 0000000000000..1c5b1664eab68 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/_variables.scss @@ -0,0 +1 @@ +$embEditingModeHoverColor: transparentize($euiColorWarning, lightOrDarkTheme(.9, .7)); diff --git a/src/legacy/core_plugins/embeddable_api/public/actions/action.test.ts b/src/legacy/core_plugins/embeddable_api/public/actions/action.test.ts new file mode 100644 index 0000000000000..f74318855e2a7 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/actions/action.test.ts @@ -0,0 +1,39 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import '../np_core.test.mocks'; + +import { HelloWorldAction, SayHelloAction, EmptyEmbeddable } from '../test_samples/index'; + +test('SayHelloAction is not compatible with not matching embeddables', async () => { + const sayHelloAction = new SayHelloAction(() => {}); + const emptyEmbeddable = new EmptyEmbeddable({ id: '234' }); + + // @ts-ignore Typescript is nice and tells us ahead of time this is invalid, but + // I want to make sure it also returns false. + const isCompatible = await sayHelloAction.isCompatible({ embeddable: emptyEmbeddable }); + expect(isCompatible).toBe(false); +}); + +test('HelloWorldAction inherits isCompatible from base action', async () => { + const helloWorldAction = new HelloWorldAction(); + const emptyEmbeddable = new EmptyEmbeddable({ id: '234' }); + const isCompatible = await helloWorldAction.isCompatible({ embeddable: emptyEmbeddable }); + expect(isCompatible).toBe(true); +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/actions/action.ts b/src/legacy/core_plugins/embeddable_api/public/actions/action.ts new file mode 100644 index 0000000000000..81ceea6e76e6f --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/actions/action.ts @@ -0,0 +1,81 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { EuiContextMenuItemIcon } from '@elastic/eui'; + +import { IEmbeddable } from '../embeddables'; + +export interface ActionContext< + TEmbeddable extends IEmbeddable = IEmbeddable, + TTriggerContext extends {} = {} +> { + embeddable: TEmbeddable; + triggerContext?: TTriggerContext; +} + +export abstract class Action< + TEmbeddable extends IEmbeddable = IEmbeddable, + TTriggerContext extends {} = {} +> { + /** + * Determined the order when there is more than one action matched to a trigger. + * Higher numbers are displayed first. + */ + public order: number = 0; + public abstract readonly type: string; + + constructor(public readonly id: string) {} + + /** + * Optional icon that can be displayed along with the title. + */ + public getIcon( + context: ActionContext + ): EuiContextMenuItemIcon | undefined { + return undefined; + } + + /** + * Returns a title to be displayed to the user. + * @param context + */ + public abstract getDisplayName(context: ActionContext): string; + + /** + * Returns a promise that resolves to true if this action is compatible given the context, + * otherwise resolves to false. + */ + public async isCompatible( + context: ActionContext + ): Promise { + return true; + } + + /** + * If this returns something other than undefined, this is used instead of execute when clicked. + */ + public getHref(context: ActionContext): string | undefined { + return undefined; + } + + /** + * Executes the action. + */ + public abstract execute(context: ActionContext): void; +} diff --git a/src/legacy/core_plugins/embeddable_api/public/actions/apply_filter_action.test.ts b/src/legacy/core_plugins/embeddable_api/public/actions/apply_filter_action.test.ts new file mode 100644 index 0000000000000..67071cc171b5d --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/actions/apply_filter_action.test.ts @@ -0,0 +1,136 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import '../np_core.test.mocks'; +import { + FilterableEmbeddable, + FilterableEmbeddableFactory, + FilterableContainer, + FILTERABLE_CONTAINER, + FilterableContainerInput, + FILTERABLE_EMBEDDABLE, + HelloWorldContainer, +} from '../test_samples/index'; +import { ApplyFilterAction } from './apply_filter_action'; +import { embeddableFactories, isErrorEmbeddable, EmbeddableOutput } from '../embeddables'; +import { FilterableContainerFactory } from '../test_samples/embeddables/filterable_container_factory'; +import { FilterableEmbeddableInput } from '../test_samples/embeddables/filterable_embeddable'; +import { Filter, FilterStateStore } from '@kbn/es-query'; + +beforeAll(() => { + embeddableFactories.set(FILTERABLE_CONTAINER, new FilterableContainerFactory()); + embeddableFactories.set(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory()); +}); + +afterAll(() => { + embeddableFactories.reset(); +}); + +test('ApplyFilterAction applies the filter to the root of the container tree', async () => { + const applyFilterAction = new ApplyFilterAction(); + + const root = new FilterableContainer( + { id: 'root', panels: {}, filters: [] }, + embeddableFactories + ); + + const node1 = await root.addNewEmbeddable< + FilterableContainerInput, + EmbeddableOutput, + FilterableContainer + >(FILTERABLE_CONTAINER, { panels: {}, id: 'node1' }); + + const node2 = await root.addNewEmbeddable< + FilterableContainerInput, + EmbeddableOutput, + FilterableContainer + >(FILTERABLE_CONTAINER, { panels: {}, id: 'Node2' }); + + if (isErrorEmbeddable(node2) || isErrorEmbeddable(node1)) { + throw new Error(); + } + + const embeddable = await node2.addNewEmbeddable< + FilterableEmbeddableInput, + EmbeddableOutput, + FilterableEmbeddable + >(FILTERABLE_EMBEDDABLE, { id: 'leaf' }); + + if (isErrorEmbeddable(embeddable)) { + throw new Error(); + } + + const filter: Filter = { + $state: { store: FilterStateStore.APP_STATE }, + meta: { + disabled: false, + negate: false, + alias: '', + }, + query: { match: { extension: { query: 'foo' } } }, + }; + + applyFilterAction.execute({ embeddable, triggerContext: { filters: [filter] } }); + expect(root.getInput().filters.length).toBe(1); + expect(node1.getInput().filters.length).toBe(1); + expect(embeddable.getInput().filters.length).toBe(1); + expect(node2.getInput().filters.length).toBe(1); +}); + +test('ApplyFilterAction is incompatible if the root container does not accept a filter as input', async () => { + const applyFilterAction = new ApplyFilterAction(); + const parent = new HelloWorldContainer({ id: 'root', panels: {} }, embeddableFactories); + + const embeddable = await parent.addNewEmbeddable< + FilterableContainerInput, + EmbeddableOutput, + FilterableContainer + >(FILTERABLE_EMBEDDABLE, { id: 'leaf' }); + + if (isErrorEmbeddable(embeddable)) { + throw new Error(); + } + + expect(await applyFilterAction.isCompatible({ embeddable })).toBe(false); +}); + +test('trying to execute on incompatible context throws an error ', async () => { + const applyFilterAction = new ApplyFilterAction(); + const parent = new HelloWorldContainer({ id: 'root', panels: {} }, embeddableFactories); + + const embeddable = await parent.addNewEmbeddable< + FilterableContainerInput, + EmbeddableOutput, + FilterableContainer + >(FILTERABLE_EMBEDDABLE, { id: 'leaf' }); + + if (isErrorEmbeddable(embeddable)) { + throw new Error(); + } + + async function check() { + await applyFilterAction.execute({ embeddable }); + } + await expect(check()).rejects.toThrow(Error); +}); + +test('gets title', async () => { + const applyFilterAction = new ApplyFilterAction(); + expect(applyFilterAction.getDisplayName()).toBeDefined(); +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/actions/apply_filter_action.ts b/src/legacy/core_plugins/embeddable_api/public/actions/apply_filter_action.ts new file mode 100644 index 0000000000000..f833ef4a9d9e1 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/actions/apply_filter_action.ts @@ -0,0 +1,94 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { Filter } from '@kbn/es-query'; +import { Container, ContainerInput } from '../containers'; +import { IEmbeddable } from '../embeddables'; +import { APPLY_FILTER_TRIGGER, triggerRegistry } from '../triggers'; +import { Action, ActionContext } from './action'; +import { actionRegistry } from '../actions'; +import { IncompatibleActionError } from './incompatible_action_error'; +import { IContainer } from '../containers/i_container'; +import { attachAction } from '../triggers/attach_action'; + +interface ApplyFilterContainerInput extends ContainerInput { + filters: Filter[]; +} + +const APPLY_FILTER_ACTION = 'APPLY_FILTER_ACTION'; + +function containerAcceptsFilterInput( + container: IEmbeddable | IContainer | IContainer +): container is Container { + return (container as Container).getInput().filters !== undefined; +} + +export class ApplyFilterAction extends Action { + public readonly type = APPLY_FILTER_ACTION; + + constructor() { + super(APPLY_FILTER_ACTION); + } + + public getDisplayName() { + return i18n.translate('embeddableApi.actions.applyFilterActionTitle', { + defaultMessage: 'Apply filter to current view', + }); + } + + public async isCompatible(context: ActionContext) { + return Boolean( + containerAcceptsFilterInput(context.embeddable.getRoot()) && + context.triggerContext && + context.triggerContext.filters !== undefined + ); + } + + public execute({ + embeddable, + triggerContext, + }: ActionContext) { + if (!triggerContext) { + throw new Error('Applying a filter requires a filter as context'); + } + const root = embeddable.getRoot(); + + if (!this.isCompatible({ triggerContext, embeddable })) { + throw new IncompatibleActionError(); + } + + // This logic is duplicated from isCompatible only for typescript not to complain on the following line + // since this function is a type guard + if (!containerAcceptsFilterInput(root)) { + throw new IncompatibleActionError(); + } + + root.updateInput({ + filters: triggerContext.filters, + }); + } +} + +actionRegistry.set(APPLY_FILTER_ACTION, new ApplyFilterAction()); + +attachAction(triggerRegistry, { + triggerId: APPLY_FILTER_TRIGGER, + actionId: APPLY_FILTER_ACTION, +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/actions/incompatible_action_error.ts b/src/legacy/core_plugins/embeddable_api/public/actions/incompatible_action_error.ts new file mode 100644 index 0000000000000..acff09b64f735 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/actions/incompatible_action_error.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; + +export class IncompatibleActionError extends Error { + constructor() { + super( + i18n.translate('embeddableApi.errors.incompatibleAction', { + defaultMessage: 'Action is incompatible', + }) + ); + } +} diff --git a/src/legacy/core_plugins/embeddable_api/public/actions/index.ts b/src/legacy/core_plugins/embeddable_api/public/actions/index.ts new file mode 100644 index 0000000000000..81964b62520bc --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/actions/index.ts @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +export { Action, ActionContext } from './action'; +export { IncompatibleActionError } from './incompatible_action_error'; + +import { createRegistry } from '../create_registry'; +import { Action } from './action'; + +export const actionRegistry = createRegistry(); diff --git a/src/legacy/core_plugins/embeddable_api/public/containers/container.test.ts b/src/legacy/core_plugins/embeddable_api/public/containers/container.test.ts new file mode 100644 index 0000000000000..58e8e74b4c178 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/containers/container.test.ts @@ -0,0 +1,750 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import '../np_core.test.mocks'; + +import * as Rx from 'rxjs'; +import { skip } from 'rxjs/operators'; +import { + CONTACT_CARD_EMBEDDABLE, + HelloWorldContainer, + FilterableContainer, + FILTERABLE_EMBEDDABLE, + FilterableEmbeddableFactory, + FilterableContainerInput, + ContactCardEmbeddable, + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + SlowContactCardEmbeddableFactory, + HELLO_WORLD_EMBEDDABLE_TYPE, + HelloWorldEmbeddableFactory, +} from '../test_samples/index'; +import { isErrorEmbeddable, EmbeddableOutput, EmbeddableFactory } from '../embeddables'; +import { ContainerInput } from './i_container'; +import { ViewMode } from '../types'; +import { createRegistry } from '../create_registry'; +import { + FilterableEmbeddableInput, + FilterableEmbeddable, +} from '../test_samples/embeddables/filterable_embeddable'; +import { ERROR_EMBEDDABLE_TYPE } from '../embeddables/error_embeddable'; +import { Filter, FilterStateStore } from '@kbn/es-query'; +import { PanelNotFoundError } from './panel_not_found_error'; + +const embeddableFactories = createRegistry(); +embeddableFactories.set(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory()); +embeddableFactories.set(CONTACT_CARD_EMBEDDABLE, new SlowContactCardEmbeddableFactory()); +embeddableFactories.set(HELLO_WORLD_EMBEDDABLE_TYPE, new HelloWorldEmbeddableFactory()); + +async function creatHelloWorldContainerAndEmbeddable( + containerInput: ContainerInput = { id: 'hello', panels: {} }, + embeddableInput = {} +) { + const container = new HelloWorldContainer(containerInput, embeddableFactories); + const embeddable = await container.addNewEmbeddable< + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardEmbeddable + >(CONTACT_CARD_EMBEDDABLE, embeddableInput); + + if (isErrorEmbeddable(embeddable)) { + throw new Error('Error adding embeddable'); + } + + return { container, embeddable }; +} + +test('Container initializes embeddables', async done => { + const container = new HelloWorldContainer( + { + id: 'hello', + panels: { + '123': { + explicitInput: { firstName: 'Sam', lastName: 'Tarley', id: '123' }, + type: CONTACT_CARD_EMBEDDABLE, + }, + }, + }, + embeddableFactories + ); + + const subscription = container.getOutput$().subscribe(() => { + if (container.getOutput().embeddableLoaded['123']) { + const embeddable = container.getChild('123'); + expect(embeddable).toBeDefined(); + expect(embeddable.id).toBe('123'); + subscription.unsubscribe(); + done(); + } + }); + + if (container.getOutput().embeddableLoaded['123']) { + const embeddable = container.getChild('123'); + expect(embeddable).toBeDefined(); + expect(embeddable.id).toBe('123'); + done(); + } +}); + +test('Container.addNewEmbeddable', async () => { + const { container, embeddable } = await creatHelloWorldContainerAndEmbeddable( + { id: 'hello', panels: {} }, + { + firstName: 'Susy', + } + ); + expect(embeddable).toBeDefined(); + + if (!isErrorEmbeddable(embeddable)) { + expect(embeddable.getInput().firstName).toBe('Susy'); + } else { + expect(false).toBe(true); + } + + const embeddableInContainer = container.getChild(embeddable.id); + expect(embeddableInContainer).toBeDefined(); + expect(embeddableInContainer.id).toBe(embeddable.id); +}); + +test('Container.removeEmbeddable removes and cleans up', async done => { + const container = new HelloWorldContainer( + { + id: 'hello', + panels: { + '123': { + explicitInput: { id: '123', firstName: 'Sam', lastName: 'Tarley' }, + type: CONTACT_CARD_EMBEDDABLE, + }, + }, + }, + embeddableFactories + ); + const embeddable = await container.addNewEmbeddable< + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardEmbeddable + >(CONTACT_CARD_EMBEDDABLE, { + firstName: 'Susy', + lastName: 'Q', + }); + + if (isErrorEmbeddable(embeddable)) { + expect(false).toBe(true); + return; + } + + embeddable.updateInput({ lastName: 'Z' }); + + container + .getOutput$() + .pipe(skip(1)) + .subscribe(() => { + const noFind = container.getChild(embeddable.id); + expect(noFind).toBeUndefined(); + + expect(container.getInput().panels[embeddable.id]).toBeUndefined(); + if (isErrorEmbeddable(embeddable)) { + expect(false).toBe(true); + done(); + } + + expect(() => embeddable.updateInput({ nameTitle: 'Sir' })).toThrowError(); + expect(container.getOutput().embeddableLoaded[embeddable.id]).toBeUndefined(); + done(); + }); + + container.removeEmbeddable(embeddable.id); +}); + +test('Container.input$ is notified when child embeddable input is updated', async () => { + const { container, embeddable } = await creatHelloWorldContainerAndEmbeddable( + { id: 'hello', panels: {} }, + { + firstName: 'Susy', + lastName: 'Q', + } + ); + + if (isErrorEmbeddable(embeddable)) { + expect(false).toBe(true); + return; + } + + const changes = jest.fn(); + const subscription = container.getInput$().subscribe(changes); + + embeddable.updateInput({ lastName: 'Z' }); + + expect(changes).toBeCalledTimes(2); + + expect(embeddable.getInput().lastName === 'Z'); + + embeddable.updateInput({ lastName: embeddable.getOutput().originalLastName }); + + expect(embeddable.getInput().lastName === 'Q'); + + expect(changes).toBeCalledTimes(3); + + subscription.unsubscribe(); + + embeddable.updateInput({ nameTitle: 'Dr.' }); + + expect(changes).toBeCalledTimes(3); +}); + +test('Container.input$', async () => { + const { container, embeddable } = await creatHelloWorldContainerAndEmbeddable( + { id: 'hello', panels: {} }, + { + firstName: 'Susy', + id: 'Susy', + } + ); + + if (isErrorEmbeddable(embeddable)) { + expect(false).toBe(true); + return; + } + + const changes = jest.fn(); + const input = container.getInput(); + expect(input.panels[embeddable.id].explicitInput).toEqual({ firstName: 'Susy', id: 'Susy' }); + + const subscription = container.getInput$().subscribe(changes); + embeddable.updateInput({ nameTitle: 'Dr.' }); + expect(container.getInput().panels[embeddable.id].explicitInput).toEqual({ + nameTitle: 'Dr.', + firstName: 'Susy', + id: 'Susy', + }); + subscription.unsubscribe(); +}); + +test('Container.getInput$ not triggered if state is the same', async () => { + const { container, embeddable } = await creatHelloWorldContainerAndEmbeddable( + { id: 'hello', panels: {} }, + { + firstName: 'Susy', + id: 'Susy', + } + ); + + if (isErrorEmbeddable(embeddable)) { + expect(false).toBe(true); + return; + } + + const changes = jest.fn(); + const input = container.getInput(); + expect(input.panels[embeddable.id].explicitInput).toEqual({ + id: 'Susy', + firstName: 'Susy', + }); + const subscription = container.getInput$().subscribe(changes); + embeddable.updateInput({ nameTitle: 'Dr.' }); + expect(changes).toBeCalledTimes(2); + embeddable.updateInput({ nameTitle: 'Dr.' }); + expect(changes).toBeCalledTimes(2); + subscription.unsubscribe(); +}); + +test('Container view mode change propagates to children', async () => { + const { container, embeddable } = await creatHelloWorldContainerAndEmbeddable( + { id: 'hello', panels: {}, viewMode: ViewMode.VIEW }, + { + firstName: 'Susy', + id: 'Susy', + } + ); + + expect(embeddable.getInput().viewMode).toBe(ViewMode.VIEW); + + container.updateInput({ viewMode: ViewMode.EDIT }); + + expect(embeddable.getInput().viewMode).toBe(ViewMode.EDIT); +}); + +test(`Container updates its state when a child's input is updated`, async done => { + const { container, embeddable } = await creatHelloWorldContainerAndEmbeddable( + { id: 'hello', panels: {}, viewMode: ViewMode.VIEW }, + { + id: '123', + firstName: 'Susy', + } + ); + + if (isErrorEmbeddable(embeddable)) { + throw new Error('Error adding embeddable'); + } + + const containerSubscription = Rx.merge(container.getInput$(), container.getOutput$()).subscribe( + () => { + const child = container.getChild(embeddable.id); + if ( + container.getOutput().embeddableLoaded[embeddable.id] && + child.getInput().nameTitle === 'Dr.' + ) { + containerSubscription.unsubscribe(); + + // Make sure a brand new container built off the output of container also creates an embeddable + // with "Dr.", not the default the embeddable was first added with. Makes sure changed input + // is preserved with the container. + const containerClone = new HelloWorldContainer(container.getInput(), embeddableFactories); + const cloneSubscription = Rx.merge( + containerClone.getOutput$(), + containerClone.getInput$() + ).subscribe(() => { + const childClone = containerClone.getChild(embeddable.id); + + if ( + containerClone.getOutput().embeddableLoaded[embeddable.id] && + childClone.getInput().nameTitle === 'Dr.' + ) { + cloneSubscription.unsubscribe(); + done(); + } + }); + } + } + ); + + embeddable.updateInput({ nameTitle: 'Dr.' }); +}); + +test(`Derived container state passed to children`, async () => { + const { container, embeddable } = await creatHelloWorldContainerAndEmbeddable( + { id: 'hello', panels: {}, viewMode: ViewMode.VIEW }, + { + firstName: 'Susy', + } + ); + + let subscription = embeddable + .getInput$() + .pipe(skip(1)) + .subscribe((changes: Partial) => { + expect(changes.viewMode).toBe(ViewMode.EDIT); + }); + container.updateInput({ viewMode: ViewMode.EDIT }); + + subscription.unsubscribe(); + subscription = embeddable + .getInput$() + .pipe(skip(1)) + .subscribe((changes: Partial) => { + expect(changes.viewMode).toBe(ViewMode.VIEW); + }); + container.updateInput({ viewMode: ViewMode.VIEW }); + subscription.unsubscribe(); +}); + +test(`Can subscribe to children embeddable updates`, async done => { + const { embeddable } = await creatHelloWorldContainerAndEmbeddable( + { + id: 'hello container', + panels: {}, + viewMode: ViewMode.VIEW, + }, + { + firstName: 'Susy', + } + ); + + if (isErrorEmbeddable(embeddable)) { + throw new Error('Error adding embeddable'); + } + + const subscription = embeddable.getInput$().subscribe((input: ContactCardEmbeddableInput) => { + if (input.nameTitle === 'Dr.') { + subscription.unsubscribe(); + done(); + } + }); + embeddable.updateInput({ nameTitle: 'Dr.' }); +}); + +test('Test nested reactions', async done => { + const { container, embeddable } = await creatHelloWorldContainerAndEmbeddable( + { id: 'hello', panels: {}, viewMode: ViewMode.VIEW }, + { + firstName: 'Susy', + } + ); + + if (isErrorEmbeddable(embeddable)) { + throw new Error('Error adding embeddable'); + } + + const containerSubscription = container.getInput$().subscribe(input => { + const embeddableNameTitle = embeddable.getInput().nameTitle; + const viewMode = input.viewMode; + const nameTitleFromContainer = container.getInputForChild( + embeddable.id + ).nameTitle; + if ( + embeddableNameTitle === 'Dr.' && + nameTitleFromContainer === 'Dr.' && + viewMode === ViewMode.EDIT + ) { + containerSubscription.unsubscribe(); + embeddableSubscription.unsubscribe(); + done(); + } + }); + + const embeddableSubscription = embeddable.getInput$().subscribe(() => { + if (embeddable.getInput().nameTitle === 'Dr.') { + container.updateInput({ viewMode: ViewMode.EDIT }); + } + }); + + embeddable.updateInput({ nameTitle: 'Dr.' }); +}); + +test('Explicit embeddable input mapped to undefined will default to inherited', async () => { + const derivedFilter: Filter = { + $state: { store: FilterStateStore.APP_STATE }, + meta: { disabled: false, alias: 'name', negate: false }, + query: { match: {} }, + }; + const container = new FilterableContainer( + { id: 'hello', panels: {}, filters: [derivedFilter] }, + embeddableFactories + ); + const embeddable = await container.addNewEmbeddable< + FilterableEmbeddableInput, + EmbeddableOutput, + FilterableEmbeddable + >(FILTERABLE_EMBEDDABLE, {}); + + if (isErrorEmbeddable(embeddable)) { + throw new Error('Error adding embeddable'); + } + + embeddable.updateInput({ filters: [] }); + + expect(container.getInputForChild(embeddable.id).filters).toEqual([]); + + embeddable.updateInput({ filters: undefined }); + + expect(container.getInputForChild(embeddable.id).filters).toEqual([ + derivedFilter, + ]); +}); + +test('Explicit embeddable input mapped to undefined with no inherited value will get passed to embeddable', async done => { + const container = new HelloWorldContainer({ id: 'hello', panels: {} }, embeddableFactories); + + const embeddable = await container.addNewEmbeddable< + FilterableEmbeddableInput, + EmbeddableOutput, + FilterableEmbeddable + >(FILTERABLE_EMBEDDABLE, {}); + + if (isErrorEmbeddable(embeddable)) { + throw new Error('Error adding embeddable'); + } + + embeddable.updateInput({ filters: [] }); + + expect(container.getInputForChild(embeddable.id).filters).toEqual([]); + + const subscription = embeddable + .getInput$() + .pipe(skip(1)) + .subscribe(() => { + if (embeddable.getInput().filters === undefined) { + subscription.unsubscribe(); + done(); + } + }); + + embeddable.updateInput({ filters: undefined }); +}); + +test('Panel removed from input state', async done => { + const container = new FilterableContainer( + { id: 'hello', panels: {}, filters: [] }, + embeddableFactories + ); + + const embeddable = await container.addNewEmbeddable< + FilterableEmbeddableInput, + EmbeddableOutput, + FilterableEmbeddable + >(FILTERABLE_EMBEDDABLE, {}); + + const filteredPanels = { ...container.getInput().panels }; + delete filteredPanels[embeddable.id]; + const newInput: Partial = { + ...container.getInput(), + panels: filteredPanels, + }; + + const subscription = container + .getOutput$() + .pipe(skip(1)) + .subscribe(() => { + expect(container.getChild(embeddable.id)).toBeUndefined(); + expect(container.getOutput().embeddableLoaded[embeddable.id]).toBeUndefined(); + subscription.unsubscribe(); + done(); + }); + + container.updateInput(newInput); +}); + +test('Panel added to input state', async done => { + const container = new FilterableContainer( + { id: 'hello', panels: {}, filters: [] }, + embeddableFactories + ); + + const embeddable = await container.addNewEmbeddable< + FilterableEmbeddableInput, + EmbeddableOutput, + FilterableEmbeddable + >(FILTERABLE_EMBEDDABLE, {}); + + const embeddable2 = await container.addNewEmbeddable< + FilterableEmbeddableInput, + EmbeddableOutput, + FilterableEmbeddable + >(FILTERABLE_EMBEDDABLE, {}); + + const container2 = new FilterableContainer( + { id: 'hello', panels: {}, filters: [] }, + embeddableFactories + ); + + const subscription = container2 + .getOutput$() + .pipe(skip(2)) + .subscribe(() => { + expect(container.getChild(embeddable.id)).toBeDefined(); + expect(container.getOutput().embeddableLoaded[embeddable.id]).toBe(true); + expect(container.getChild(embeddable2.id)).toBeDefined(); + expect(container.getOutput().embeddableLoaded[embeddable2.id]).toBe(true); + subscription.unsubscribe(); + done(); + }); + + // Container 1 has the panel in it's array, copy it to container2. + container2.updateInput(container.getInput()); +}); + +test('Container changes made directly after adding a new embeddable are propagated', async done => { + const container = new HelloWorldContainer( + { id: 'hello', panels: {}, viewMode: ViewMode.EDIT }, + embeddableFactories + ); + + embeddableFactories.reset(); + embeddableFactories.set( + CONTACT_CARD_EMBEDDABLE, + new SlowContactCardEmbeddableFactory({ loadTickCount: 3 }) + ); + + const subscription = Rx.merge(container.getOutput$(), container.getInput$()) + .pipe(skip(2)) + .subscribe(() => { + expect(Object.keys(container.getOutput().embeddableLoaded).length).toBe(1); + if (Object.keys(container.getOutput().embeddableLoaded).length > 0) { + const embeddableId = Object.keys(container.getOutput().embeddableLoaded)[0]; + + if (container.getOutput().embeddableLoaded[embeddableId] === true) { + const embeddable = container.getChild(embeddableId); + if (embeddable.getInput().viewMode === ViewMode.VIEW) { + subscription.unsubscribe(); + done(); + } + } + } + }); + + container.addNewEmbeddable(CONTACT_CARD_EMBEDDABLE, { + firstName: 'A girl', + lastName: 'has no name', + }); + + container.updateInput({ viewMode: ViewMode.VIEW }); +}); + +test('container stores ErrorEmbeddables when a factory for a child cannot be found (so the panel can be removed)', async done => { + const container = new HelloWorldContainer( + { + id: 'hello', + panels: { + '123': { + type: 'IDontExist', + explicitInput: { id: '123' }, + }, + }, + viewMode: ViewMode.EDIT, + }, + embeddableFactories + ); + + container.getOutput$().subscribe(() => { + if (container.getOutput().embeddableLoaded['123']) { + const child = container.getChild('123'); + expect(child.type).toBe(ERROR_EMBEDDABLE_TYPE); + done(); + } + }); +}); + +test('container stores ErrorEmbeddables when a saved object cannot be found', async done => { + const container = new HelloWorldContainer( + { + id: 'hello', + panels: { + '123': { + type: 'vis', + explicitInput: { id: '123' }, + savedObjectId: '456', + }, + }, + viewMode: ViewMode.EDIT, + }, + embeddableFactories + ); + + container.getOutput$().subscribe(() => { + if (container.getOutput().embeddableLoaded['123']) { + const child = container.getChild('123'); + expect(child.type).toBe(ERROR_EMBEDDABLE_TYPE); + done(); + } + }); +}); + +test('ErrorEmbeddables get updated when parent does', async done => { + const container = new HelloWorldContainer( + { + id: 'hello', + panels: { + '123': { + type: 'vis', + explicitInput: { id: '123' }, + savedObjectId: '456', + }, + }, + viewMode: ViewMode.EDIT, + }, + embeddableFactories + ); + + container.getOutput$().subscribe(() => { + if (container.getOutput().embeddableLoaded['123']) { + const embeddable = container.getChild('123'); + + expect(embeddable.getInput().viewMode).toBe(ViewMode.EDIT); + + container.updateInput({ viewMode: ViewMode.VIEW }); + + expect(embeddable.getInput().viewMode).toBe(ViewMode.VIEW); + done(); + } + }); +}); + +test('untilEmbeddableLoaded throws an error if there is no such child panel in the container', () => { + const container = new HelloWorldContainer( + { + id: 'hello', + panels: {}, + }, + embeddableFactories + ); + + expect(container.untilEmbeddableLoaded('idontexist')).rejects.toThrowError(); +}); + +test('untilEmbeddableLoaded resolves if child is has an type that does not exist', async done => { + embeddableFactories.reset(); + const container = new HelloWorldContainer( + { + id: 'hello', + panels: { + '123': { + type: HELLO_WORLD_EMBEDDABLE_TYPE, + explicitInput: { id: '123' }, + }, + }, + }, + embeddableFactories + ); + + const child = await container.untilEmbeddableLoaded('123'); + expect(child).toBeDefined(); + expect(child.type).toBe(ERROR_EMBEDDABLE_TYPE); + done(); +}); + +test('untilEmbeddableLoaded resolves if child is loaded in the container', async done => { + embeddableFactories.reset(); + embeddableFactories.set(HELLO_WORLD_EMBEDDABLE_TYPE, new HelloWorldEmbeddableFactory()); + + const container = new HelloWorldContainer( + { + id: 'hello', + panels: { + '123': { + type: HELLO_WORLD_EMBEDDABLE_TYPE, + explicitInput: { id: '123' }, + }, + }, + }, + embeddableFactories + ); + + const child = await container.untilEmbeddableLoaded('123'); + expect(child).toBeDefined(); + expect(child.type).toBe(HELLO_WORLD_EMBEDDABLE_TYPE); + done(); +}); + +test('untilEmbeddableLoaded rejects with an error if child is subsequently removed', async done => { + embeddableFactories.reset(); + embeddableFactories.set( + CONTACT_CARD_EMBEDDABLE, + new SlowContactCardEmbeddableFactory({ loadTickCount: 3 }) + ); + + const container = new HelloWorldContainer( + { + id: 'hello', + panels: { + '123': { + explicitInput: { id: '123', firstName: 'Sam', lastName: 'Tarley' }, + type: CONTACT_CARD_EMBEDDABLE, + }, + }, + }, + embeddableFactories + ); + + container.untilEmbeddableLoaded('123').catch(error => { + expect(error).toBeInstanceOf(PanelNotFoundError); + done(); + }); + + container.updateInput({ panels: {} }); +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/containers/container.ts b/src/legacy/core_plugins/embeddable_api/public/containers/container.ts new file mode 100644 index 0000000000000..e848c0a23d828 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/containers/container.ts @@ -0,0 +1,377 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import uuid from 'uuid'; +import { merge, Subscription } from 'rxjs'; +import { + Embeddable, + EmbeddableInput, + EmbeddableOutput, + ErrorEmbeddable, + EmbeddableFactory, + EmbeddableFactoryNotFoundError, +} from '../embeddables'; +import { IContainer, ContainerInput, ContainerOutput, PanelState } from './i_container'; +import { IEmbeddable } from '../embeddables/i_embeddable'; +import { IRegistry } from '../types'; +import { PanelNotFoundError } from './panel_not_found_error'; + +const getKeys = (o: T): Array => Object.keys(o) as Array; + +export abstract class Container< + TChildInput extends Partial = {}, + TContainerInput extends ContainerInput = ContainerInput, + TContainerOutput extends ContainerOutput = ContainerOutput +> extends Embeddable + implements IContainer { + public readonly isContainer: boolean = true; + protected readonly children: { + [key: string]: IEmbeddable | ErrorEmbeddable; + } = {}; + public readonly embeddableFactories: IRegistry; + + private subscription: Subscription; + + constructor( + input: TContainerInput, + output: TContainerOutput, + embeddableFactories: IRegistry, + parent?: Container + ) { + super(input, output, parent); + this.embeddableFactories = embeddableFactories; + this.subscription = this.getInput$().subscribe(() => this.maybeUpdateChildren()); + } + + public updateInputForChild( + id: string, + changes: Partial + ) { + if (!this.input.panels[id]) { + throw new PanelNotFoundError(); + } + const panels = { + panels: { + ...this.input.panels, + [id]: { + ...this.input.panels[id], + explicitInput: { + ...this.input.panels[id].explicitInput, + ...changes, + }, + }, + }, + }; + this.updateInput(panels as Partial); + } + + public reload() { + Object.values(this.children).forEach(child => child.reload()); + } + + public async addNewEmbeddable< + EEI extends EmbeddableInput = EmbeddableInput, + EEO extends EmbeddableOutput = EmbeddableOutput, + E extends IEmbeddable = IEmbeddable + >(type: string, explicitInput: Partial): Promise { + const factory = this.embeddableFactories.get(type) as + | EmbeddableFactory + | undefined; + + if (!factory) { + throw new EmbeddableFactoryNotFoundError(type); + } + + const panelState = this.createNewPanelState(factory, explicitInput); + + return this.createAndSaveEmbeddable(type, panelState); + } + + public async addSavedObjectEmbeddable< + TEmbeddableInput extends EmbeddableInput = EmbeddableInput, + TEmbeddable extends IEmbeddable = IEmbeddable + >(type: string, savedObjectId: string): Promise { + const factory = this.embeddableFactories.get(type) as EmbeddableFactory< + TEmbeddableInput, + any, + TEmbeddable + >; + const panelState = this.createNewPanelState(factory); + panelState.savedObjectId = savedObjectId; + + return this.createAndSaveEmbeddable(type, panelState); + } + + public removeEmbeddable(embeddableId: string) { + // Just a shortcut for removing the panel from input state, all internal state will get cleaned up naturally + // by the listener. + const panels = { ...this.input.panels }; + delete panels[embeddableId]; + this.updateInput({ panels } as Partial); + } + + public getChild(id: string): E { + return this.children[id] as E; + } + + public getInputForChild( + embeddableId: string + ): TEmbeddableInput { + const containerInput: TChildInput = this.getInheritedInput(embeddableId); + const panelState = this.getPanelState(embeddableId); + + const explicitInput = panelState.explicitInput; + const explicitFiltered: { [key: string]: unknown } = {}; + + const keys = getKeys(panelState.explicitInput); + + // If explicit input for a particular value is undefined, and container has that input defined, + // we will use the inherited container input. This way children can set a value to undefined in order + // to default back to inherited input. However, if the particular value is not part of the container, then + // the caller may be trying to explicitly tell the child to clear out a given value, so in that case, we want + // to pass it along. + keys.forEach(key => { + if (explicitInput[key] === undefined && containerInput[key] !== undefined) { + return; + } + explicitFiltered[key] = explicitInput[key]; + }); + + return ({ + ...containerInput, + ...explicitFiltered, + // Typescript has difficulties with inferring this type but it is accurate with all + // tests I tried. Could probably be revisted with future releases of TS to see if + // it can accurately infer the type. + } as unknown) as TEmbeddableInput; + } + + public destroy() { + super.destroy(); + Object.values(this.children).forEach(child => child.destroy()); + this.subscription.unsubscribe(); + } + + public async untilEmbeddableLoaded( + id: string + ): Promise { + if (!this.input.panels[id]) { + throw new PanelNotFoundError(); + } + + if (this.output.embeddableLoaded[id]) { + return this.children[id] as TEmbeddable; + } + + return new Promise((resolve, reject) => { + const subscription = merge(this.getOutput$(), this.getInput$()).subscribe(() => { + if (this.output.embeddableLoaded[id]) { + subscription.unsubscribe(); + resolve(this.children[id] as TEmbeddable); + } + + // If a panel is removed before the embeddable was loaded there is a chance this will + // never resolve. + if (this.input.panels[id] === undefined) { + subscription.unsubscribe(); + reject(new PanelNotFoundError()); + } + }); + }); + } + + protected createNewPanelState< + TEmbeddableInput extends EmbeddableInput, + TEmbeddable extends IEmbeddable + >( + factory: EmbeddableFactory, + partial: Partial = {} + ): PanelState { + const embeddableId = partial.id || uuid.v4(); + + const explicitInput = this.createNewExplicitEmbeddableInput( + embeddableId, + factory, + partial + ); + + return { + type: factory.type, + explicitInput: { + id: embeddableId, + ...explicitInput, + } as TEmbeddableInput, + }; + } + + protected getPanelState( + embeddableId: string + ) { + if (this.input.panels[embeddableId] === undefined) { + throw new PanelNotFoundError(); + } + const panelState: PanelState = this.input.panels[embeddableId]; + return panelState as PanelState; + } + + /** + * Return state that comes from the container and is passed down to the child. For instance, time range and + * filters are common inherited input state. Note that any state stored in `this.input.panels[embeddableId].explicitInput` + * will override inherited input. + */ + protected abstract getInheritedInput(id: string): TChildInput; + + private async createAndSaveEmbeddable< + TEmbeddableInput extends EmbeddableInput = EmbeddableInput, + TEmbeddable extends IEmbeddable = IEmbeddable + >(type: string, panelState: PanelState) { + this.updateInput({ + panels: { + ...this.input.panels, + [panelState.explicitInput.id]: panelState, + }, + } as Partial); + + return await this.untilEmbeddableLoaded(panelState.explicitInput.id); + } + + private createNewExplicitEmbeddableInput< + TEmbeddableInput extends EmbeddableInput = EmbeddableInput, + TEmbeddable extends IEmbeddable = IEmbeddable< + TEmbeddableInput + > + >( + id: string, + factory: EmbeddableFactory, + partial: Partial = {} + ): Partial { + const inheritedInput = this.getInheritedInput(id); + const defaults = factory.getDefaultInput(partial); + + // Container input overrides defaults. + const explicitInput: Partial = partial; + + getKeys(defaults).forEach(key => { + // @ts-ignore We know this key might not exist on inheritedInput. + const inheritedValue = inheritedInput[key]; + if (inheritedValue === undefined && explicitInput[key] === undefined) { + explicitInput[key] = defaults[key]; + } + }); + return explicitInput; + } + + private onPanelRemoved(id: string) { + // Clean up + const embeddable = this.getChild(id); + if (embeddable) { + embeddable.destroy(); + + // Remove references. + delete this.children[id]; + } + + this.updateOutput({ + embeddableLoaded: { + ...this.output.embeddableLoaded, + [id]: undefined, + }, + } as Partial); + } + + private async onPanelAdded(panel: PanelState) { + this.updateOutput({ + embeddableLoaded: { + ...this.output.embeddableLoaded, + [panel.explicitInput.id]: false, + }, + } as Partial); + let embeddable: IEmbeddable | ErrorEmbeddable | undefined; + const inputForChild = this.getInputForChild(panel.explicitInput.id); + try { + const factory = this.embeddableFactories.get(panel.type); + if (!factory) { + throw new EmbeddableFactoryNotFoundError(panel.type); + } + + embeddable = panel.savedObjectId + ? await factory.createFromSavedObject(panel.savedObjectId, inputForChild, this) + : await factory.create(inputForChild, this); + } catch (e) { + embeddable = new ErrorEmbeddable(e, { id: panel.explicitInput.id }, this); + } + + // EmbeddableFactory.create can return undefined without throwing an error, which indicates that an embeddable + // can't be created. This logic essentially only exists to support the current use case of + // visualizations being created from the add panel, which redirects the user to the visualize app. Once we + // switch over to inline creation we can probably clean this up, and force EmbeddableFactory.create to always + // return an embeddable, or throw an error. + if (embeddable) { + // The factory creation process may ask the user for input to update or override any input coming + // from the container. + const input = embeddable.getInput(); + const newOrChangedInput = getKeys(input) + .filter(key => input[key] !== inputForChild[key]) + .reduce((res, key) => Object.assign(res, { [key]: input[key] }), {}); + + if (embeddable.getOutput().savedObjectId || Object.keys(newOrChangedInput).length > 0) { + this.updateInput({ + panels: { + ...this.input.panels, + [panel.explicitInput.id]: { + ...this.input.panels[panel.explicitInput.id], + ...(embeddable.getOutput().savedObjectId + ? { savedObjectId: embeddable.getOutput().savedObjectId } + : undefined), + explicitInput: { + ...this.input.panels[panel.explicitInput.id].explicitInput, + ...newOrChangedInput, + }, + }, + }, + } as Partial); + } + + this.children[embeddable.id] = embeddable; + this.updateOutput({ + embeddableLoaded: { + ...this.output.embeddableLoaded, + [panel.explicitInput.id]: true, + }, + } as Partial); + } else if (embeddable === undefined) { + this.removeEmbeddable(panel.explicitInput.id); + } + return embeddable; + } + + private maybeUpdateChildren() { + const allIds = Object.keys({ ...this.input.panels, ...this.output.embeddableLoaded }); + allIds.forEach(id => { + if (this.input.panels[id] !== undefined && this.output.embeddableLoaded[id] === undefined) { + this.onPanelAdded(this.input.panels[id]); + } else if ( + this.input.panels[id] === undefined && + this.output.embeddableLoaded[id] !== undefined + ) { + this.onPanelRemoved(id); + } + }); + } +} diff --git a/src/legacy/core_plugins/embeddable_api/public/containers/embeddable_child_panel.test.tsx b/src/legacy/core_plugins/embeddable_api/public/containers/embeddable_child_panel.test.tsx new file mode 100644 index 0000000000000..7f9a513e3caba --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/containers/embeddable_child_panel.test.tsx @@ -0,0 +1,91 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import '../np_core.test.mocks'; +import { + ContactCardEmbeddable, + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + HelloWorldContainer, + CONTACT_CARD_EMBEDDABLE, +} from '../test_samples'; +import { embeddableFactories } from '../embeddables/embeddable_factories_registry'; +import React from 'react'; +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { EmbeddableChildPanel } from './embeddable_child_panel'; + +test('EmbeddableChildPanel renders an embeddable when it is done loading', async () => { + const container = new HelloWorldContainer({ id: 'hello', panels: {} }, embeddableFactories); + const newEmbeddable = await container.addNewEmbeddable< + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardEmbeddable + >(CONTACT_CARD_EMBEDDABLE, { + firstName: 'Theon', + lastName: 'Greyjoy', + id: '123', + }); + + expect(newEmbeddable.id).toBeDefined(); + + const component = mountWithIntl( + + ); + + await nextTick(); + component.update(); + + // Due to the way embeddables mount themselves on the dom node, they are not forced to be + // react components, and hence, we can't use the usual + // findTestSubject(component, 'embeddablePanelHeading-HelloTheonGreyjoy'); + expect( + component + .getDOMNode() + .querySelectorAll('[data-test-subj="embeddablePanelHeading-HelloTheonGreyjoy"]').length + ).toBe(1); +}); + +test(`EmbeddableChildPanel renders an error message if the factory doesn't exist`, async () => { + const container = new HelloWorldContainer( + { + id: 'hello', + panels: { '1': { type: 'idontexist', explicitInput: { id: '1' } } }, + }, + embeddableFactories + ); + + const component = mountWithIntl( + + ); + + await nextTick(); + component.update(); + + expect( + component.getDOMNode().querySelectorAll('[data-test-subj="embeddableStackError"]').length + ).toBe(1); +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/containers/embeddable_child_panel.tsx b/src/legacy/core_plugins/embeddable_api/public/containers/embeddable_child_panel.tsx new file mode 100644 index 0000000000000..0a5366e4b0a35 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/containers/embeddable_child_panel.tsx @@ -0,0 +1,98 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import classNames from 'classnames'; +import _ from 'lodash'; +import React from 'react'; + +import { EuiLoadingChart } from '@elastic/eui'; +import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; + +import { ErrorEmbeddable, IEmbeddable } from 'plugins/embeddable_api'; + +import { Subscription } from 'rxjs'; +import { EmbeddablePanel } from '../panel'; +import { IContainer } from './i_container'; + +export interface EmbeddableChildPanelUiProps { + intl: InjectedIntl; + embeddableId: string; + className?: string; + container: IContainer; +} + +interface State { + loading: boolean; +} + +/** + * This component can be used by embeddable containers using react to easily render children. It waits + * for the child to be initialized, showing a loading indicator until that is complete. + */ + +class EmbeddableChildPanelUi extends React.Component { + [panel: string]: any; + public mounted: boolean; + public embeddable!: IEmbeddable | ErrorEmbeddable; + private subscription?: Subscription; + + constructor(props: EmbeddableChildPanelUiProps) { + super(props); + this.state = { + loading: true, + }; + + this.mounted = false; + } + + public async componentDidMount() { + this.mounted = true; + const { container } = this.props; + + this.embeddable = await container.untilEmbeddableLoaded(this.props.embeddableId); + if (this.mounted) { + this.setState({ loading: false }); + } + } + + public componentWillUnmount() { + this.mounted = false; + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + public render() { + const classes = classNames('embPanel embPanel__content', { + 'embPanel__content-isLoading': this.state.loading, + }); + + return ( +
+ {this.state.loading || !this.embeddable ? ( + + ) : ( + + )} +
+ ); + } +} + +export const EmbeddableChildPanel = injectI18n(EmbeddableChildPanelUi); diff --git a/src/legacy/core_plugins/embeddable_api/public/containers/i_container.ts b/src/legacy/core_plugins/embeddable_api/public/containers/i_container.ts new file mode 100644 index 0000000000000..1c8374d193e98 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/containers/i_container.ts @@ -0,0 +1,122 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + Embeddable, + EmbeddableInput, + EmbeddableOutput, + ErrorEmbeddable, + EmbeddableFactory, +} from '../embeddables'; +import { IEmbeddable } from '../embeddables/i_embeddable'; +import { IRegistry } from '../types'; + +export interface PanelState< + E extends { [key: string]: unknown } & { id: string } = { [key: string]: unknown } & { + id: string; + } +> { + savedObjectId?: string; + + // The type of embeddable in this panel. Will be used to find the factory in which to + // load the embeddable. + type: string; + + // Stores input for this embeddable that is specific to this embeddable. Other parts of embeddable input + // will be derived from the container's input. **Any state in here will override any state derived from + // the container.** + explicitInput: E; +} + +export interface ContainerOutput extends EmbeddableOutput { + embeddableLoaded: { [key: string]: boolean }; +} + +export interface ContainerInput extends EmbeddableInput { + hidePanelTitles?: boolean; + panels: { + [key: string]: PanelState; + }; +} + +export interface IContainer< + I extends ContainerInput = ContainerInput, + O extends ContainerOutput = ContainerOutput +> extends IEmbeddable { + readonly embeddableFactories: IRegistry; + + /** + * Call if you want to wait until an embeddable with that id has finished loading. + */ + untilEmbeddableLoaded( + id: string + ): Promise; + + /** + * Returns the input for the given child. Uses a combination of explicit input + * for the child stored on the parent and derived/inherited input taken from the + * container itself. + * @param id + */ + getInputForChild(id: string): EEI; + + /** + * Changes the input for a given child. Note, this will override any inherited state taken from + * the container itself. + * @param id + * @param changes + */ + updateInputForChild(id: string, changes: Partial): void; + + /** + * Returns the child embeddable with the given id. + * @param id + */ + getChild = Embeddable>(id: string): E; + + /** + * Removes the embeddable with the given id. + * @param embeddableId + */ + removeEmbeddable(embeddableId: string): void; + + /** + * Adds a new embeddable that is backed off of a saved object. + */ + addSavedObjectEmbeddable< + EEI extends EmbeddableInput = EmbeddableInput, + E extends Embeddable = Embeddable + >( + type: string, + savedObjectId: string + ): Promise; + + /** + * Adds a new embeddable to the container. `explicitInput` may partially specify the required embeddable input, + * but the remainder must come from inherited container state. + */ + addNewEmbeddable< + EEI extends EmbeddableInput = EmbeddableInput, + EEO extends EmbeddableOutput = EmbeddableOutput, + E extends Embeddable = Embeddable + >( + type: string, + explicitInput: Partial + ): Promise; +} diff --git a/src/legacy/core_plugins/embeddable_api/public/containers/index.ts b/src/legacy/core_plugins/embeddable_api/public/containers/index.ts new file mode 100644 index 0000000000000..620399a8ff8d9 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/containers/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { IContainer, PanelState, ContainerInput, ContainerOutput } from './i_container'; +export { Container } from './container'; +export * from './embeddable_child_panel'; diff --git a/src/legacy/core_plugins/embeddable_api/public/containers/panel_not_found_error.ts b/src/legacy/core_plugins/embeddable_api/public/containers/panel_not_found_error.ts new file mode 100644 index 0000000000000..3bd38104dcb6e --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/containers/panel_not_found_error.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; + +export class PanelNotFoundError extends Error { + constructor() { + super( + i18n.translate('embeddableApi.errors.paneldoesNotExist', { + defaultMessage: 'Panel not found', + }) + ); + } +} diff --git a/src/legacy/core_plugins/embeddable_api/public/context_menu_actions/build_eui_context_menu_panels.ts b/src/legacy/core_plugins/embeddable_api/public/context_menu_actions/build_eui_context_menu_panels.ts new file mode 100644 index 0000000000000..5187b929373de --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/context_menu_actions/build_eui_context_menu_panels.ts @@ -0,0 +1,119 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { EuiContextMenuPanelDescriptor, EuiContextMenuPanelItemDescriptor } from '@elastic/eui'; +import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { Action, ActionContext } from '../actions'; + +/** + * Transforms an array of Actions to the shape EuiContextMenuPanel expects. + */ +export async function buildContextMenuForActions({ + actions, + actionContext, + closeMenu, +}: { + actions: Action[]; + actionContext: ActionContext; + closeMenu: () => void; +}): Promise { + const menuItems = await buildEuiContextMenuPanelItems({ + actions, + actionContext, + closeMenu, + }); + + return { + id: 'mainMenu', + title: i18n.translate('embeddableAPI.actionPanel.title', { + defaultMessage: 'Options', + }), + items: menuItems, + }; +} + +/** + * Transform an array of Actions into the shape needed to build an EUIContextMenu + */ +async function buildEuiContextMenuPanelItems({ + actions, + actionContext, + closeMenu, +}: { + actions: Action[]; + actionContext: ActionContext; + closeMenu: () => void; +}) { + const items: EuiContextMenuPanelItemDescriptor[] = []; + const promises = actions.map(async action => { + const isCompatible = await action.isCompatible(actionContext); + if (!isCompatible) { + return; + } + + items.push( + convertPanelActionToContextMenuItem({ + action, + actionContext, + closeMenu, + }) + ); + }); + + await Promise.all(promises); + + return items; +} + +/** + * + * @param {ContextMenuAction} action + * @param {Embeddable} embeddable + * @return {EuiContextMenuPanelItemDescriptor} + */ +function convertPanelActionToContextMenuItem({ + action, + actionContext, + closeMenu, +}: { + action: Action; + actionContext: ActionContext; + closeMenu: () => void; +}): EuiContextMenuPanelItemDescriptor { + const menuPanelItem: EuiContextMenuPanelItemDescriptor = { + name: action.getDisplayName(actionContext), + icon: action.getIcon(actionContext), + panel: _.get(action, 'childContextMenuPanel.id'), + 'data-test-subj': `embeddablePanelAction-${action.id}`, + }; + + if (action.getHref(actionContext) === undefined) { + menuPanelItem.onClick = () => { + action.execute(actionContext); + closeMenu(); + }; + } + + if (action.getHref(actionContext)) { + menuPanelItem.href = action.getHref(actionContext); + } + + return menuPanelItem; +} diff --git a/src/legacy/core_plugins/embeddable_api/public/context_menu_actions/index.ts b/src/legacy/core_plugins/embeddable_api/public/context_menu_actions/index.ts new file mode 100644 index 0000000000000..aa8df8b6965d8 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/context_menu_actions/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { buildContextMenuForActions } from './build_eui_context_menu_panels'; +export { openContextMenu } from './open_context_menu'; diff --git a/src/legacy/core_plugins/embeddable_api/public/context_menu_actions/open_context_menu.tsx b/src/legacy/core_plugins/embeddable_api/public/context_menu_actions/open_context_menu.tsx new file mode 100644 index 0000000000000..9ddeb6f422835 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/context_menu_actions/open_context_menu.tsx @@ -0,0 +1,161 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { EuiContextMenu, EuiContextMenuPanelDescriptor, EuiPopover } from '@elastic/eui'; +import { EventEmitter } from 'events'; +import ReactDOM from 'react-dom'; + +let activeSession: ContextMenuSession | null = null; + +const CONTAINER_ID = 'contextMenu-container'; +let initialized = false; + +function getOrCreateContainerElement() { + let container = document.getElementById(CONTAINER_ID); + const y = getMouseY() + document.body.scrollTop; + if (!container) { + container = document.createElement('div'); + container.style.left = getMouseX() + 'px'; + container.style.top = y + 'px'; + container.style.position = 'absolute'; + container.style.zIndex = '999'; + + container.id = CONTAINER_ID; + document.body.appendChild(container); + } else { + container.style.left = getMouseX() + 'px'; + container.style.top = y + 'px'; + } + return container; +} + +let x: number = 0; +let y: number = 0; + +function initialize() { + if (!initialized) { + document.addEventListener('mousemove', onMouseUpdate, false); + document.addEventListener('mouseenter', onMouseUpdate, false); + initialized = true; + } +} + +function onMouseUpdate(e: any) { + x = e.pageX; + y = e.pageY; +} + +function getMouseX() { + return x; +} + +function getMouseY() { + return y; +} + +initialize(); + +/** + * A FlyoutSession describes the session of one opened flyout panel. It offers + * methods to close the flyout panel again. If you open a flyout panel you should make + * sure you call {@link ContextMenuSession#close} when it should be closed. + * Since a flyout could also be closed without calling this method (e.g. because + * the user closes it), you must listen to the "closed" event on this instance. + * It will be emitted whenever the flyout will be closed and you should throw + * away your reference to this instance whenever you receive that event. + * @extends EventEmitter + */ +class ContextMenuSession extends EventEmitter { + /** + * Binds the current flyout session to an Angular scope, meaning this flyout + * session will be closed as soon as the Angular scope gets destroyed. + * @param {object} scope - An angular scope object to bind to. + */ + public bindToAngularScope(scope: ng.IScope): void { + const removeWatch = scope.$on('$destroy', () => this.close()); + this.on('closed', () => removeWatch()); + } + + /** + * Closes the opened flyout as long as it's still the open one. + * If this is not the active session anymore, this method won't do anything. + * If this session was still active and a flyout was closed, the 'closed' + * event will be emitted on this FlyoutSession instance. + */ + public close(): void { + if (activeSession === this) { + const container = document.getElementById(CONTAINER_ID); + if (container) { + ReactDOM.unmountComponentAtNode(container); + this.emit('closed'); + } + } + } +} + +/** + * Opens a flyout panel with the given component inside. You can use + * {@link ContextMenuSession#close} on the return value to close the flyout. + * + * @param flyoutChildren - Mounts the children inside a fly out panel + * @return {FlyoutSession} The session instance for the opened flyout panel. + */ +export function openContextMenu( + panels: EuiContextMenuPanelDescriptor[], + props: { + closeButtonAriaLabel?: string; + onClose?: () => void; + 'data-test-subj'?: string; + } = {} +): ContextMenuSession { + // If there is an active inspector session close it before opening a new one. + if (activeSession) { + activeSession.close(); + } + const container = getOrCreateContainerElement(); + const session = (activeSession = new ContextMenuSession()); + const onClose = () => { + if (props.onClose) { + props.onClose(); + } + session.close(); + }; + + ReactDOM.render( + + + , + container + ); + + return session; +} + +export { ContextMenuSession }; diff --git a/src/legacy/core_plugins/embeddable_api/public/create_registry.ts b/src/legacy/core_plugins/embeddable_api/public/create_registry.ts new file mode 100644 index 0000000000000..dbc8318c78a64 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/create_registry.ts @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IRegistry } from './types'; + +export const createRegistry = (): IRegistry => { + let data = new Map(); + + const get = (id: string) => data.get(id); + const set = (id: string, obj: T) => { + data.set(id, obj); + }; + const reset = () => { + data = new Map(); + }; + const length = () => data.size; + + const getAll = () => Array.from(data.values()); + + return { + get, + set, + reset, + length, + getAll, + }; +}; diff --git a/src/legacy/core_plugins/embeddable_api/public/embeddables/embeddable.test.tsx b/src/legacy/core_plugins/embeddable_api/public/embeddables/embeddable.test.tsx new file mode 100644 index 0000000000000..a3fba7694ed62 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/embeddables/embeddable.test.tsx @@ -0,0 +1,97 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* eslint-disable max-classes-per-file */ + +import '../np_core.test.mocks'; + +import { skip } from 'rxjs/operators'; +import { ContactCardEmbeddable, FilterableEmbeddable } from '../test_samples/index'; +import { Embeddable } from './embeddable'; +import { EmbeddableOutput, EmbeddableInput } from './i_embeddable'; +import { ViewMode } from '../types'; + +class TestClass { + constructor() {} +} + +interface Output extends EmbeddableOutput { + testClass: TestClass; + inputUpdatedTimes: number; +} + +class OutputTestEmbeddable extends Embeddable { + public readonly type = 'test'; + constructor() { + super( + { id: 'test', viewMode: ViewMode.VIEW }, + { testClass: new TestClass(), inputUpdatedTimes: 0 } + ); + + this.getInput$().subscribe(() => { + this.updateOutput({ inputUpdatedTimes: this.getOutput().inputUpdatedTimes + 1 }); + }); + } + + reload() {} +} + +test('Embeddable calls input subscribers when changed', async done => { + const hello = new ContactCardEmbeddable({ id: '123', firstName: 'Brienne', lastName: 'Tarth' }); + + const subscription = hello + .getInput$() + .pipe(skip(1)) + .subscribe(input => { + expect(input.nameTitle).toEqual('Sir'); + done(); + subscription.unsubscribe(); + }); + + hello.updateInput({ nameTitle: 'Sir' }); +}); + +test('Embeddable reload is called if lastReloadRequest input time changes', async () => { + const hello = new FilterableEmbeddable({ id: '123', filters: [], lastReloadRequestTime: 0 }); + + hello.reload = jest.fn(); + + hello.updateInput({ lastReloadRequestTime: 1 }); + + expect(hello.reload).toBeCalledTimes(1); +}); + +test('Embeddable reload is not called if lastReloadRequest input time does not change', async () => { + const hello = new FilterableEmbeddable({ id: '123', filters: [], lastReloadRequestTime: 1 }); + + hello.reload = jest.fn(); + + hello.updateInput({ lastReloadRequestTime: 1 }); + + expect(hello.reload).toBeCalledTimes(0); +}); + +test('updating output state retains instance information', async () => { + const outputTest = new OutputTestEmbeddable(); + expect(outputTest.getOutput().testClass).toBeInstanceOf(TestClass); + expect(outputTest.getOutput().inputUpdatedTimes).toBe(1); + outputTest.updateInput({ viewMode: ViewMode.EDIT }); + expect(outputTest.getOutput().inputUpdatedTimes).toBe(2); + expect(outputTest.getOutput().testClass).toBeInstanceOf(TestClass); +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/embeddables/embeddable.tsx b/src/legacy/core_plugins/embeddable_api/public/embeddables/embeddable.tsx new file mode 100644 index 0000000000000..02eb0cc639559 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/embeddables/embeddable.tsx @@ -0,0 +1,188 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { isEqual, cloneDeep } from 'lodash'; +import { Adapters } from 'ui/inspector'; +import * as Rx from 'rxjs'; +import { IContainer } from '../containers'; +import { IEmbeddable, EmbeddableInput, EmbeddableOutput } from './i_embeddable'; +import { ViewMode } from '../types'; + +function getPanelTitle(input: EmbeddableInput, output: EmbeddableOutput) { + return input.hidePanelTitles ? '' : input.title === undefined ? output.defaultTitle : input.title; +} + +export abstract class Embeddable< + TEmbeddableInput extends EmbeddableInput = EmbeddableInput, + TEmbeddableOutput extends EmbeddableOutput = EmbeddableOutput +> implements IEmbeddable { + public readonly parent?: IContainer; + public readonly isContainer: boolean = false; + public abstract readonly type: string; + public readonly id: string; + + protected output: TEmbeddableOutput; + protected input: TEmbeddableInput; + + private readonly input$: Rx.BehaviorSubject; + private readonly output$: Rx.BehaviorSubject; + + // Listener to parent changes, if this embeddable exists in a parent, in order + // to update input when the parent changes. + private parentSubscription?: Rx.Subscription; + + private destoyed: boolean = false; + + constructor(input: TEmbeddableInput, output: TEmbeddableOutput, parent?: IContainer) { + this.id = input.id; + this.output = { + title: getPanelTitle(input, output), + ...output, + }; + this.input = { + viewMode: ViewMode.EDIT, + ...input, + }; + this.parent = parent; + + this.input$ = new Rx.BehaviorSubject(this.input); + this.output$ = new Rx.BehaviorSubject(this.output); + + if (parent) { + this.parentSubscription = Rx.merge(parent.getInput$(), parent.getOutput$()).subscribe(() => { + const newInput = parent.getInputForChild(this.id); + this.onResetInput(newInput); + }); + } + } + + public getIsContainer(): this is IContainer { + return this.isContainer === true; + } + + /** + * Reload will be called when there is a request to refresh the data or view, even if the + * input data did not change. + */ + public abstract reload(): void; + + public getInput$(): Readonly> { + return this.input$.asObservable(); + } + + public getOutput$(): Readonly> { + return this.output$.asObservable(); + } + + public getOutput(): Readonly { + return this.output; + } + + public getInput(): Readonly { + return this.input; + } + + public getTitle() { + return this.output.title; + } + + /** + * Returns the top most parent embeddable, or itself if this embeddable + * is not within a parent. + */ + public getRoot(): IEmbeddable | IContainer { + let root: IEmbeddable | IContainer = this; + while (root.parent) { + root = root.parent; + } + return root; + } + + public updateInput(changes: Partial): void { + if (this.destoyed) { + throw new Error('Embeddable has been destroyed'); + } + if (this.parent) { + // Ensures state changes flow from container downward. + this.parent.updateInputForChild(this.id, changes); + } else { + this.onInputChanged(changes); + } + } + + public render(domNode: HTMLElement | Element): void { + if (this.destoyed) { + throw new Error('Embeddable has been destroyed'); + } + return; + } + + /** + * An embeddable can return inspector adapters if it want the inspector to be + * available via the context menu of that panel. + * @return Inspector adapters that will be used to open an inspector for. + */ + public getInspectorAdapters(): Adapters | undefined { + return undefined; + } + + /** + * Called when this embeddable is no longer used, this should be the place for + * implementors to add any additional clean up tasks, like unmounting and unsubscribing. + */ + public destroy(): void { + this.destoyed = true; + if (this.parentSubscription) { + this.parentSubscription.unsubscribe(); + } + return; + } + + protected updateOutput(outputChanges: Partial): void { + const newOutput = { + ...this.output, + ...outputChanges, + }; + if (!isEqual(this.output, newOutput)) { + this.output = newOutput; + this.output$.next(this.output); + } + } + + private onResetInput(newInput: TEmbeddableInput) { + if (!isEqual(this.input, newInput)) { + if (this.input.lastReloadRequestTime !== newInput.lastReloadRequestTime) { + this.reload(); + } + this.input = newInput; + this.input$.next(newInput); + this.updateOutput({ + title: getPanelTitle(this.input, this.output), + } as Partial); + } + } + + private onInputChanged(changes: Partial) { + const newInput = cloneDeep({ + ...this.input, + ...changes, + }); + + this.onResetInput(newInput); + } +} diff --git a/src/legacy/core_plugins/embeddable_api/public/embeddables/embeddable_factories_registry.ts b/src/legacy/core_plugins/embeddable_api/public/embeddables/embeddable_factories_registry.ts new file mode 100644 index 0000000000000..ca648a62b412b --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/embeddables/embeddable_factories_registry.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { EmbeddableFactory } from './embeddable_factory'; +import { createRegistry } from '../create_registry'; + +export const embeddableFactories = createRegistry(); diff --git a/src/legacy/core_plugins/embeddable_api/public/embeddables/embeddable_factory.ts b/src/legacy/core_plugins/embeddable_api/public/embeddables/embeddable_factory.ts new file mode 100644 index 0000000000000..545514a452f95 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/embeddables/embeddable_factory.ts @@ -0,0 +1,141 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObjectMetaData } from 'ui/saved_objects/components/saved_object_finder'; +import { SavedObjectAttributes } from '../../../../server/saved_objects'; +import { EmbeddableInput, EmbeddableOutput } from './i_embeddable'; +import { ErrorEmbeddable } from './error_embeddable'; +import { IContainer } from '../containers/i_container'; +import { IEmbeddable } from './i_embeddable'; + +export interface EmbeddableInstanceConfiguration { + id: string; + savedObjectId?: string; +} + +export interface PropertySpec { + displayName: string; + accessPath: string; + id: string; + description: string; + value?: string; +} + +export interface OutputSpec { + [key: string]: PropertySpec; +} + +/** + * The EmbeddableFactory creates and initializes an embeddable instance + */ +export abstract class EmbeddableFactory< + TEmbeddableInput extends EmbeddableInput = EmbeddableInput, + TEmbeddableOutput extends EmbeddableOutput = EmbeddableOutput, + TEmbeddable extends IEmbeddable = IEmbeddable< + TEmbeddableInput, + TEmbeddableOutput + >, + TSavedObjectAttributes extends SavedObjectAttributes = SavedObjectAttributes +> { + // A unique identified for this factory, which will be used to map an embeddable spec to + // a factory that can generate an instance of it. + public abstract readonly type: string; + + public readonly savedObjectMetaData?: SavedObjectMetaData; + + /** + * True is this factory create embeddables that are Containers. Used in the add panel to + * conditionally show whether these can be added to another container. It's just not + * supported right now, but once nested containers are officially supported we can probably get + * rid of this interface. + */ + public readonly isContainerType: boolean = false; + + constructor({ + savedObjectMetaData, + }: { + savedObjectMetaData?: SavedObjectMetaData; + } = {}) { + this.savedObjectMetaData = savedObjectMetaData; + } + + /** + * Returns whether the current user should be allowed to edit this type of + * embeddable. + */ + public abstract isEditable(): boolean; + + /** + * Returns a display name for this type of embeddable. Used in "Create new... " options + * in the add panel for containers. + */ + public abstract getDisplayName(): string; + + /** + * If false, this type of embeddable can't be created with the "createNew" functionality. Instead, + * use createFromSavedObject, where an existing saved object must first exist. + */ + public canCreateNew() { + return true; + } + + /** + * Can be used to get any default input, to be passed in to during the creation process. Default + * input will not be stored in a parent container, so any inherited input from a container will trump + * default input parameters. + * @param partial + */ + public getDefaultInput(partial: Partial): Partial { + return {}; + } + + /** + * Can be used to request explicit input from the user, to be passed in to `EmbeddableFactory:create`. + * Explicit input is stored on the parent container for this embeddable. It overrides any inherited + * input passed down from the parent container. + */ + public async getExplicitInput(): Promise> { + return {}; + } + + /** + * Creates a new embeddable instance based off the saved object id. + * @param savedObjectId + * @param input - some input may come from a parent, or user, if it's not stored with the saved object. For example, the time + * range of the parent container. + * @param parent + */ + public createFromSavedObject( + savedObjectId: string, + input: Partial, + parent?: IContainer + ): Promise { + throw new Error(`Creation from saved object not supported by type ${this.type}`); + } + + /** + * Resolves to undefined if a new Embeddable cannot be directly created and the user will instead be redirected + * elsewhere. + * This will likely change in future iterations when we improve in place editing capabilities. + */ + public abstract create( + initialInput: TEmbeddableInput, + parent?: IContainer + ): Promise; +} diff --git a/src/legacy/core_plugins/embeddable_api/public/embeddables/embeddable_factory_not_found_error.ts b/src/legacy/core_plugins/embeddable_api/public/embeddables/embeddable_factory_not_found_error.ts new file mode 100644 index 0000000000000..7b3015e56c8ef --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/embeddables/embeddable_factory_not_found_error.ts @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; + +export class EmbeddableFactoryNotFoundError extends Error { + constructor(type: string) { + super( + i18n.translate('embeddableApi.errors.embeddableFactoryNotFound', { + defaultMessage: `{type} can't be loaded. Please upgrade to the default distribution of Elasticsearch and Kibana with the appropriate license.`, + values: { + type, + }, + }) + ); + } +} diff --git a/src/legacy/core_plugins/embeddable_api/public/embeddables/error_embeddable.tsx b/src/legacy/core_plugins/embeddable_api/public/embeddables/error_embeddable.tsx new file mode 100644 index 0000000000000..2c2c47775369d --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/embeddables/error_embeddable.tsx @@ -0,0 +1,68 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { EuiText, EuiIcon, EuiSpacer } from '@elastic/eui'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Embeddable } from './embeddable'; +import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from './i_embeddable'; +import { IContainer } from '../containers'; + +export const ERROR_EMBEDDABLE_TYPE = 'error'; + +export function isErrorEmbeddable( + embeddable: TEmbeddable | ErrorEmbeddable +): embeddable is ErrorEmbeddable { + return (embeddable as ErrorEmbeddable).error !== undefined; +} + +export class ErrorEmbeddable extends Embeddable { + public readonly type = ERROR_EMBEDDABLE_TYPE; + public error: Error | string; + private dom?: HTMLElement; + + constructor(error: Error | string, input: EmbeddableInput, parent?: IContainer) { + super(input, {}, parent); + this.error = error; + } + + public reload() {} + + public render(dom: HTMLElement) { + const title = typeof this.error === 'string' ? this.error : this.error.message; + this.dom = dom; + ReactDOM.render( + // @ts-ignore +
+ + + + {title} + +
, + dom + ); + } + + public destroy() { + if (this.dom) { + ReactDOM.unmountComponentAtNode(this.dom); + } + } +} diff --git a/src/legacy/core_plugins/embeddable_api/public/embeddables/i_embeddable.ts b/src/legacy/core_plugins/embeddable_api/public/embeddables/i_embeddable.ts new file mode 100644 index 0000000000000..066b47a01fab4 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/embeddables/i_embeddable.ts @@ -0,0 +1,143 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Adapters } from 'ui/inspector'; +import { Observable } from 'rxjs'; +import { IContainer } from '../containers'; +import { ViewMode } from '../types'; +export interface EmbeddableInput { + viewMode?: ViewMode; + title?: string; + id: string; + lastReloadRequestTime?: number; + hidePanelTitles?: boolean; +} + +export interface EmbeddableOutput { + editUrl?: string; + defaultTitle?: string; + title?: string; + editable?: boolean; + savedObjectId?: string; +} + +export interface IEmbeddable< + I extends EmbeddableInput = EmbeddableInput, + O extends EmbeddableOutput = EmbeddableOutput +> { + /** + * Is this embeddable an instance of a Container class, can it contain + * nested embeddables? + **/ + readonly isContainer: boolean; + + /** + * If this embeddable is nested inside a container, this will contain + * a reference to its parent. + **/ + readonly parent?: IContainer; + + /** + * The type of embeddable, this is what will be used to take a serialized + * embeddable and find the correct factory for which to create an instance of it. + **/ + readonly type: string; + + /** + * A unique identifier for this embeddable. Mainly only used by containers to map their + * Panel States to a child embeddable instance. + **/ + readonly id: string; + + /** + * A functional representation of the isContainer variable, but helpful for typescript to + * know the shape if this returns true + */ + getIsContainer(): this is IContainer; + + /** + * Get the input used to instantiate this embeddable. The input is a serialized representation of + * this embeddable instance and can be used to clone or re-instantiate it. Input state: + * - Can be updated externally + * - Can change multiple times for a single embeddable instance. + * Examples: title, pie slice colors, custom search columns and sort order. + **/ + getInput(): Readonly; + + /** + * Output state is: + * - State that should not change once the embeddable is instantiated, or + * - State that is derived from the input state, or + * - State that only the embeddable instance itself knows about, or the factory. + * Examples: editUrl, title taken from a saved object, if your input state was first name and + * last name, your output state could be greeting. + **/ + getOutput(): Readonly; + + /** + * Updates input state with the given changes. + * @param changes + */ + updateInput(changes: Partial): void; + + /** + * Returns an observable which will be notified when input state changes. + */ + getInput$(): Readonly>; + + /** + * Returns an observable which will be notified when output state changes. + */ + getOutput$(): Readonly>; + + /** + * Returns the title of this embeddable. + */ + getTitle(): string | undefined; + + /** + * Returns the top most parent embeddable, or itself if this embeddable + * is not within a parent. + */ + getRoot(): IEmbeddable | IContainer; + + /** + * Renders the embeddable at the given node. + * @param domNode + */ + render(domNode: HTMLElement | Element): void; + + /** + * Reload the embeddable so output and rendering is up to date. Especially relevant + * if the embeddable takes relative time as input (e.g. now to now-15) + */ + reload(): void; + + /** + * An embeddable can return inspector adapters if it want the inspector to be + * available via the context menu of that panel. + * @return Inspector adapters that will be used to open an inspector for. + */ + getInspectorAdapters(): Adapters | undefined; + + /** + * Cleans up subscriptions, destroy nodes mounted from calls to render. + */ + destroy(): void; +} diff --git a/src/legacy/core_plugins/embeddable_api/public/embeddables/index.ts b/src/legacy/core_plugins/embeddable_api/public/embeddables/index.ts new file mode 100644 index 0000000000000..2e346b4f3bd8a --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/embeddables/index.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +export { EmbeddableOutput, EmbeddableInput, IEmbeddable } from './i_embeddable'; +export { Embeddable } from './embeddable'; +export { + EmbeddableInstanceConfiguration, + EmbeddableFactory, + OutputSpec, +} from './embeddable_factory'; +export { embeddableFactories } from './embeddable_factories_registry'; +export { ErrorEmbeddable, isErrorEmbeddable } from './error_embeddable'; + +export { EmbeddableFactoryNotFoundError } from './embeddable_factory_not_found_error'; diff --git a/src/legacy/core_plugins/embeddable_api/public/get_actions_for_trigger.test.ts b/src/legacy/core_plugins/embeddable_api/public/get_actions_for_trigger.test.ts new file mode 100644 index 0000000000000..14fafacd5de69 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/get_actions_for_trigger.test.ts @@ -0,0 +1,134 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import './np_core.test.mocks'; + +import { + HelloWorldAction, + SayHelloAction, + EmptyEmbeddable, + RestrictedAction, +} from './test_samples/index'; +import { actionRegistry, ActionContext } from './actions'; +import { SAY_HELLO_ACTION } from './test_samples/actions/say_hello_action'; +import { triggerRegistry } from './triggers'; +import { HELLO_WORLD_ACTION_ID } from './test_samples'; +import { getActionsForTrigger } from './get_actions_for_trigger'; +import { attachAction } from './triggers/attach_action'; + +beforeEach(() => { + actionRegistry.reset(); + triggerRegistry.reset(); +}); + +afterAll(() => { + actionRegistry.reset(); + triggerRegistry.reset(); +}); + +test('ActionRegistry adding and getting an action', async () => { + const sayHelloAction = new SayHelloAction(() => {}); + const helloWorldAction = new HelloWorldAction(); + + actionRegistry.set(sayHelloAction.id, sayHelloAction); + actionRegistry.set(helloWorldAction.id, helloWorldAction); + + expect(actionRegistry.length()).toBe(2); + + expect(actionRegistry.get(sayHelloAction.id)).toBe(sayHelloAction); + expect(actionRegistry.get(helloWorldAction.id)).toBe(helloWorldAction); +}); + +test(`ActionRegistry getting an action that doesn't exist returns undefined`, async () => { + expect(actionRegistry.get(SAY_HELLO_ACTION)).toBeUndefined(); +}); + +test('getActionsForTrigger returns attached actions', async () => { + const embeddable = new EmptyEmbeddable({ id: '123' }); + const helloWorldAction = new HelloWorldAction(); + actionRegistry.set(helloWorldAction.id, helloWorldAction); + + const testTrigger = { + id: 'MYTRIGGER', + title: 'My trigger', + actionIds: [], + }; + triggerRegistry.set(testTrigger.id, testTrigger); + + attachAction(triggerRegistry, { triggerId: 'MYTRIGGER', actionId: HELLO_WORLD_ACTION_ID }); + + const moreActions = await getActionsForTrigger(actionRegistry, triggerRegistry, 'MYTRIGGER', { + embeddable, + }); + + expect(moreActions.length).toBe(1); +}); + +test('getActionsForTrigger filters out actions not applicable based on the context', async () => { + const action = new RestrictedAction((context: ActionContext) => { + return context.embeddable.id === 'accept'; + }); + actionRegistry.set(action.id, action); + const acceptEmbeddable = new EmptyEmbeddable({ id: 'accept' }); + const rejectEmbeddable = new EmptyEmbeddable({ id: 'reject' }); + + const testTrigger = { + id: 'MYTRIGGER', + title: 'My trigger', + actionIds: [action.id], + }; + triggerRegistry.set(testTrigger.id, testTrigger); + + let actions = await getActionsForTrigger(actionRegistry, triggerRegistry, testTrigger.id, { + embeddable: acceptEmbeddable, + }); + + expect(actions.length).toBe(1); + + actions = await getActionsForTrigger(actionRegistry, triggerRegistry, testTrigger.id, { + embeddable: rejectEmbeddable, + }); + + expect(actions.length).toBe(0); +}); + +test(`getActionsForTrigger with an invalid trigger id throws an error`, async () => { + async function check() { + await getActionsForTrigger(actionRegistry, triggerRegistry, 'I do not exist', { + embeddable: new EmptyEmbeddable({ id: 'empty' }), + }); + } + await expect(check()).rejects.toThrow(Error); +}); + +test(`getActionsForTrigger with a trigger mapping that maps to an non existant action throws an error`, async () => { + const testTrigger = { + id: '123', + title: '123', + actionIds: ['I do not exist'], + }; + triggerRegistry.set(testTrigger.id, testTrigger); + + async function check() { + await getActionsForTrigger(actionRegistry, triggerRegistry, '123', { + embeddable: new EmptyEmbeddable({ id: 'empty' }), + }); + } + await expect(check()).rejects.toThrow(Error); +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/get_actions_for_trigger.ts b/src/legacy/core_plugins/embeddable_api/public/get_actions_for_trigger.ts new file mode 100644 index 0000000000000..6574e10d6002c --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/get_actions_for_trigger.ts @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Action } from './actions'; +import { IEmbeddable } from './embeddables'; +import { IContainer } from './containers'; +import { IRegistry, Trigger } from './types'; + +export async function getActionsForTrigger( + actionRegistry: IRegistry, + triggerRegistry: IRegistry, + triggerId: string, + context: { embeddable: IEmbeddable; container?: IContainer } +) { + const trigger = triggerRegistry.get(triggerId); + + if (!trigger) { + throw new Error(`Trigger with id ${triggerId} does not exist`); + } + + const actions: Action[] = []; + const promises = trigger.actionIds.map(async id => { + const action = actionRegistry.get(id); + if (!action) { + throw new Error(`Action ${id} does not exist`); + } + + if (await action.isCompatible(context)) { + actions.push(action); + } + }); + + await Promise.all(promises); + + return actions; +} diff --git a/src/legacy/core_plugins/embeddable_api/public/index.scss b/src/legacy/core_plugins/embeddable_api/public/index.scss new file mode 100644 index 0000000000000..8f01ebbf0443d --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/index.scss @@ -0,0 +1,13 @@ +@import 'src/legacy/ui/public/styles/styling_constants'; + +// Prefix all styles with "emb" to avoid conflicts. +// Examples +// embChart +// embChart__legend +// embChart__legend--small +// embChart__legend-isLoading + +@import './variables'; + +@import './panel/index'; + diff --git a/src/legacy/core_plugins/embeddable_api/public/index.ts b/src/legacy/core_plugins/embeddable_api/public/index.ts new file mode 100644 index 0000000000000..287b893202d6c --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/index.ts @@ -0,0 +1,61 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +export { + IEmbeddable, + EmbeddableFactory, + EmbeddableInstanceConfiguration, + Embeddable, + embeddableFactories, + OutputSpec, + ErrorEmbeddable, + EmbeddableInput, + EmbeddableOutput, + isErrorEmbeddable, +} from './embeddables'; + +export { + Query, + TimeRange, + RefreshConfig, + ViewMode, + QueryLanguageType, + Trigger, + IRegistry, +} from './types'; + +export { actionRegistry, Action, ActionContext, IncompatibleActionError } from './actions'; + +export { + APPLY_FILTER_TRIGGER, + triggerRegistry, + executeTriggerActions, + CONTEXT_MENU_TRIGGER, + attachAction, +} from './triggers'; + +export { + Container, + ContainerInput, + ContainerOutput, + PanelState, + IContainer, + EmbeddableChildPanel, +} from './containers'; + +export { AddPanelAction, EmbeddablePanel, openAddPanelFlyout } from './panel'; diff --git a/src/legacy/core_plugins/embeddable_api/public/np_core.test.mocks.ts b/src/legacy/core_plugins/embeddable_api/public/np_core.test.mocks.ts new file mode 100644 index 0000000000000..dff1cf84f1196 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/np_core.test.mocks.ts @@ -0,0 +1,63 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { fatalErrorsServiceMock, notificationServiceMock } from '../../../../core/public/mocks'; + +let modalContents: React.Component; + +export const getModalContents = () => modalContents; + +jest.doMock('ui/new_platform', () => { + return { + npStart: { + core: { + overlays: { + openFlyout: jest.fn(), + openModal: (component: React.Component) => { + modalContents = component; + return { + close: jest.fn(), + }; + }, + }, + }, + }, + npSetup: { + core: { + fatalErrors: fatalErrorsServiceMock.createSetupContract(), + notifications: notificationServiceMock.createSetupContract(), + }, + }, + }; +}); + +jest.doMock('ui/metadata', () => ({ + metadata: { + branch: 'my-metadata-branch', + version: 'my-metadata-version', + }, +})); + +jest.doMock('ui/capabilities', () => ({ + uiCapabilities: { + visualize: { + save: true, + }, + }, +})); diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/_embeddable_panel.scss b/src/legacy/core_plugins/embeddable_api/public/panel/_embeddable_panel.scss new file mode 100644 index 0000000000000..64b8f0f720103 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/_embeddable_panel.scss @@ -0,0 +1,130 @@ +.embPanel { + z-index: auto; + flex: 1; + display: flex; + flex-direction: column; + height: 100%; + min-height: $euiSizeL + 2px; // + 2px to account for border + position: relative; + + // SASSTODO: The inheritence factor stemming from embeddables makes this class hard to change + .embPanel__content { + display: flex; + flex: 1 1 100%; + z-index: 1; + min-height: 0; // Absolute must for Firefox to scroll contents + } + + // SASSTODO: Pretty sure this doesn't do anything since the flex-basis 100%, + // but it MIGHT be fixing IE + .embPanel__content--fullWidth { + width: 100%; + } + + .embPanel__content-isLoading { + // completely center the loading indicator + justify-content: center; + align-items: center; + } +} + +// HEADER + +.embPanel__header { + flex: 0 0 auto; + display: flex; + // ensure menu button is on the right even if the title doesn't exist + justify-content: flex-end; +} + +.embPanel__title { + @include euiTextTruncate; + @include euiTitle('xxxs'); + line-height: 1.5; + flex-grow: 1; + + &:not(:empty) { + padding: ($euiSizeXS * 1.5) $euiSizeS 0; + } +} + +.embPanel__dragger:not(.embPanel__title) { + flex-grow: 1; +} + +.embPanel__header--floater { + position: absolute; + right: 0; + top: 0; + left: 0; + z-index: $euiZLevel1; +} + +// OPTIONS MENU + +/** + * 1. Use opacity to make this element accessible to screen readers and keyboard. + * 2. Show on focus to enable keyboard accessibility. + * 3. Always show in editing mode + */ + +.embPanel__optionsMenuButton { + background-color: transparentize($euiColorDarkestShade, .9); + border-bottom-right-radius: 0; + border-top-left-radius: 0; + + &:focus { + background-color: $euiFocusBackgroundColor; + } +} + +.embPanel +.embPanel__optionsMenuButton { + opacity: 0; /* 1 */ + + &:focus { + opacity: 1; /* 2 */ + } +} + +.embPanel__optionsMenuPopover[class*="-isOpen"], +.embPanel:hover { + .embPanel__optionsMenuButton { + opacity: 1; + } +} + +// EDITING MODE + +.embPanel--editing { + border-style: dashed; + border-color: $euiColorMediumShade; + transition: all $euiAnimSpeedFast $euiAnimSlightResistance; + + &:hover, + &:focus { + @include euiSlightShadowHover; + } + + .embPanel__dragger { + transition: background-color $euiAnimSpeedFast $euiAnimSlightResistance; + + &:hover { + background-color: $embEditingModeHoverColor; + cursor: move; + } + } + + .embPanel__optionsMenuButton { + opacity: 1; /* 3 */ + } +} + +.embPanel__error { + text-align: center; + justify-content: center; + flex-direction: column; + overflow: auto; + text-align: center; + padding: $euiSizeS; +} \ No newline at end of file diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/_index.scss b/src/legacy/core_plugins/embeddable_api/public/panel/_index.scss new file mode 100644 index 0000000000000..8ff319f660072 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/_index.scss @@ -0,0 +1,3 @@ +@import './embeddable_panel'; + +@import './panel_header/index'; diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/embeddable_panel.test.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/embeddable_panel.test.tsx new file mode 100644 index 0000000000000..a0207a1de0c2b --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/embeddable_panel.test.tsx @@ -0,0 +1,248 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import '../np_core.test.mocks'; + +import React from 'react'; + +import { + ContactCardEmbeddable, + ContactCardEmbeddableInput, + CONTACT_CARD_EMBEDDABLE, + ContactCardEmbeddableFactory, + HelloWorldContainer, + EditModeAction, + ContactCardEmbeddableOutput, +} from '../test_samples'; +import { + isErrorEmbeddable, + ViewMode, + actionRegistry, + triggerRegistry, + EmbeddablePanel, +} from '../../../embeddable_api/public'; +import { mount } from 'enzyme'; +import { nextTick } from 'test_utils/enzyme_helpers'; + +// @ts-ignore +import { findTestSubject } from '@elastic/eui/lib/test'; +import { I18nProvider } from '@kbn/i18n/react'; +import { CONTEXT_MENU_TRIGGER } from '../triggers'; +import { attachAction } from '../triggers/attach_action'; +import { createRegistry } from '../create_registry'; +import { EmbeddableFactory } from '../embeddables'; + +const editModeAction = new EditModeAction(); +actionRegistry.set(editModeAction.id, editModeAction); +attachAction(triggerRegistry, { + triggerId: CONTEXT_MENU_TRIGGER, + actionId: editModeAction.id, +}); + +const embeddableFactories = createRegistry(); +embeddableFactories.set(CONTACT_CARD_EMBEDDABLE, new ContactCardEmbeddableFactory()); + +test('HelloWorldContainer initializes embeddables', async done => { + const container = new HelloWorldContainer( + { + id: '123', + panels: { + '123': { + explicitInput: { id: '123', firstName: 'Sam' }, + type: CONTACT_CARD_EMBEDDABLE, + }, + }, + }, + embeddableFactories + ); + + const subscription = container.getOutput$().subscribe(() => { + if (container.getOutput().embeddableLoaded['123']) { + const embeddable = container.getChild('123'); + expect(embeddable).toBeDefined(); + expect(embeddable.id).toBe('123'); + done(); + } + }); + + if (container.getOutput().embeddableLoaded['123']) { + const embeddable = container.getChild('123'); + expect(embeddable).toBeDefined(); + expect(embeddable.id).toBe('123'); + subscription.unsubscribe(); + done(); + } +}); + +test('HelloWorldContainer.addNewEmbeddable', async () => { + const container = new HelloWorldContainer({ id: '123', panels: {} }, embeddableFactories); + const embeddable = await container.addNewEmbeddable( + CONTACT_CARD_EMBEDDABLE, + { + firstName: 'Kibana', + } + ); + expect(embeddable).toBeDefined(); + + if (!isErrorEmbeddable(embeddable)) { + expect(embeddable.getInput().firstName).toBe('Kibana'); + } else { + expect(false).toBe(true); + } + + const embeddableInContainer = container.getChild(embeddable.id); + expect(embeddableInContainer).toBeDefined(); + expect(embeddableInContainer.id).toBe(embeddable.id); +}); + +test('Container view mode change propagates to children', async () => { + const container = new HelloWorldContainer( + { id: '123', panels: {}, viewMode: ViewMode.VIEW }, + embeddableFactories + ); + const embeddable = await container.addNewEmbeddable< + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardEmbeddable + >(CONTACT_CARD_EMBEDDABLE, { + firstName: 'Bob', + }); + + expect(embeddable.getInput().viewMode).toBe(ViewMode.VIEW); + + container.updateInput({ viewMode: ViewMode.EDIT }); + + expect(embeddable.getInput().viewMode).toBe(ViewMode.EDIT); +}); + +test('HelloWorldContainer in view mode hides edit mode actions', async () => { + const container = new HelloWorldContainer( + { id: '123', panels: {}, viewMode: ViewMode.VIEW }, + embeddableFactories + ); + + const embeddable = await container.addNewEmbeddable< + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardEmbeddable + >(CONTACT_CARD_EMBEDDABLE, { + firstName: 'Bob', + }); + + const component = mount( + + + + ); + + findTestSubject(component, 'embeddablePanelToggleMenuIcon').simulate('click'); + expect(findTestSubject(component, `embeddablePanelContextMenuOpen`).length).toBe(1); + await nextTick(); + component.update(); + expect(findTestSubject(component, `embeddablePanelAction-${editModeAction.id}`).length).toBe(0); +}); + +test('HelloWorldContainer in edit mode shows edit mode actions', async () => { + const container = new HelloWorldContainer( + { id: '123', panels: {}, viewMode: ViewMode.VIEW }, + embeddableFactories + ); + + const embeddable = await container.addNewEmbeddable< + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardEmbeddable + >(CONTACT_CARD_EMBEDDABLE, { + firstName: 'Bob', + }); + + const component = mount( + + + + ); + + const button = findTestSubject(component, 'embeddablePanelToggleMenuIcon'); + + expect(button.length).toBe(1); + findTestSubject(component, 'embeddablePanelToggleMenuIcon').simulate('click'); + + expect(findTestSubject(component, `embeddablePanelContextMenuOpen`).length).toBe(1); + await nextTick(); + component.update(); + expect(findTestSubject(component, `embeddablePanelAction-${editModeAction.id}`).length).toBe(0); + + container.updateInput({ viewMode: ViewMode.EDIT }); + await nextTick(); + component.update(); + + // Need to close and re-open to refresh. It doesn't update automatically. + findTestSubject(component, 'embeddablePanelToggleMenuIcon').simulate('click'); + await nextTick(); + findTestSubject(component, 'embeddablePanelToggleMenuIcon').simulate('click'); + await nextTick(); + expect(findTestSubject(component, 'embeddablePanelContextMenuOpen').length).toBe(1); + + await nextTick(); + component.update(); + + const action = findTestSubject(component, `embeddablePanelAction-${editModeAction.id}`); + expect(action.length).toBe(1); +}); + +test('Updates when hidePanelTitles is toggled', async () => { + const container = new HelloWorldContainer( + { id: '123', panels: {}, viewMode: ViewMode.VIEW, hidePanelTitles: false }, + embeddableFactories + ); + + const embeddable = await container.addNewEmbeddable< + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardEmbeddable + >(CONTACT_CARD_EMBEDDABLE, { + firstName: 'Rob', + lastName: 'Stark', + }); + + const component = mount( + + + + ); + + let title = findTestSubject(component, `embeddablePanelHeading-HelloRobStark`); + expect(title.length).toBe(1); + + await container.updateInput({ hidePanelTitles: true }); + + await nextTick(); + component.update(); + + title = findTestSubject(component, `embeddablePanelHeading-HelloRobStark`); + expect(title.length).toBe(0); + + await container.updateInput({ hidePanelTitles: false }); + + await nextTick(); + component.update(); + + title = findTestSubject(component, `embeddablePanelHeading-HelloRobStark`); + expect(title.length).toBe(1); +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/embeddable_panel.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/embeddable_panel.tsx new file mode 100644 index 0000000000000..015d2a49af0e6 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/embeddable_panel.tsx @@ -0,0 +1,186 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { EuiContextMenuPanelDescriptor, EuiPanel } from '@elastic/eui'; +import classNames from 'classnames'; +import React from 'react'; +import { Subscription } from 'rxjs'; +import { buildContextMenuForActions } from '../context_menu_actions'; + +import { CONTEXT_MENU_TRIGGER, triggerRegistry } from '../triggers'; +import { IEmbeddable } from '../embeddables/i_embeddable'; +import { ViewMode } from '../types'; + +import { RemovePanelAction } from './panel_header/panel_actions'; +import { AddPanelAction } from './panel_header/panel_actions/add_panel/add_panel_action'; +import { CustomizePanelTitleAction } from './panel_header/panel_actions/customize_title/customize_panel_action'; +import { PanelHeader } from './panel_header/panel_header'; +import { actionRegistry } from '../actions'; +import { InspectPanelAction } from './panel_header/panel_actions/inspect_panel_action'; +import { EditPanelAction } from './panel_header/panel_actions/edit_panel_action'; +import { getActionsForTrigger } from '../get_actions_for_trigger'; + +interface Props { + embeddable: IEmbeddable; +} + +interface State { + panels: EuiContextMenuPanelDescriptor[]; + focusedPanelIndex?: string; + viewMode: ViewMode; + hidePanelTitles: boolean; + closeContextMenu: boolean; +} + +export class EmbeddablePanel extends React.Component { + private embeddableRoot: React.RefObject; + private parentSubscription?: Subscription; + private subscription?: Subscription; + private mounted: boolean = false; + constructor(props: Props) { + super(props); + const { embeddable } = this.props; + const viewMode = embeddable.getInput().viewMode + ? embeddable.getInput().viewMode + : ViewMode.EDIT; + const hidePanelTitles = embeddable.parent + ? Boolean(embeddable.parent.getInput().hidePanelTitles) + : false; + + this.state = { + panels: [], + viewMode, + hidePanelTitles, + closeContextMenu: false, + }; + + this.embeddableRoot = React.createRef(); + } + + public componentWillMount() { + this.mounted = true; + const { embeddable } = this.props; + const { parent } = embeddable; + + this.subscription = embeddable.getInput$().subscribe(async () => { + if (this.mounted) { + this.setState({ + viewMode: embeddable.getInput().viewMode ? embeddable.getInput().viewMode : ViewMode.EDIT, + }); + } + }); + + if (parent) { + this.parentSubscription = parent.getInput$().subscribe(async () => { + if (this.mounted && parent) { + this.setState({ + hidePanelTitles: Boolean(parent.getInput().hidePanelTitles), + }); + } + }); + } + } + + public componentWillUnmount() { + this.mounted = false; + if (this.subscription) { + this.subscription.unsubscribe(); + } + if (this.parentSubscription) { + this.parentSubscription.unsubscribe(); + } + this.props.embeddable.destroy(); + } + + public onFocus = (focusedPanelIndex: string) => { + this.setState({ focusedPanelIndex }); + }; + + public onBlur = (blurredPanelIndex: string) => { + if (this.state.focusedPanelIndex === blurredPanelIndex) { + this.setState({ focusedPanelIndex: undefined }); + } + }; + + public render() { + const viewOnlyMode = this.state.viewMode === ViewMode.VIEW; + const classes = classNames('embPanel', { + 'embPanel--editing': !viewOnlyMode, + }); + const title = this.props.embeddable.getTitle(); + return ( + + +
+ + ); + } + + public componentDidMount() { + if (this.embeddableRoot.current) { + this.props.embeddable.render(this.embeddableRoot.current); + } + } + + closeMyContextMenuPanel = () => { + if (this.mounted) { + this.setState({ closeContextMenu: true }, () => { + if (this.mounted) { + this.setState({ closeContextMenu: false }); + } + }); + } + }; + + private getActionContextMenuPanel = async () => { + const actions = await getActionsForTrigger( + actionRegistry, + triggerRegistry, + CONTEXT_MENU_TRIGGER, + { + embeddable: this.props.embeddable, + } + ); + + // These actions are exposed on the context menu for every embeddable, they bypass the trigger + // registry. + const extraActions = [ + new CustomizePanelTitleAction(), + new AddPanelAction(), + new InspectPanelAction(), + new RemovePanelAction(), + new EditPanelAction(), + ]; + + const sorted = actions.concat(extraActions).sort((a, b) => { + return b.order - a.order; + }); + + return await buildContextMenuForActions({ + actions: sorted, + actionContext: { embeddable: this.props.embeddable }, + closeMenu: this.closeMyContextMenuPanel, + }); + }; +} diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/index.ts b/src/legacy/core_plugins/embeddable_api/public/panel/index.ts new file mode 100644 index 0000000000000..dee52bc5bec50 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { EmbeddablePanel } from './embeddable_panel'; +export { ADD_PANEL_ACTION_ID, AddPanelAction, openAddPanelFlyout } from './panel_header'; diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/_index.scss b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/_index.scss new file mode 100644 index 0000000000000..b6cea833f65cf --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/_index.scss @@ -0,0 +1 @@ +@import './panel_options_menu_form'; diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/_panel_options_menu_form.scss b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/_panel_options_menu_form.scss new file mode 100644 index 0000000000000..cdf0fb79f732a --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/_panel_options_menu_form.scss @@ -0,0 +1,3 @@ +.embPanel__optionsMenuForm { + padding: $euiSize; +} diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/index.ts b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/index.ts new file mode 100644 index 0000000000000..e5975b06ba1e9 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/index.ts @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { + ADD_PANEL_ACTION_ID, + AddPanelAction, + RemovePanelAction, + openAddPanelFlyout, +} from './panel_actions'; diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx new file mode 100644 index 0000000000000..c375494d497c8 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx @@ -0,0 +1,120 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import '../../../../np_core.test.mocks'; + +import { + FilterableContainer, + FilterableEmbeddable, + FilterableEmbeddableFactory, + ContactCardEmbeddable, + FilterableEmbeddableInput, + FILTERABLE_EMBEDDABLE, +} from '../../../../test_samples'; + +import { ViewMode, EmbeddableOutput, isErrorEmbeddable } from '../../../../'; +import { AddPanelAction } from './add_panel_action'; +import { createRegistry } from '../../../../create_registry'; +import { EmbeddableFactory } from '../../../../embeddables'; +import { Filter, FilterStateStore } from '@kbn/es-query'; + +const embeddableFactories = createRegistry(); +embeddableFactories.set(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory()); + +let container: FilterableContainer; +let embeddable: FilterableEmbeddable; + +beforeEach(async () => { + const derivedFilter: Filter = { + $state: { store: FilterStateStore.APP_STATE }, + meta: { disabled: false, alias: 'name', negate: false }, + query: { match: {} }, + }; + container = new FilterableContainer( + { id: 'hello', panels: {}, filters: [derivedFilter] }, + embeddableFactories + ); + + const filterableEmbeddable = await container.addNewEmbeddable< + FilterableEmbeddableInput, + EmbeddableOutput, + FilterableEmbeddable + >(FILTERABLE_EMBEDDABLE, { + id: '123', + }); + + if (isErrorEmbeddable(filterableEmbeddable)) { + throw new Error('Error creating new filterable embeddable'); + } else { + embeddable = filterableEmbeddable; + } +}); + +test('Is not compatible when container is in view mode', async () => { + const action = new AddPanelAction(); + container.updateInput({ viewMode: ViewMode.VIEW }); + expect(await action.isCompatible({ embeddable: container })).toBe(false); +}); + +test('Is not compatible when embeddable is not a container', async () => { + const action = new AddPanelAction(); + expect( + await action.isCompatible({ + embeddable, + }) + ).toBe(false); +}); + +test('Is compatible when embeddable is a parent and in edit mode', async () => { + const action = new AddPanelAction(); + container.updateInput({ viewMode: ViewMode.EDIT }); + expect(await action.isCompatible({ embeddable: container })).toBe(true); +}); + +test('Execute throws an error when called with an embeddable that is not a container', async () => { + const action = new AddPanelAction(); + async function check() { + await action.execute({ + // @ts-ignore + embeddable: new ContactCardEmbeddable({ + firstName: 'sue', + id: '123', + viewMode: ViewMode.EDIT, + }), + }); + } + await expect(check()).rejects.toThrow(Error); +}); +test('Execute does not throw an error when called with a compatible container', async () => { + const action = new AddPanelAction(); + container.updateInput({ viewMode: ViewMode.EDIT }); + await action.execute({ + embeddable: container, + }); +}); + +test('Returns title', async () => { + const action = new AddPanelAction(); + expect(action.getDisplayName()).toBeDefined(); +}); + +test('Returns an icon', async () => { + const action = new AddPanelAction(); + expect(action.getIcon()).toBeDefined(); +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_action.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_action.tsx new file mode 100644 index 0000000000000..859194cc6f673 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_action.tsx @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { EuiIcon } from '@elastic/eui'; +import React from 'react'; + +import { i18n } from '@kbn/i18n'; +import { ViewMode } from '../../../../types'; +import { Action, ActionContext } from '../../../../actions'; +import { openAddPanelFlyout } from './open_add_panel_flyout'; + +export const ADD_PANEL_ACTION_ID = 'ADD_PANEL_ACTION_ID'; + +export class AddPanelAction extends Action { + public readonly type = ADD_PANEL_ACTION_ID; + + constructor() { + super(ADD_PANEL_ACTION_ID); + } + + public getDisplayName() { + return i18n.translate('kbn.embeddable.panel.addPanel.displayName', { + defaultMessage: 'Add panel', + }); + } + + public getIcon() { + return ; + } + + public async isCompatible({ embeddable }: ActionContext) { + return embeddable.getIsContainer() && embeddable.getInput().viewMode === ViewMode.EDIT; + } + + public async execute({ embeddable }: ActionContext) { + if (!embeddable.getIsContainer() || !(await this.isCompatible({ embeddable }))) { + throw new Error('Context is incompatible'); + } + + openAddPanelFlyout(embeddable); + } +} diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx new file mode 100644 index 0000000000000..42289ab65df27 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx @@ -0,0 +1,87 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getModalContents } from '../../../../np_core.test.mocks'; + +import React from 'react'; +import { + CONTACT_CARD_EMBEDDABLE, + ContactCardEmbeddableFactory, + HelloWorldContainer, + ContactCardEmbeddable, + ContactCardInitializerProps, +} from '../../../../test_samples/index'; + +import { AddPanelFlyout } from './add_panel_flyout'; +import { Container } from '../../../..'; +// @ts-ignore +import { findTestSubject } from '@elastic/eui/lib/test'; +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { skip } from 'rxjs/operators'; +import * as Rx from 'rxjs'; +import { createRegistry } from '../../../../create_registry'; +import { EmbeddableFactory } from '../../../../embeddables'; + +const onClose = jest.fn(); +let container: Container; + +function createHelloWorldContainer(input = { id: '123', panels: {} }) { + const embeddableFactories = createRegistry(); + embeddableFactories.set(CONTACT_CARD_EMBEDDABLE, new ContactCardEmbeddableFactory()); + return new HelloWorldContainer(input, embeddableFactories); +} + +beforeEach(() => { + container = createHelloWorldContainer(); +}); + +test('create new calls factory.adds a panel to the container', async done => { + const component = mountWithIntl(); + + expect(Object.values(container.getInput().panels).length).toBe(0); + + const subscription = Rx.merge(container.getOutput$(), container.getInput$()) + .pipe(skip(2)) + .subscribe(() => { + if (container.getInput().panels) { + const ids = Object.keys(container.getInput().panels); + expect(ids.length).toBe(1); + const embeddableId = ids[0]; + + if (container.getOutput().embeddableLoaded[embeddableId]) { + const child = container.getChild(embeddableId); + expect(child).toBeDefined(); + expect(child.getInput().firstName).toBe('Dany'); + expect(child.getInput().lastName).toBe('Targaryan'); + subscription.unsubscribe(); + done(); + } + } + }); + + findTestSubject(component, 'createNew').simulate('click'); + findTestSubject(component, `createNew-${CONTACT_CARD_EMBEDDABLE}`).simulate('click'); + + await nextTick(); + + (getModalContents().props as ContactCardInitializerProps).onCreate({ + firstName: 'Dany', + lastName: 'Targaryan', + }); +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx new file mode 100644 index 0000000000000..6477378aa2bc5 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx @@ -0,0 +1,182 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; +import { toastNotifications } from 'ui/notify'; +import { + SavedObjectFinder, + SavedObjectMetaData, +} from 'ui/saved_objects/components/saved_object_finder'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + // @ts-ignore + EuiSuperSelect, + EuiTitle, + EuiText, +} from '@elastic/eui'; + +import { SavedObjectAttributes } from '../../../../../../../server/saved_objects'; +import { EmbeddableFactoryNotFoundError } from '../../../../embeddables/embeddable_factory_not_found_error'; +import { IContainer } from '../../../../containers'; + +interface Props { + onClose: () => void; + container: IContainer; +} + +export class AddPanelFlyout extends React.Component { + private lastToast: any; + + constructor(props: Props) { + super(props); + } + + public showToast = (name: string) => { + // To avoid the clutter of having toast messages cover flyout + // close previous toast message before creating a new one + if (this.lastToast) { + toastNotifications.remove(this.lastToast); + } + + this.lastToast = toastNotifications.addSuccess({ + title: i18n.translate( + 'kbn.embeddables.addPanel.savedObjectAddedToContainerSuccessMessageTitle', + { + defaultMessage: '{savedObjectName} was added', + values: { + savedObjectName: name, + }, + } + ), + 'data-test-subj': 'addObjectToContainerSuccess', + }); + }; + + public createNewEmbeddable = async (type: string) => { + this.props.onClose(); + const factory = this.props.container.embeddableFactories.get(type); + + if (!factory) { + throw new EmbeddableFactoryNotFoundError(type); + } + + const explicitInput = await factory.getExplicitInput(); + const embeddable = await this.props.container.addNewEmbeddable(type, explicitInput); + if (embeddable) { + this.showToast(embeddable.getInput().title || ''); + } + }; + + public onAddPanel = async (id: string, type: string, name: string) => { + this.props.container.addSavedObjectEmbeddable(type, id); + + this.showToast(name); + }; + + private getSelectCreateNewOptions() { + return [ + { + value: 'createNew', + inputDisplay: ( + + + + ), + }, + + ...this.props.container.embeddableFactories + .getAll() + .filter( + factory => factory.isEditable() && !factory.isContainerType && factory.canCreateNew() + ) + .map(factory => ({ + inputDisplay: ( + + + + ), + value: factory.type, + 'data-test-subj': `createNew-${factory.type}`, + })), + ]; + } + + public render() { + return ( + + + +

+ +

+
+
+ + + Boolean(embeddableFactory.savedObjectMetaData) && + !embeddableFactory.isContainerType + ) + .map(({ savedObjectMetaData }) => savedObjectMetaData) as Array< + SavedObjectMetaData + > + } + showFilter={true} + noItemsMessage={i18n.translate('kbn.embeddables.addPanel.noMatchingObjectsMessage', { + defaultMessage: 'No matching objects found.', + })} + /> + + + + + this.createNewEmbeddable(value)} + /> + + + +
+ ); + } +} diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/index.ts b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/index.ts new file mode 100644 index 0000000000000..78f04ee94abed --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './add_panel_action'; +export * from './open_add_panel_flyout'; diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx new file mode 100644 index 0000000000000..ae96e75468c52 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { npStart } from 'ui/new_platform'; +import { IContainer } from '../../../../containers'; +import { AddPanelFlyout } from './add_panel_flyout'; + +export async function openAddPanelFlyout(embeddable: IContainer) { + const flyoutSession = npStart.core.overlays.openFlyout( + { + if (flyoutSession) { + flyoutSession.close(); + } + }} + />, + { + 'data-test-subj': 'addPanelFlyout', + } + ); +} diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts new file mode 100644 index 0000000000000..eee46df447194 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts @@ -0,0 +1,113 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import '../../../../np_core.test.mocks'; + +import { + CONTACT_CARD_EMBEDDABLE, + ContactCardEmbeddableFactory, + HelloWorldContainer, + ContactCardEmbeddable, + ContactCardEmbeddableOutput, + ContactCardEmbeddableInput, +} from '../../../../test_samples/index'; + +import { Container, isErrorEmbeddable } from '../../../..'; +// @ts-ignore +import { findTestSubject } from '@elastic/eui/lib/test'; +import { nextTick } from 'test_utils/enzyme_helpers'; +import { CustomizePanelTitleAction } from './customize_panel_action'; +import { createRegistry } from '../../../../create_registry'; +import { EmbeddableFactory } from '../../../../embeddables'; + +let container: Container; +let embeddable: ContactCardEmbeddable; + +function createHelloWorldContainer(input = { id: '123', panels: {} }) { + const embeddableFactories = createRegistry(); + embeddableFactories.set(CONTACT_CARD_EMBEDDABLE, new ContactCardEmbeddableFactory()); + return new HelloWorldContainer(input, embeddableFactories); +} + +beforeEach(async () => { + container = createHelloWorldContainer(); + const contactCardEmbeddable = await container.addNewEmbeddable< + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardEmbeddable + >(CONTACT_CARD_EMBEDDABLE, { + id: 'robert', + firstName: 'Robert', + lastName: 'Baratheon', + }); + if (isErrorEmbeddable(contactCardEmbeddable)) { + throw new Error('Error creating new hello world embeddable'); + } else { + embeddable = contactCardEmbeddable; + } +}); + +test('Updates the embeddable title when given', async done => { + const getUserData = () => Promise.resolve({ title: 'What is up?' }); + const customizePanelAction = new CustomizePanelTitleAction(getUserData); + expect(embeddable.getInput().title).toBeUndefined(); + expect(embeddable.getTitle()).toBe('Hello Robert Baratheon'); + await customizePanelAction.execute({ embeddable }); + await nextTick(); + expect(embeddable.getTitle()).toBe('What is up?'); + expect(embeddable.getInput().title).toBe('What is up?'); + + // Recreating the container should preserve the custom title. + const containerClone = createHelloWorldContainer(container.getInput()); + // Need to wait for the container to tell us the embeddable has been loaded. + const subscription = containerClone.getOutput$().subscribe(() => { + if (containerClone.getOutput().embeddableLoaded[embeddable.id]) { + expect(embeddable.getInput().title).toBe('What is up?'); + subscription.unsubscribe(); + done(); + } + }); +}); + +test('Empty string results in an empty title', async () => { + const getUserData = () => Promise.resolve({ title: '' }); + const customizePanelAction = new CustomizePanelTitleAction(getUserData); + expect(embeddable.getInput().title).toBeUndefined(); + expect(embeddable.getTitle()).toBe('Hello Robert Baratheon'); + + await customizePanelAction.execute({ embeddable }); + await nextTick(); + expect(embeddable.getTitle()).toBe(''); +}); + +test('Undefined title results in the original title', async () => { + const getUserData = () => Promise.resolve({ title: 'hi' }); + const customizePanelAction = new CustomizePanelTitleAction(getUserData); + expect(embeddable.getInput().title).toBeUndefined(); + expect(embeddable.getTitle()).toBe('Hello Robert Baratheon'); + await customizePanelAction.execute({ embeddable }); + await nextTick(); + expect(embeddable.getTitle()).toBe('hi'); + + await new CustomizePanelTitleAction(() => Promise.resolve({ title: undefined })).execute({ + embeddable, + }); + await nextTick(); + expect(embeddable.getTitle()).toBe('Hello Robert Baratheon'); +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_panel_action.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_panel_action.tsx new file mode 100644 index 0000000000000..8673ab8b6f14b --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_panel_action.tsx @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { EuiIcon } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { Action, ActionContext } from '../../../../actions'; +import { ViewMode } from '../../../../types'; +import { getUserData } from './get_user_data'; + +const CUSTOMIZE_PANEL_ACTION_ID = 'CUSTOMIZE_PANEL_ACTION_ID'; + +export class CustomizePanelTitleAction extends Action { + public readonly type = CUSTOMIZE_PANEL_ACTION_ID; + private getDataFromUser: (context: ActionContext) => Promise<{ title: string | undefined }>; + constructor( + getDataFromUser: ( + context: ActionContext + ) => Promise<{ title: string | undefined }> = getUserData + ) { + super(CUSTOMIZE_PANEL_ACTION_ID); + this.order = 10; + this.getDataFromUser = getDataFromUser; + } + + public getDisplayName() { + return i18n.translate('kbn.embeddables.panel.customizePanel.displayName', { + defaultMessage: 'Customize panel', + }); + } + + public getIcon() { + return ; + } + + public async isCompatible({ embeddable }: ActionContext) { + return embeddable.getInput().viewMode === ViewMode.EDIT ? true : false; + } + + public async execute({ embeddable }: ActionContext) { + const customTitle = await this.getDataFromUser({ embeddable }); + embeddable.updateInput(customTitle); + } +} diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_panel_modal.test.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_panel_modal.test.tsx new file mode 100644 index 0000000000000..9dbb481c429bd --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_panel_modal.test.tsx @@ -0,0 +1,187 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import '../../../../np_core.test.mocks'; + +import React from 'react'; +import { + CONTACT_CARD_EMBEDDABLE, + ContactCardEmbeddableFactory, + HelloWorldContainer, + ContactCardEmbeddableOutput, + ContactCardEmbeddable, + ContactCardEmbeddableInput, +} from '../../../../test_samples/index'; + +// @ts-ignore +import { findTestSubject } from '@elastic/eui/lib/test'; +import { CustomizePanelModal } from './customize_panel_modal'; +import { Container, isErrorEmbeddable } from '../../../..'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { createRegistry } from '../../../../create_registry'; +import { EmbeddableFactory } from '../../../../embeddables'; + +let container: Container; +let embeddable: ContactCardEmbeddable; + +beforeEach(async () => { + const embeddableFactories = createRegistry(); + embeddableFactories.set(CONTACT_CARD_EMBEDDABLE, new ContactCardEmbeddableFactory()); + container = new HelloWorldContainer({ id: '123', panels: {} }, embeddableFactories); + const contactCardEmbeddable = await container.addNewEmbeddable< + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardEmbeddable + >(CONTACT_CARD_EMBEDDABLE, { + firstName: 'Joe', + }); + if (isErrorEmbeddable(contactCardEmbeddable)) { + throw new Error('Error creating new hello world embeddable'); + } else { + embeddable = contactCardEmbeddable; + } +}); + +test('Is initialized with the embeddables title', async () => { + const component = mountWithIntl( + {}} + /> + ); + + const inputField = findTestSubject(component, 'customEmbeddablePanelTitleInput').find('input'); + expect(inputField.props().placeholder).toBe(embeddable.getOutput().title); + expect(inputField.props().placeholder).toBe(embeddable.getOutput().defaultTitle); + expect(inputField.props().value).toBe(''); +}); + +test('Calls updateTitle with a new title', async () => { + const updateTitle = jest.fn(); + const component = mountWithIntl( + + ); + + const inputField = findTestSubject(component, 'customEmbeddablePanelTitleInput').find('input'); + const event = { target: { value: 'new title' } }; + inputField.simulate('change', event); + + findTestSubject(component, 'saveNewTitleButton').simulate('click'); + + expect(updateTitle).toBeCalledWith('new title'); +}); + +test('Input value shows custom title if one given', async () => { + embeddable.updateInput({ title: 'new title' }); + + const updateTitle = jest.fn(); + const component = mountWithIntl( + + ); + + const inputField = findTestSubject(component, 'customEmbeddablePanelTitleInput').find('input'); + expect(inputField.props().value).toBe('new title'); + findTestSubject(component, 'saveNewTitleButton').simulate('click'); + expect(inputField.props().value).toBe('new title'); +}); + +test('Reset updates the input with the default title when the embeddable has no title override', async () => { + const updateTitle = jest.fn(); + + embeddable.updateInput({ title: 'my custom title' }); + const component = mountWithIntl( + + ); + + const inputField = findTestSubject(component, 'customEmbeddablePanelTitleInput').find('input'); + const event = { target: { value: 'another custom title' } }; + inputField.simulate('change', event); + + findTestSubject(component, 'resetCustomEmbeddablePanelTitle').simulate('click'); + expect(inputField.props().placeholder).toBe(embeddable.getOutput().defaultTitle); +}); + +test('Reset updates the input with the default title when the embeddable has a title override', async () => { + const updateTitle = jest.fn(); + const component = mountWithIntl( + + ); + + const inputField = findTestSubject(component, 'customEmbeddablePanelTitleInput').find('input'); + const event = { target: { value: 'new title' } }; + inputField.simulate('change', event); + + findTestSubject(component, 'resetCustomEmbeddablePanelTitle').simulate('click'); + expect(inputField.props().placeholder).toBe(embeddable.getOutput().defaultTitle); +}); + +test('Reset calls updateTitle with undefined', async () => { + const updateTitle = jest.fn(); + const component = mountWithIntl( + + ); + + const inputField = findTestSubject(component, 'customEmbeddablePanelTitleInput').find('input'); + const event = { target: { value: 'new title' } }; + inputField.simulate('change', event); + + findTestSubject(component, 'resetCustomEmbeddablePanelTitle').simulate('click'); + findTestSubject(component, 'saveNewTitleButton').simulate('click'); + + expect(updateTitle).toBeCalledWith(undefined); +}); + +test('Can set title to an empty string', async () => { + const updateTitle = jest.fn(); + const component = mountWithIntl( + + ); + + const inputField = findTestSubject(component, 'customizePanelHideTitle'); + inputField.simulate('change'); + + findTestSubject(component, 'saveNewTitleButton').simulate('click'); + expect(inputField.props().value).toBeUndefined(); + expect(updateTitle).toBeCalledWith(''); +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_panel_modal.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_panel_modal.tsx new file mode 100644 index 0000000000000..91de837fabc18 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_panel_modal.tsx @@ -0,0 +1,157 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { Component } from 'react'; + +import { + EuiFormRow, + EuiFieldText, + EuiButton, + EuiSwitch, + EuiButtonEmpty, + EuiModalHeader, + EuiModalFooter, + EuiModalBody, + EuiModalHeaderTitle, +} from '@elastic/eui'; +import { injectI18n, FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; +import { IEmbeddable } from '../../../../'; + +interface CustomizePanelProps { + embeddable: IEmbeddable; + updateTitle: (newTitle: string | undefined) => void; + intl: InjectedIntl; +} + +interface State { + title: string | undefined; + hideTitle: boolean; +} + +export class CustomizePanelModalUi extends Component { + constructor(props: CustomizePanelProps) { + super(props); + this.state = { + hideTitle: props.embeddable.getOutput().title === '', + title: props.embeddable.getInput().title, + }; + } + + updateTitle = (title: string | undefined) => { + // An empty string will mean "use the default value", which is represented by setting + // title to undefined (where as an empty string is actually used to indicate "hide title"). + this.setState({ title: title === '' ? undefined : title }); + }; + + reset = () => { + this.setState({ title: undefined }); + }; + + onHideTitleToggle = () => { + this.setState(prevState => ({ + hideTitle: !prevState.hideTitle, + })); + }; + + save = () => { + if (this.state.hideTitle) { + this.props.updateTitle(''); + } else { + const newTitle = this.state.title === '' ? undefined : this.state.title; + this.props.updateTitle(newTitle); + } + }; + + public render() { + return ( + + + + Customize panel + + + + + {' '} + + + } + onChange={this.onHideTitleToggle} + /> + + + this.updateTitle(e.target.value)} + aria-label={this.props.intl.formatMessage({ + id: 'kbn.embeddable.panel.optionsMenuForm.panelTitleInputAriaLabel', + defaultMessage: 'Enter a custom title for your panel', + })} + append={ + + + + } + /> + + + + this.props.updateTitle(this.props.embeddable.getOutput().title)} + > + {' '} + + + + + + + + + ); + } +} + +export const CustomizePanelModal = injectI18n(CustomizePanelModalUi); diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_title_form.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_title_form.tsx new file mode 100644 index 0000000000000..c4b9f9ff9a40a --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_title_form.tsx @@ -0,0 +1,77 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { ChangeEvent } from 'react'; + +import { EuiButtonEmpty, EuiFieldText, EuiFormRow } from '@elastic/eui'; +import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; + +export interface Props { + title?: string; + onReset: () => void; + onUpdatePanelTitle: (newPanelTitle: string) => void; +} + +interface PanelOptionsMenuFormUiProps extends Props { + intl: InjectedIntl; +} + +function CustomizeTitleFormUi({ + title, + onReset, + onUpdatePanelTitle, + intl, +}: PanelOptionsMenuFormUiProps) { + function onInputChange(event: ChangeEvent) { + onUpdatePanelTitle(event.target.value); + } + + return ( +
+ + + + + + + +
+ ); +} + +export const CustomizeTitleForm = injectI18n(CustomizeTitleFormUi); diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/get_user_data.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/get_user_data.tsx new file mode 100644 index 0000000000000..eeb5a4dfb2318 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/get_user_data.tsx @@ -0,0 +1,39 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { npStart } from 'ui/new_platform'; +import { ActionContext } from '../../../../actions'; +import { CustomizePanelModal } from './customize_panel_modal'; + +export async function getUserData(context: ActionContext) { + return new Promise<{ title: string | undefined }>(resolve => { + const session = npStart.core.overlays.openModal( + { + session.close(); + resolve({ title }); + }} + />, + { + 'data-test-subj': 'customizePanel', + } + ); + }); +} diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/edit_panel_action.test.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/edit_panel_action.test.tsx new file mode 100644 index 0000000000000..edc3f2190aa68 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/edit_panel_action.test.tsx @@ -0,0 +1,105 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import '../../../np_core.test.mocks'; + +import { EmbeddableInput } from '../../../embeddables/i_embeddable'; +import { Embeddable } from '../../../embeddables/embeddable'; +import { ContactCardEmbeddable } from '../../../test_samples'; +import { ViewMode } from '../../../types'; +import { EditPanelAction } from './edit_panel_action'; + +class EditableEmbeddable extends Embeddable { + public readonly type = 'EDITABLE_EMBEDDABLE'; + + constructor(input: EmbeddableInput, editable: boolean) { + super(input, { + editUrl: 'www.google.com', + editable, + }); + } + + public reload() {} +} + +test('is compatible when edit url is available, in edit mode and editable', async () => { + const action = new EditPanelAction(); + expect( + await action.isCompatible({ + embeddable: new EditableEmbeddable({ id: '123', viewMode: ViewMode.EDIT }, true), + }) + ).toBe(true); +}); + +test('getHref returns the edit urls', async () => { + const action = new EditPanelAction(); + expect(action.getHref).toBeDefined(); + + if (action.getHref) { + const embeddable = new EditableEmbeddable({ id: '123', viewMode: ViewMode.EDIT }, true); + expect( + action.getHref({ + embeddable, + }) + ).toBe(embeddable.getOutput().editUrl); + } +}); + +test('is not compatible when edit url is not available', async () => { + const action = new EditPanelAction(); + expect( + await action.isCompatible({ + embeddable: new ContactCardEmbeddable({ + id: '123', + firstName: 'sue', + viewMode: ViewMode.EDIT, + }), + }) + ).toBe(false); +}); + +test('is not visible when edit url is available but in view mode', async () => { + const action = new EditPanelAction(); + expect( + await action.isCompatible({ + embeddable: new EditableEmbeddable( + { + id: '123', + viewMode: ViewMode.VIEW, + }, + true + ), + }) + ).toBe(false); +}); + +test('is not compatible when edit url is available, in edit mode, but not editable', async () => { + const action = new EditPanelAction(); + expect( + await action.isCompatible({ + embeddable: new EditableEmbeddable( + { + id: '123', + viewMode: ViewMode.EDIT, + }, + false + ), + }) + ).toBe(false); +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/edit_panel_action.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/edit_panel_action.tsx new file mode 100644 index 0000000000000..4e7a0a63edf33 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/edit_panel_action.tsx @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiIcon } from '@elastic/eui'; + +import { embeddableFactories } from '../../../embeddables/embeddable_factories_registry'; +import { Action, ActionContext } from '../../../actions'; +import { ViewMode } from '../../../types'; +import { EmbeddableFactoryNotFoundError } from '../../../embeddables'; + +export const EDIT_PANEL_ACTION_ID = 'editPanel'; + +export class EditPanelAction extends Action { + public readonly type = EDIT_PANEL_ACTION_ID; + constructor() { + super(EDIT_PANEL_ACTION_ID); + this.order = 15; + } + + public getDisplayName({ embeddable }: ActionContext) { + const factory = embeddableFactories.get(embeddable.type); + if (!factory) { + throw new EmbeddableFactoryNotFoundError(embeddable.type); + } + return i18n.translate('kbn.dashboard.panel.editPanel.displayName', { + defaultMessage: 'Edit {value}', + values: { + value: factory.getDisplayName(), + }, + }); + } + + getIcon() { + return ; + } + + public async isCompatible({ embeddable }: ActionContext) { + const canEditEmbeddable = Boolean( + embeddable && embeddable.getOutput().editable && embeddable.getOutput().editUrl + ); + const inDashboardEditMode = embeddable.getInput().viewMode === ViewMode.EDIT; + return Boolean(canEditEmbeddable && inDashboardEditMode); + } + + public execute() { + return undefined; + } + + public getHref({ embeddable }: ActionContext): string { + const editUrl = embeddable ? embeddable.getOutput().editUrl : undefined; + return editUrl ? editUrl : ''; + } +} diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/index.ts b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/index.ts new file mode 100644 index 0000000000000..7810e0095b632 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { InspectPanelAction } from './inspect_panel_action'; +export { ADD_PANEL_ACTION_ID, AddPanelAction, openAddPanelFlyout } from './add_panel'; +export { RemovePanelAction } from './remove_panel_action'; diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/inspect_panel_action.test.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/inspect_panel_action.test.tsx new file mode 100644 index 0000000000000..d5e9a2e222424 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/inspect_panel_action.test.tsx @@ -0,0 +1,128 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import '../../../np_core.test.mocks'; + +jest.mock('ui/inspector', () => ({ + Inspector: { + open: jest.fn(() => ({ + onClose: Promise.resolve(), + })), + isAvailable: (adapters: Adapters) => { + return Boolean(adapters); + }, + }, +})); + +import { + FilterableContainer, + FilterableEmbeddable, + FilterableEmbeddableFactory, + ContactCardEmbeddable, + FilterableEmbeddableInput, + FILTERABLE_EMBEDDABLE, +} from '../../../test_samples'; + +import { EmbeddableOutput, isErrorEmbeddable } from '../../..'; +import { InspectPanelAction } from './inspect_panel_action'; +import { Inspector, Adapters } from 'ui/inspector'; +import { createRegistry } from '../../../create_registry'; +import { EmbeddableFactory } from '../../../embeddables'; +import { Filter, FilterStateStore } from '@kbn/es-query'; + +const embeddableFactories = createRegistry(); +embeddableFactories.set(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory()); + +let container: FilterableContainer; +let embeddable: FilterableEmbeddable; + +beforeEach(async () => { + const derivedFilter: Filter = { + $state: { store: FilterStateStore.APP_STATE }, + meta: { disabled: false, alias: 'name', negate: false }, + query: { match: {} }, + }; + container = new FilterableContainer( + { id: 'hello', panels: {}, filters: [derivedFilter] }, + embeddableFactories + ); + + const filterableEmbeddable = await container.addNewEmbeddable< + FilterableEmbeddableInput, + EmbeddableOutput, + FilterableEmbeddable + >(FILTERABLE_EMBEDDABLE, { + id: '123', + }); + + if (isErrorEmbeddable(filterableEmbeddable)) { + throw new Error('Error creating new filterable embeddable'); + } else { + embeddable = filterableEmbeddable; + } +}); + +test('Is compatible when inspector adapters are available', async () => { + const inspectAction = new InspectPanelAction(); + expect(await inspectAction.isCompatible({ embeddable })).toBe(true); +}); + +test('Is not compatible when inspector adapters are not available', async () => { + const inspectAction = new InspectPanelAction(); + expect( + await inspectAction.isCompatible({ + embeddable: new ContactCardEmbeddable({ + firstName: 'Davos', + lastName: 'Seaworth', + id: '123', + }), + }) + ).toBe(false); +}); + +test('Executes when inspector adapters are available', async () => { + const inspectAction = new InspectPanelAction(); + await inspectAction.execute({ embeddable }); + expect(Inspector.open).toBeCalled(); +}); + +test('Execute throws an error when inspector adapters are not available', async () => { + const inspectAction = new InspectPanelAction(); + await inspectAction.execute({ embeddable }); + + await expect( + inspectAction.execute({ + embeddable: new ContactCardEmbeddable({ + firstName: 'John', + lastName: 'Snow', + id: '123', + }), + }) + ).rejects.toThrow(Error); +}); + +test('Returns title', async () => { + const inspectAction = new InspectPanelAction(); + expect(inspectAction.getDisplayName()).toBeDefined(); +}); + +test('Returns an icon', async () => { + const inspectAction = new InspectPanelAction(); + expect(inspectAction.getIcon()).toBeDefined(); +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/inspect_panel_action.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/inspect_panel_action.tsx new file mode 100644 index 0000000000000..3656bdc3f83a6 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/inspect_panel_action.tsx @@ -0,0 +1,74 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { EuiIcon } from '@elastic/eui'; +import React from 'react'; + +import { i18n } from '@kbn/i18n'; +import { Inspector } from 'ui/inspector'; +import { Action, ActionContext } from '../../../actions'; + +export const INSPECT_PANEL_ACTION_ID = 'openInspector'; + +export class InspectPanelAction extends Action { + public readonly type = INSPECT_PANEL_ACTION_ID; + constructor() { + super(INSPECT_PANEL_ACTION_ID); + this.order = 20; + } + + public getDisplayName() { + return i18n.translate('kbn.embeddable.panel.inspectPanel.displayName', { + defaultMessage: 'Inspect', + }); + } + + public getIcon() { + return ; + } + + public async isCompatible({ embeddable }: ActionContext) { + return Inspector.isAvailable(embeddable.getInspectorAdapters()); + } + + public async execute({ embeddable }: ActionContext) { + const adapters = embeddable.getInspectorAdapters(); + + if (!(await this.isCompatible({ embeddable })) || adapters === undefined) { + throw new Error('Action not compatible with context'); + } + + const session = Inspector.open(adapters, { + title: embeddable.getTitle(), + }); + // Overwrite the embeddables.destroy() function to close the inspector + // before calling the original destroy method + const originalDestroy = embeddable.destroy; + embeddable.destroy = () => { + session.close(); + if (originalDestroy) { + originalDestroy.call(embeddable); + } + }; + // In case the inspector gets closed (otherwise), restore the original destroy function + session.onClose.finally(() => { + embeddable.destroy = originalDestroy; + }); + } +} diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/remove_panel_action.test.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/remove_panel_action.test.tsx new file mode 100644 index 0000000000000..cadcb315439b9 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/remove_panel_action.test.tsx @@ -0,0 +1,107 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import '../../../np_core.test.mocks'; + +import { + FilterableContainer, + FilterableEmbeddable, + FilterableEmbeddableFactory, + ContactCardEmbeddable, + FilterableEmbeddableInput, + FILTERABLE_EMBEDDABLE, +} from '../../../test_samples'; + +import { EmbeddableOutput, isErrorEmbeddable } from '../../../'; +import { RemovePanelAction } from './remove_panel_action'; +import { createRegistry } from '../../../create_registry'; +import { EmbeddableFactory } from '../../../embeddables'; +import { Filter, FilterStateStore } from '@kbn/es-query'; + +const embeddableFactories = createRegistry(); +embeddableFactories.set(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory()); + +let container: FilterableContainer; +let embeddable: FilterableEmbeddable; + +beforeEach(async () => { + const derivedFilter: Filter = { + $state: { store: FilterStateStore.APP_STATE }, + meta: { disabled: false, alias: 'name', negate: false }, + query: { match: {} }, + }; + container = new FilterableContainer( + { id: 'hello', panels: {}, filters: [derivedFilter] }, + embeddableFactories + ); + + const filterableEmbeddable = await container.addNewEmbeddable< + FilterableEmbeddableInput, + EmbeddableOutput, + FilterableEmbeddable + >(FILTERABLE_EMBEDDABLE, { + id: '123', + }); + + if (isErrorEmbeddable(filterableEmbeddable)) { + throw new Error('Error creating new filterable embeddable'); + } else { + embeddable = filterableEmbeddable; + } +}); + +test('Removes the embeddable', async () => { + const removePanelAction = new RemovePanelAction(); + expect(container.getChild(embeddable.id)).toBeDefined(); + + removePanelAction.execute({ embeddable }); + + expect(container.getChild(embeddable.id)).toBeUndefined(); +}); + +test('Is not compatible when embeddable is not in a parent', async () => { + const action = new RemovePanelAction(); + expect( + await action.isCompatible({ + embeddable: new ContactCardEmbeddable({ + firstName: 'Sandor', + lastName: 'Clegane', + id: '123', + }), + }) + ).toBe(false); +}); + +test('Execute throws an error when called with an embeddable not in a parent', async () => { + const action = new RemovePanelAction(); + async function check() { + await action.execute({ embeddable: container }); + } + await expect(check()).rejects.toThrow(Error); +}); + +test('Returns title', async () => { + const action = new RemovePanelAction(); + expect(action.getDisplayName()).toBeDefined(); +}); + +test('Returns an icon', async () => { + const action = new RemovePanelAction(); + expect(action.getIcon()).toBeDefined(); +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/remove_panel_action.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/remove_panel_action.tsx new file mode 100644 index 0000000000000..62fc04436abda --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/remove_panel_action.tsx @@ -0,0 +1,73 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiIcon } from '@elastic/eui'; +import { ContainerInput, IContainer } from '../../../containers'; +import { ViewMode } from '../../../types'; +import { Action, ActionContext, IncompatibleActionError } from '../../../actions'; + +export const REMOVE_PANEL_ACTION = 'deletePanel'; + +interface ExpandedPanelInput extends ContainerInput { + expandedPanelId: string; +} + +function hasExpandedPanelInput( + container: IContainer | IContainer +): container is IContainer { + return (container as IContainer).getInput().expandedPanelId !== undefined; +} + +export class RemovePanelAction extends Action { + public readonly type = REMOVE_PANEL_ACTION; + constructor() { + super(REMOVE_PANEL_ACTION); + + this.order = 5; + } + + public getDisplayName() { + return i18n.translate('kbn.embeddable.panel.removePanel.displayName', { + defaultMessage: 'Delete from dashboard', + }); + } + + public getIcon() { + return ; + } + + public async isCompatible({ embeddable }: ActionContext) { + const isPanelExpanded = + embeddable.parent && + hasExpandedPanelInput(embeddable.parent) && + embeddable.parent.getInput().expandedPanelId === embeddable.id; + + return Boolean( + embeddable.parent && embeddable.getInput().viewMode === ViewMode.EDIT && !isPanelExpanded + ); + } + + public execute({ embeddable }: ActionContext) { + if (!embeddable.parent || !this.isCompatible({ embeddable })) { + throw new IncompatibleActionError(); + } + embeddable.parent.removeEmbeddable(embeddable.id); + } +} diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_header.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_header.tsx new file mode 100644 index 0000000000000..bea899fd63335 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_header.tsx @@ -0,0 +1,93 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { EuiContextMenuPanelDescriptor } from '@elastic/eui'; +import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; +import classNames from 'classnames'; +import React from 'react'; +import { PanelOptionsMenu } from './panel_options_menu'; + +export interface PanelHeaderProps { + title?: string; + isViewMode: boolean; + hidePanelTitles: boolean; + getActionContextMenuPanel: () => Promise; + closeContextMenu: boolean; +} + +interface PanelHeaderUiProps extends PanelHeaderProps { + intl: InjectedIntl; +} + +function PanelHeaderUi({ + title, + isViewMode, + hidePanelTitles, + getActionContextMenuPanel, + intl, + closeContextMenu, +}: PanelHeaderUiProps) { + const classes = classNames('embPanel__header', { + 'embPanel__header--floater': !title || hidePanelTitles, + }); + + if (isViewMode && (!title || hidePanelTitles)) { + return ( +
+ +
+ ); + } + + return ( +
+
+ {hidePanelTitles ? '' : title} +
+ + +
+ ); +} + +export const PanelHeader = injectI18n(PanelHeaderUi); diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_options_menu.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_options_menu.tsx new file mode 100644 index 0000000000000..351e007357e5a --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_options_menu.tsx @@ -0,0 +1,144 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; +import React from 'react'; + +import { + EuiButtonIcon, + EuiContextMenu, + EuiContextMenuPanelDescriptor, + EuiPopover, +} from '@elastic/eui'; + +export interface PanelOptionsMenuProps { + getActionContextMenuPanel: () => Promise; + isViewMode: boolean; + closeContextMenu: boolean; +} + +interface PanelOptionsMenuUiProps extends PanelOptionsMenuProps { + intl: InjectedIntl; +} + +interface State { + actionContextMenuPanel?: EuiContextMenuPanelDescriptor; + isPopoverOpen: boolean; +} + +class PanelOptionsMenuUi extends React.Component { + private mounted = false; + public static getDerivedStateFromProps(props: PanelOptionsMenuUiProps, state: State) { + if (props.closeContextMenu) { + return { + ...state, + isPopoverOpen: false, + }; + } else { + return state; + } + } + + constructor(props: PanelOptionsMenuUiProps) { + super(props); + this.state = { + actionContextMenuPanel: undefined, + isPopoverOpen: false, + }; + } + + public async componentDidMount() { + this.mounted = true; + this.setState({ actionContextMenuPanel: undefined }); + const actionContextMenuPanel = await this.props.getActionContextMenuPanel(); + if (this.mounted) { + this.setState({ actionContextMenuPanel }); + } + } + + public componentWillUnmount() { + this.mounted = false; + } + + public render() { + const { isViewMode, intl } = this.props; + const button = ( + + ); + + return ( + + + + ); + } + private closePopover = () => { + if (this.mounted) { + this.setState({ + isPopoverOpen: false, + }); + } + }; + + private toggleContextMenu = () => { + if (this.mounted) { + this.setState( + { + isPopoverOpen: !this.state.isPopoverOpen, + }, + async () => { + if (this.mounted && this.state.isPopoverOpen) { + this.setState({ actionContextMenuPanel: undefined }); + const actionContextMenuPanel = await this.props.getActionContextMenuPanel(); + this.setState({ actionContextMenuPanel }); + } + } + ); + } + }; +} + +export const PanelOptionsMenu = injectI18n(PanelOptionsMenuUi); diff --git a/src/legacy/core_plugins/embeddable_api/public/test_samples/actions/edit_mode_action.ts b/src/legacy/core_plugins/embeddable_api/public/test_samples/actions/edit_mode_action.ts new file mode 100644 index 0000000000000..d2e893887808b --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/test_samples/actions/edit_mode_action.ts @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ViewMode } from '../../types'; +import { Action, ActionContext } from '../../actions'; + +export const EDIT_MODE_ACTION = 'EDIT_MODE_ACTION'; + +export class EditModeAction extends Action { + public readonly type = EDIT_MODE_ACTION; + + constructor() { + super(EDIT_MODE_ACTION); + } + + getDisplayName() { + return `I should only show up in edit mode`; + } + + async isCompatible(context: ActionContext) { + return context.embeddable.getInput().viewMode === ViewMode.EDIT; + } + + execute() { + return; + } +} diff --git a/src/legacy/core_plugins/embeddable_api/public/test_samples/actions/get_message_modal.tsx b/src/legacy/core_plugins/embeddable_api/public/test_samples/actions/get_message_modal.tsx new file mode 100644 index 0000000000000..7b396ce1bd10a --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/test_samples/actions/get_message_modal.tsx @@ -0,0 +1,85 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + EuiForm, + EuiFormRow, + EuiFieldText, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiButton, + EuiModalFooter, + EuiButtonEmpty, +} from '@elastic/eui'; +import React, { Component } from 'react'; + +interface Props { + onDone: (message: string) => void; + onCancel: () => void; +} + +interface State { + message?: string; +} + +export class GetMessageModal extends Component { + constructor(props: Props) { + super(props); + this.state = {}; + } + + render() { + return ( + + + Enter your message + + + + + + this.setState({ message: e.target.value })} + /> + + + + + + Cancel + + { + if (this.state.message) { + this.props.onDone(this.state.message); + } + }} + fill + > + Done + + + + ); + } +} diff --git a/src/legacy/core_plugins/embeddable_api/public/test_samples/actions/hello_world_action.tsx b/src/legacy/core_plugins/embeddable_api/public/test_samples/actions/hello_world_action.tsx new file mode 100644 index 0000000000000..379b84933a84d --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/test_samples/actions/hello_world_action.tsx @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { npStart } from 'ui/new_platform'; +import { EuiFlyout } from '@elastic/eui'; +import { Action, actionRegistry, triggerRegistry, CONTEXT_MENU_TRIGGER } from '../..'; + +import { attachAction } from '../../triggers/attach_action'; + +export const HELLO_WORLD_ACTION_ID = 'HELLO_WORLD_ACTION_ID'; + +export class HelloWorldAction extends Action { + public readonly type = HELLO_WORLD_ACTION_ID; + constructor() { + super(HELLO_WORLD_ACTION_ID); + } + + public getDisplayName() { + return 'Hello World Action!'; + } + + public execute() { + const flyoutSession = npStart.core.overlays.openFlyout( + flyoutSession && flyoutSession.close()}> + Hello World, I am a hello world action! + , + { + 'data-test-subj': 'helloWorldAction', + } + ); + } +} + +actionRegistry.set(HELLO_WORLD_ACTION_ID, new HelloWorldAction()); + +// Attaching to CONTEXT_MENU_TRIGGER makes this action appear in the context menu for +// all embeddables. +attachAction(triggerRegistry, { + triggerId: CONTEXT_MENU_TRIGGER, + actionId: HELLO_WORLD_ACTION_ID, +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/test_samples/actions/index.ts b/src/legacy/core_plugins/embeddable_api/public/test_samples/actions/index.ts new file mode 100644 index 0000000000000..1c097a5b3a3ec --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/test_samples/actions/index.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { HelloWorldAction, HELLO_WORLD_ACTION_ID } from './hello_world_action'; +export { SayHelloAction } from './say_hello_action'; +export { EditModeAction } from './edit_mode_action'; +export { RestrictedAction } from './restricted_action'; +export { SendMessageAction, SEND_MESSAGE_ACTION } from './send_message_action'; diff --git a/src/legacy/core_plugins/embeddable_api/public/test_samples/actions/restricted_action.ts b/src/legacy/core_plugins/embeddable_api/public/test_samples/actions/restricted_action.ts new file mode 100644 index 0000000000000..950b843816c7a --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/test_samples/actions/restricted_action.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Action, ActionContext } from '../../actions'; + +export const RESTRICTED_ACTION = 'RESTRICTED_ACTION'; + +export class RestrictedAction extends Action { + public readonly type = RESTRICTED_ACTION; + + private isCompatibleFn: (context: ActionContext) => boolean; + constructor(isCompatible: (context: ActionContext) => boolean) { + super(RESTRICTED_ACTION); + this.isCompatibleFn = isCompatible; + } + + getDisplayName() { + return `I am only sometimes compatible`; + } + + async isCompatible(context: ActionContext) { + return this.isCompatibleFn(context); + } + + execute() {} +} diff --git a/src/legacy/core_plugins/embeddable_api/public/test_samples/actions/say_hello_action.tsx b/src/legacy/core_plugins/embeddable_api/public/test_samples/actions/say_hello_action.tsx new file mode 100644 index 0000000000000..082e1e08a222e --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/test_samples/actions/say_hello_action.tsx @@ -0,0 +1,93 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { npStart } from 'ui/new_platform'; +import { EuiFlyoutBody } from '@elastic/eui'; +import { triggerRegistry, CONTEXT_MENU_TRIGGER, attachAction } from '../../triggers'; +import { Action, ActionContext, actionRegistry, IncompatibleActionError } from '../../actions'; +import { EmbeddableInput, Embeddable, EmbeddableOutput, IEmbeddable } from '../../embeddables'; + +export const SAY_HELLO_ACTION = 'SAY_HELLO_ACTION'; + +export interface FullNameEmbeddableOutput extends EmbeddableOutput { + fullName: string; +} + +export function hasFullNameOutput( + embeddable: IEmbeddable | Embeddable +) { + return ( + (embeddable as Embeddable).getOutput().fullName !== + undefined + ); +} + +function openSayHelloFlyout(hello: string) { + npStart.core.overlays.openFlyout({hello}); +} + +export class SayHelloAction extends Action { + public readonly type = SAY_HELLO_ACTION; + private sayHello: (name: string) => void; + + // Taking in a function, instead of always directly interacting with the dom, + // can make testing the execute part of the action easier. + constructor(sayHello: (name: string) => void = openSayHelloFlyout) { + super(SAY_HELLO_ACTION); + this.sayHello = sayHello; + } + + getDisplayName() { + return 'Say hello'; + } + + // Can use typescript generics to get compiler time warnings for immediate feedback if + // the context is not compatible. + async isCompatible( + context: ActionContext> + ) { + // Option 1: only compatible with Greeting Embeddables. + // return context.embeddable.type === CONTACT_CARD_EMBEDDABLE; + + // Option 2: require an embeddable with a specific input or output shape + return hasFullNameOutput(context.embeddable); + } + + async execute( + context: ActionContext< + Embeddable, + { message?: string } + > + ) { + if (!(await this.isCompatible(context))) { + throw new IncompatibleActionError(); + } + + const greeting = `Hello, ${context.embeddable.getOutput().fullName}`; + + if (context.triggerContext && context.triggerContext.message) { + this.sayHello(`${greeting}. ${context.triggerContext.message}`); + } else { + this.sayHello(greeting); + } + } +} + +actionRegistry.set(SAY_HELLO_ACTION, new SayHelloAction()); +attachAction(triggerRegistry, { triggerId: CONTEXT_MENU_TRIGGER, actionId: SAY_HELLO_ACTION }); diff --git a/src/legacy/core_plugins/embeddable_api/public/test_samples/actions/send_message_action.tsx b/src/legacy/core_plugins/embeddable_api/public/test_samples/actions/send_message_action.tsx new file mode 100644 index 0000000000000..fd9ef395a177c --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/test_samples/actions/send_message_action.tsx @@ -0,0 +1,86 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { npStart } from 'ui/new_platform'; +import { EuiFlyoutBody } from '@elastic/eui'; +import { + Action, + ActionContext, + actionRegistry, + IncompatibleActionError, + triggerRegistry, + CONTEXT_MENU_TRIGGER, +} from '../..'; +import { attachAction } from '../../triggers/attach_action'; +import { Embeddable, EmbeddableInput } from '../../embeddables'; +import { GetMessageModal } from './get_message_modal'; +import { FullNameEmbeddableOutput, hasFullNameOutput } from './say_hello_action'; + +export const SEND_MESSAGE_ACTION = 'SEND_MESSAGE_ACTION'; + +export class SendMessageAction extends Action { + public readonly type = SEND_MESSAGE_ACTION; + constructor() { + super(SEND_MESSAGE_ACTION); + } + + getDisplayName() { + return 'Send message'; + } + + async isCompatible( + context: ActionContext> + ) { + return hasFullNameOutput(context.embeddable); + } + + async sendMessage( + context: ActionContext>, + message: string + ) { + const greeting = `Hello, ${context.embeddable.getOutput().fullName}`; + + const content = message ? `${greeting}. ${message}` : greeting; + npStart.core.overlays.openFlyout({content}); + } + + async execute( + context: ActionContext< + Embeddable, + { message?: string } + > + ) { + if (!(await this.isCompatible(context))) { + throw new IncompatibleActionError(); + } + + const modal = npStart.core.overlays.openModal( + modal.close()} + onDone={message => { + modal.close(); + this.sendMessage(context, message); + }} + /> + ); + } +} + +actionRegistry.set(SEND_MESSAGE_ACTION, new SendMessageAction()); +attachAction(triggerRegistry, { triggerId: CONTEXT_MENU_TRIGGER, actionId: SEND_MESSAGE_ACTION }); diff --git a/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/contact_card/contact_card.tsx b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/contact_card/contact_card.tsx new file mode 100644 index 0000000000000..4a0136137bc83 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/contact_card/contact_card.tsx @@ -0,0 +1,101 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { + // @ts-ignore + EuiCard, + EuiFlexItem, + EuiFlexGroup, + EuiFormRow, +} from '@elastic/eui'; + +import { Subscription } from 'rxjs'; +import { EuiButton } from '@elastic/eui'; +import * as Rx from 'rxjs'; +import { executeTriggerActions } from '../../../triggers'; +import { ContactCardEmbeddable, CONTACT_USER_TRIGGER } from './contact_card_embeddable'; + +interface Props { + embeddable: ContactCardEmbeddable; +} + +interface State { + fullName: string; + firstName: string; +} + +export class ContactCardEmbeddableComponent extends React.Component { + private subscription?: Subscription; + private mounted: boolean = false; + + constructor(props: Props) { + super(props); + this.state = { + fullName: this.props.embeddable.getOutput().fullName, + firstName: this.props.embeddable.getInput().firstName, + }; + } + + componentDidMount() { + this.mounted = true; + this.subscription = Rx.merge( + this.props.embeddable.getOutput$(), + this.props.embeddable.getInput$() + ).subscribe(() => { + if (this.mounted) { + this.setState({ + fullName: this.props.embeddable.getOutput().fullName, + firstName: this.props.embeddable.getInput().firstName, + }); + } + }); + } + + componentWillUnmount() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + this.mounted = false; + } + + emitContactTrigger = () => { + executeTriggerActions(CONTACT_USER_TRIGGER, { + embeddable: this.props.embeddable, + triggerContext: {}, + }); + }; + + getCardFooterContent = () => ( + + + + {`Contact ${ + this.state.firstName + }`} + + + + ); + + render() { + return ( + + ); + } +} diff --git a/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/contact_card/contact_card_embeddable.tsx b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/contact_card/contact_card_embeddable.tsx new file mode 100644 index 0000000000000..ed61d4495800b --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/contact_card/contact_card_embeddable.tsx @@ -0,0 +1,95 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import ReactDom from 'react-dom'; +import { Subscription } from 'rxjs'; +import { Container } from '../../../containers'; +import { triggerRegistry } from '../../../triggers'; +import { EmbeddableOutput, Embeddable, EmbeddableInput } from '../../../embeddables'; +import { CONTACT_CARD_EMBEDDABLE } from './contact_card_embeddable_factory'; +import { ContactCardEmbeddableComponent } from './contact_card'; +import { SEND_MESSAGE_ACTION } from '../../actions/send_message_action'; + +export interface ContactCardEmbeddableInput extends EmbeddableInput { + firstName: string; + lastName?: string; + nameTitle?: string; +} + +export interface ContactCardEmbeddableOutput extends EmbeddableOutput { + fullName: string; + originalLastName?: string; +} + +function getFullName(input: ContactCardEmbeddableInput) { + const { nameTitle, firstName, lastName } = input; + const nameParts = [nameTitle, firstName, lastName].filter(name => name !== undefined); + return nameParts.join(' '); +} + +export class ContactCardEmbeddable extends Embeddable< + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput +> { + private subscription: Subscription; + private node?: Element; + public readonly type: string = CONTACT_CARD_EMBEDDABLE; + + constructor(initialInput: ContactCardEmbeddableInput, parent?: Container) { + super( + initialInput, + { + fullName: getFullName(initialInput), + originalLastName: initialInput.lastName, + defaultTitle: `Hello ${getFullName(initialInput)}`, + }, + parent + ); + + this.subscription = this.getInput$().subscribe(() => { + const fullName = getFullName(this.input); + this.updateOutput({ + fullName, + defaultTitle: `Hello ${fullName}`, + }); + }); + } + + public render(node: HTMLElement) { + this.node = node; + ReactDom.render(, node); + } + + public destroy() { + super.destroy(); + this.subscription.unsubscribe(); + if (this.node) { + ReactDom.unmountComponentAtNode(this.node); + } + } + + public reload() {} +} + +export const CONTACT_USER_TRIGGER = 'CONTACT_USER_TRIGGER'; + +triggerRegistry.set(CONTACT_USER_TRIGGER, { + id: CONTACT_USER_TRIGGER, + actionIds: [SEND_MESSAGE_ACTION], +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx new file mode 100644 index 0000000000000..22c0364a1eb20 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx @@ -0,0 +1,68 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { npStart } from 'ui/new_platform'; +import { embeddableFactories, EmbeddableFactory } from '../../../embeddables'; +import { Container } from '../../../containers'; +import { ContactCardEmbeddable, ContactCardEmbeddableInput } from './contact_card_embeddable'; +import { ContactCardInitializer } from './contact_card_initializer'; + +export const CONTACT_CARD_EMBEDDABLE = 'CONTACT_CARD_EMBEDDABLE'; + +export class ContactCardEmbeddableFactory extends EmbeddableFactory { + public readonly type = CONTACT_CARD_EMBEDDABLE; + + public isEditable() { + return true; + } + + public getDisplayName() { + return i18n.translate('kbn.embeddable.samples.contactCard.displayName', { + defaultMessage: 'contact card', + }); + } + + public getExplicitInput(): Promise> { + return new Promise(resolve => { + const modalSession = npStart.core.overlays.openModal( + { + modalSession.close(); + resolve(undefined); + }} + onCreate={(input: { firstName: string; lastName: string }) => { + modalSession.close(); + resolve(input); + }} + />, + { + 'data-test-subj': 'createContactCardEmbeddable', + } + ); + }); + } + + public async create(initialInput: ContactCardEmbeddableInput, parent?: Container) { + return new ContactCardEmbeddable(initialInput, parent); + } +} + +embeddableFactories.set(CONTACT_CARD_EMBEDDABLE, new ContactCardEmbeddableFactory()); diff --git a/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/contact_card/contact_card_initializer.tsx b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/contact_card/contact_card_initializer.tsx new file mode 100644 index 0000000000000..e90002261f159 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/contact_card/contact_card_initializer.tsx @@ -0,0 +1,97 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + EuiForm, + EuiFormRow, + EuiFieldText, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiButton, + EuiModalFooter, + EuiButtonEmpty, +} from '@elastic/eui'; +import React, { Component } from 'react'; + +export interface ContactCardInitializerProps { + onCreate: (name: { lastName: string; firstName: string }) => void; + onCancel: () => void; +} + +interface State { + firstName?: string; + lastName?: string; +} + +export class ContactCardInitializer extends Component { + constructor(props: ContactCardInitializerProps) { + super(props); + this.state = {}; + } + + render() { + return ( +
+ + Create a new greeting card + + + + + + this.setState({ firstName: e.target.value })} + /> + + + + this.setState({ lastName: e.target.value })} + /> + + + + + + Cancel + + { + if (this.state.lastName && this.state.firstName) { + this.props.onCreate({ + firstName: this.state.firstName, + lastName: this.state.lastName, + }); + } + }} + fill + > + Create + + +
+ ); + } +} diff --git a/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/contact_card/index.ts b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/contact_card/index.ts new file mode 100644 index 0000000000000..3065ed28f4976 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/contact_card/index.ts @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { + ContactCardEmbeddable, + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, +} from './contact_card_embeddable'; + +export { + ContactCardEmbeddableFactory, + CONTACT_CARD_EMBEDDABLE, +} from './contact_card_embeddable_factory'; +export { SlowContactCardEmbeddableFactory } from './slow_contact_card_embeddable_factory'; +export { ContactCardInitializerProps } from './contact_card_initializer'; diff --git a/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/contact_card/slow_contact_card_embeddable_factory.ts b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/contact_card/slow_contact_card_embeddable_factory.ts new file mode 100644 index 0000000000000..902f932c5334d --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/contact_card/slow_contact_card_embeddable_factory.ts @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Container, EmbeddableFactory } from '../../..'; +import { ContactCardEmbeddable, ContactCardEmbeddableInput } from './contact_card_embeddable'; + +export const CONTACT_CARD_EMBEDDABLE = 'CONTACT_CARD_EMBEDDABLE'; + +export class SlowContactCardEmbeddableFactory extends EmbeddableFactory< + ContactCardEmbeddableInput +> { + private loadTickCount = 0; + public readonly type = CONTACT_CARD_EMBEDDABLE; + + constructor(options: { loadTickCount?: number } = {}) { + super(); + if (options.loadTickCount) { + this.loadTickCount = options.loadTickCount; + } + } + + public isEditable() { + return true; + } + + public getDisplayName() { + return 'slow to load contact card'; + } + + public async create(initialInput: ContactCardEmbeddableInput, parent?: Container) { + for (let i = 0; i < this.loadTickCount; i++) { + await Promise.resolve(); + } + return new ContactCardEmbeddable(initialInput, parent); + } +} diff --git a/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/empty_embeddable.tsx b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/empty_embeddable.tsx new file mode 100644 index 0000000000000..7f57ea31b59a2 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/empty_embeddable.tsx @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Embeddable, EmbeddableInput, EmbeddableOutput } from '../..'; + +export const EMPTY_EMBEDDABLE = 'EMPTY_EMBEDDABLE'; + +export class EmptyEmbeddable extends Embeddable { + public readonly type = EMPTY_EMBEDDABLE; + constructor(initialInput: EmbeddableInput) { + super(initialInput, {}); + } + public render() {} + public reload() {} +} diff --git a/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/filterable_container.tsx b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/filterable_container.tsx new file mode 100644 index 0000000000000..56e5d0779c8fe --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/filterable_container.tsx @@ -0,0 +1,56 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Filter } from '@kbn/es-query'; +import { EmbeddableFactory } from '../../embeddables'; +import { IRegistry } from '../../types'; +import { Container, ContainerInput } from '../../containers'; + +export const FILTERABLE_CONTAINER = 'FILTERABLE_CONTAINER'; + +export interface FilterableContainerInput extends ContainerInput { + filters: Filter[]; +} + +export interface InheritedChildrenInput { + filters: Filter[]; + id?: string; +} + +export class FilterableContainer extends Container< + InheritedChildrenInput, + FilterableContainerInput +> { + public readonly type = FILTERABLE_CONTAINER; + + constructor( + initialInput: FilterableContainerInput, + embeddableFactories: IRegistry, + parent?: Container + ) { + super(initialInput, { embeddableLoaded: {} }, embeddableFactories, parent); + } + + public getInheritedInput() { + return { + filters: this.input.filters, + }; + } + + public render() {} +} diff --git a/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/filterable_container_factory.ts b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/filterable_container_factory.ts new file mode 100644 index 0000000000000..a413e9be43cb6 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/filterable_container_factory.ts @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { Container, embeddableFactories, EmbeddableFactory } from '../..'; +import { + FilterableContainer, + FilterableContainerInput, + FILTERABLE_CONTAINER, +} from './filterable_container'; + +export class FilterableContainerFactory extends EmbeddableFactory { + public readonly type = FILTERABLE_CONTAINER; + + public getDisplayName() { + return i18n.translate('kbn.embeddable.samples.filterable.displayName', { + defaultMessage: 'filterable dashboard', + }); + } + + public isEditable() { + return true; + } + + public async create(initialInput: FilterableContainerInput, parent?: Container) { + return new FilterableContainer(initialInput, embeddableFactories, parent); + } +} + +embeddableFactories.set(FILTERABLE_CONTAINER, new FilterableContainerFactory()); diff --git a/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/filterable_embeddable.tsx b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/filterable_embeddable.tsx new file mode 100644 index 0000000000000..aeee761b383b1 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/filterable_embeddable.tsx @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Adapters } from 'ui/inspector'; +import { Filter } from '@kbn/es-query'; +import { IContainer } from '../../containers'; +import { EmbeddableOutput, EmbeddableInput, Embeddable } from '../../embeddables'; + +export const FILTERABLE_EMBEDDABLE = 'FILTERABLE_EMBEDDABLE'; + +export interface FilterableEmbeddableInput extends EmbeddableInput { + filters: Filter[]; +} + +export class FilterableEmbeddable extends Embeddable { + public readonly type = FILTERABLE_EMBEDDABLE; + constructor(initialInput: FilterableEmbeddableInput, parent?: IContainer) { + super(initialInput, {}, parent); + } + + public getInspectorAdapters() { + const inspectorAdapters: Adapters = { + filters: `My filters are ${JSON.stringify(this.input.filters)}`, + }; + return inspectorAdapters; + } + + public render() {} + + public reload() {} +} diff --git a/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/filterable_embeddable_factory.ts b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/filterable_embeddable_factory.ts new file mode 100644 index 0000000000000..daf462d8b0b47 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/filterable_embeddable_factory.ts @@ -0,0 +1,47 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { + FilterableEmbeddable, + FilterableEmbeddableInput, + FILTERABLE_EMBEDDABLE, +} from './filterable_embeddable'; +import { embeddableFactories, EmbeddableFactory } from '../../embeddables'; +import { IContainer } from '../../containers'; + +export class FilterableEmbeddableFactory extends EmbeddableFactory { + public readonly type = FILTERABLE_EMBEDDABLE; + + public isEditable() { + return true; + } + + public getDisplayName() { + return i18n.translate('kbn.embeddable.samples.filterable.displayName', { + defaultMessage: 'filterable', + }); + } + + public async create(initialInput: FilterableEmbeddableInput, parent?: IContainer) { + return new FilterableEmbeddable(initialInput, parent); + } +} + +embeddableFactories.set(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory()); diff --git a/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/hello_world/hello_world_embeddable.tsx b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/hello_world/hello_world_embeddable.tsx new file mode 100644 index 0000000000000..dc41eb037caf1 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/hello_world/hello_world_embeddable.tsx @@ -0,0 +1,54 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Embeddable, EmbeddableInput, IContainer } from '../../..'; + +export const HELLO_WORLD_EMBEDDABLE_TYPE = 'HELLO_WORLD_EMBEDDABLE_TYPE'; + +export class HelloWorldEmbeddable extends Embeddable { + // The type of this embeddable. This will be used to find the appropriate factory + // to instantiate this kind of embeddable. + public readonly type = HELLO_WORLD_EMBEDDABLE_TYPE; + + constructor(initialInput: EmbeddableInput, parent?: IContainer) { + super( + // Input state is irrelevant to this embeddable, just pass it along. + initialInput, + // Initial output state - this embeddable does not do anything with output, so just + // pass along an empty object. + {}, + // Optional parent component, this embeddable can optionally be rendered inside a container. + parent + ); + } + + /** + * Render yourself at the dom node using whatever framework you like, angular, react, or just plain + * vanilla js. + * @param node + */ + public render(node: HTMLElement) { + node.innerHTML = '
HELLO WORLD!
'; + } + + /** + * This is mostly relevant for time based embeddables which need to update data + * even if EmbeddableInput has not changed at all. + */ + public reload() {} +} diff --git a/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/hello_world/hello_world_embeddable_factory.ts b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/hello_world/hello_world_embeddable_factory.ts new file mode 100644 index 0000000000000..4becc8d149a84 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/hello_world/hello_world_embeddable_factory.ts @@ -0,0 +1,47 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { IContainer, EmbeddableInput, embeddableFactories, EmbeddableFactory } from '../../..'; +import { HelloWorldEmbeddable, HELLO_WORLD_EMBEDDABLE_TYPE } from './hello_world_embeddable'; + +export class HelloWorldEmbeddableFactory extends EmbeddableFactory { + public readonly type = HELLO_WORLD_EMBEDDABLE_TYPE; + + /** + * In our simple example, we let everyone have permissions to edit this. Most + * embeddables should check the UI Capabilities service to be sure of + * the right permissions. + */ + public isEditable() { + return true; + } + + public async create(initialInput: EmbeddableInput, parent?: IContainer) { + return new HelloWorldEmbeddable(initialInput, parent); + } + + public getDisplayName() { + return i18n.translate('kbn.embeddable.samples.helloworld.displayName', { + defaultMessage: 'hello world', + }); + } +} + +embeddableFactories.set(HELLO_WORLD_EMBEDDABLE_TYPE, new HelloWorldEmbeddableFactory()); diff --git a/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/hello_world/index.ts b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/hello_world/index.ts new file mode 100644 index 0000000000000..dc34a0339611a --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/hello_world/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { HelloWorldEmbeddableFactory } from './hello_world_embeddable_factory'; +export { HelloWorldEmbeddable, HELLO_WORLD_EMBEDDABLE_TYPE } from './hello_world_embeddable'; diff --git a/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/hello_world_container.tsx b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/hello_world_container.tsx new file mode 100644 index 0000000000000..f4fdc8329097c --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/hello_world_container.tsx @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { I18nProvider } from '@kbn/i18n/react'; +import { Container, ViewMode, ContainerInput } from '../..'; +import { HelloWorldContainerComponent } from './hello_world_container_component'; +import { IRegistry } from '../../types'; +import { EmbeddableFactory } from '../../embeddables'; + +export const HELLO_WORLD_CONTAINER = 'HELLO_WORLD_CONTAINER'; + +interface InheritedInput { + id: string; + viewMode: ViewMode; + lastName: string; +} + +export class HelloWorldContainer extends Container { + public readonly type = HELLO_WORLD_CONTAINER; + + constructor(input: ContainerInput, embeddableFactories: IRegistry) { + super(input, { embeddableLoaded: {} }, embeddableFactories); + } + + public getInheritedInput(id: string) { + return { + id, + viewMode: this.input.viewMode || ViewMode.EDIT, + lastName: 'foo', + }; + } + + public render(node: HTMLElement) { + ReactDOM.render( + + + , + node + ); + } +} diff --git a/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/hello_world_container_component.tsx b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/hello_world_container_component.tsx new file mode 100644 index 0000000000000..59eba07286dac --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/hello_world_container_component.tsx @@ -0,0 +1,106 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { Component, RefObject } from 'react'; +import { Subscription } from 'rxjs'; + +import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { IContainer, PanelState, EmbeddableChildPanel } from '../..'; + +interface Props { + container: IContainer; +} + +interface State { + panels: { [key: string]: PanelState }; + loaded: { [key: string]: boolean }; +} + +export class HelloWorldContainerComponent extends Component { + private roots: { [key: string]: RefObject } = {}; + private mounted: boolean = false; + private inputSubscription?: Subscription; + private outputSubscription?: Subscription; + + public constructor(props: Props) { + super(props); + + Object.values(this.props.container.getInput().panels).forEach(panelState => { + this.roots[panelState.explicitInput.id] = React.createRef(); + }); + + this.state = { + loaded: this.props.container.getOutput().embeddableLoaded, + panels: this.props.container.getInput().panels, + }; + } + + public async componentDidMount() { + this.mounted = true; + + this.inputSubscription = this.props.container.getInput$().subscribe(() => { + if (this.mounted) { + this.setState({ panels: this.props.container.getInput().panels }); + } + }); + + this.outputSubscription = this.props.container.getOutput$().subscribe(() => { + if (this.mounted) { + this.setState({ loaded: this.props.container.getOutput().embeddableLoaded }); + } + }); + } + + public componentWillUnmount() { + this.mounted = false; + this.props.container.destroy(); + + if (this.inputSubscription) { + this.inputSubscription.unsubscribe(); + } + + if (this.outputSubscription) { + this.outputSubscription.unsubscribe(); + } + } + + public render() { + return ( +
+

HELLO WORLD! These are my precious embeddable children:

+ + {this.renderList()} +
+ ); + } + + private renderList() { + const list = Object.values(this.state.panels).map(panelState => { + const item = ( + + + + ); + return item; + }); + return list; + } +} diff --git a/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/index.ts b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/index.ts new file mode 100644 index 0000000000000..37dd8bcf0689f --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/index.ts @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { + ContactCardEmbeddable, + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardInitializerProps, + CONTACT_CARD_EMBEDDABLE, + ContactCardEmbeddableFactory, + SlowContactCardEmbeddableFactory, +} from './contact_card'; +export { + HelloWorldEmbeddableFactory, + HELLO_WORLD_EMBEDDABLE_TYPE, + HelloWorldEmbeddable, +} from './hello_world'; +export { HelloWorldContainer } from './hello_world_container'; +export { EmptyEmbeddable } from './empty_embeddable'; +export { + FilterableEmbeddable, + FilterableEmbeddableInput, + FILTERABLE_EMBEDDABLE, +} from './filterable_embeddable'; +export { + FilterableContainer, + FILTERABLE_CONTAINER, + FilterableContainerInput, +} from './filterable_container'; +export { FilterableEmbeddableFactory } from './filterable_embeddable_factory'; diff --git a/src/legacy/core_plugins/embeddable_api/public/test_samples/index.ts b/src/legacy/core_plugins/embeddable_api/public/test_samples/index.ts new file mode 100644 index 0000000000000..5c947da56b02f --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/test_samples/index.ts @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { + EmptyEmbeddable, + HelloWorldEmbeddable, + HelloWorldEmbeddableFactory, + ContactCardEmbeddableFactory, + SlowContactCardEmbeddableFactory, + CONTACT_CARD_EMBEDDABLE, + ContactCardEmbeddable, + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + HelloWorldContainer, + FilterableContainer, + FilterableEmbeddable, + FilterableEmbeddableFactory, + FILTERABLE_EMBEDDABLE, + FILTERABLE_CONTAINER, + FilterableContainerInput, + FilterableEmbeddableInput, + ContactCardInitializerProps, + HELLO_WORLD_EMBEDDABLE_TYPE, +} from './embeddables'; + +export { + SayHelloAction, + EditModeAction, + HelloWorldAction, + RestrictedAction, + HELLO_WORLD_ACTION_ID, + SEND_MESSAGE_ACTION, +} from './actions'; diff --git a/src/legacy/core_plugins/embeddable_api/public/triggers/attach_action.ts b/src/legacy/core_plugins/embeddable_api/public/triggers/attach_action.ts new file mode 100644 index 0000000000000..711f641b0cbeb --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/triggers/attach_action.ts @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IRegistry, Trigger } from '../types'; + +export function attachAction( + triggerRegistry: IRegistry, + { triggerId, actionId }: { triggerId: string; actionId: string } +) { + const trigger = triggerRegistry.get(triggerId); + if (!trigger) { + throw new Error(`No trigger with is ${triggerId} exists`); + } + + if (!trigger.actionIds.find(id => id === actionId)) { + trigger.actionIds.push(actionId); + } + triggerRegistry.set(trigger.id, trigger); +} diff --git a/src/legacy/core_plugins/embeddable_api/public/triggers/detach_action.ts b/src/legacy/core_plugins/embeddable_api/public/triggers/detach_action.ts new file mode 100644 index 0000000000000..7cb4670e50b12 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/triggers/detach_action.ts @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IRegistry, Trigger } from '../types'; + +export function detachAction( + triggerRegistry: IRegistry, + { triggerId, actionId }: { triggerId: string; actionId: string } +) { + const trigger = triggerRegistry.get(triggerId); + if (!trigger) { + throw new Error(`No trigger with is ${triggerId} exists`); + } + + trigger.actionIds = trigger.actionIds.filter(id => id !== actionId); + triggerRegistry.set(trigger.id, trigger); +} diff --git a/src/legacy/core_plugins/embeddable_api/public/triggers/execute_trigger_actions.test.ts b/src/legacy/core_plugins/embeddable_api/public/triggers/execute_trigger_actions.test.ts new file mode 100644 index 0000000000000..beefff8f75b14 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/triggers/execute_trigger_actions.test.ts @@ -0,0 +1,143 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import '../np_core.test.mocks'; + +import { EuiContextMenuPanelDescriptor } from '@elastic/eui'; + +const executeFn = jest.fn(); + +jest.mock('../context_menu_actions/open_context_menu', () => ({ + openContextMenu: (actions: EuiContextMenuPanelDescriptor[]) => jest.fn()(actions), +})); + +import { triggerRegistry } from '../triggers'; +import { Action, ActionContext, actionRegistry } from '../actions'; +import { executeTriggerActions } from './execute_trigger_actions'; +import { ContactCardEmbeddable } from '../test_samples'; + +class TestAction extends Action { + public readonly type = 'testAction'; + public checkCompatibility: (context: ActionContext) => boolean; + + constructor(id: string, checkCompatibility: (context: ActionContext) => boolean) { + super(id); + this.checkCompatibility = checkCompatibility; + } + + public getDisplayName() { + return 'test'; + } + + async isCompatible(context: ActionContext) { + return this.checkCompatibility(context); + } + + execute(context: ActionContext) { + executeFn(context); + } +} + +beforeEach(() => { + triggerRegistry.reset(); + actionRegistry.reset(); + executeFn.mockReset(); +}); + +afterAll(() => { + triggerRegistry.reset(); +}); + +test('executeTriggerActions executes a single action mapped to a trigger', async () => { + const testTrigger = { + id: 'MYTRIGGER', + title: 'My trigger', + actionIds: ['test1'], + }; + triggerRegistry.set(testTrigger.id, testTrigger); + + actionRegistry.set('test1', new TestAction('test1', () => true)); + + const context = { + embeddable: new ContactCardEmbeddable({ id: '123', firstName: 'Stacey', lastName: 'G' }), + triggerContext: {}, + }; + + await executeTriggerActions('MYTRIGGER', context); + + expect(executeFn).toBeCalledTimes(1); + expect(executeFn).toBeCalledWith(context); +}); + +test('executeTriggerActions throws an error if the action id does not exist', async () => { + const testTrigger = { + id: 'MYTRIGGER', + title: 'My trigger', + actionIds: ['testaction'], + }; + triggerRegistry.set(testTrigger.id, testTrigger); + + const context = { + embeddable: new ContactCardEmbeddable({ id: '123', firstName: 'Stacey', lastName: 'G' }), + triggerContext: {}, + }; + await expect(executeTriggerActions('MYTRIGGER', context)).rejects.toThrowError(); +}); + +test('executeTriggerActions does not execute an incompatible action', async () => { + const testTrigger = { + id: 'MYTRIGGER', + title: 'My trigger', + actionIds: ['test1'], + }; + triggerRegistry.set(testTrigger.id, testTrigger); + + actionRegistry.set( + 'test1', + new TestAction('test1', ({ embeddable }) => embeddable.id === 'executeme') + ); + + const context = { + embeddable: new ContactCardEmbeddable({ id: 'executeme', firstName: 'Stacey', lastName: 'G' }), + triggerContext: {}, + }; + + await executeTriggerActions('MYTRIGGER', context); + expect(executeFn).toBeCalledTimes(1); +}); + +test('executeTriggerActions shows a context menu when more than one action is mapped to a trigger', async () => { + const testTrigger = { + id: 'MYTRIGGER', + title: 'My trigger', + actionIds: ['test1', 'test2'], + }; + triggerRegistry.set(testTrigger.id, testTrigger); + + actionRegistry.set('test1', new TestAction('test1', () => true)); + actionRegistry.set('test2', new TestAction('test2', () => true)); + + const context = { + embeddable: new ContactCardEmbeddable({ id: 'executeme', firstName: 'Stacey', lastName: 'G' }), + triggerContext: {}, + }; + + await executeTriggerActions('MYTRIGGER', context); + expect(executeFn).toBeCalledTimes(0); +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/triggers/execute_trigger_actions.ts b/src/legacy/core_plugins/embeddable_api/public/triggers/execute_trigger_actions.ts new file mode 100644 index 0000000000000..f7dbe143645c9 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/triggers/execute_trigger_actions.ts @@ -0,0 +1,62 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { actionRegistry } from '../actions'; +import { triggerRegistry } from '../triggers'; +import { buildContextMenuForActions, openContextMenu } from '../context_menu_actions'; +import { IEmbeddable } from '../embeddables'; +import { getActionsForTrigger } from '../get_actions_for_trigger'; + +export async function executeTriggerActions( + triggerId: string, + { + embeddable, + triggerContext, + }: { + embeddable: IEmbeddable; + triggerContext: any; + } +) { + const actions = await getActionsForTrigger(actionRegistry, triggerRegistry, triggerId, { + embeddable, + }); + + if (actions.length > 1) { + const closeMyContextMenuPanel = () => { + session.close(); + }; + + const panel = await buildContextMenuForActions({ + actions, + actionContext: triggerContext, + closeMenu: closeMyContextMenuPanel, + }); + + const session = openContextMenu([panel]); + } else if (actions.length === 1) { + const href = actions[0].getHref({ + embeddable, + triggerContext, + }); + if (href) { + window.location.href = href; + } else { + actions[0].execute({ embeddable, triggerContext }); + } + } +} diff --git a/src/legacy/core_plugins/embeddable_api/public/triggers/index.ts b/src/legacy/core_plugins/embeddable_api/public/triggers/index.ts new file mode 100644 index 0000000000000..134d3387f1eee --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/triggers/index.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { attachAction } from './attach_action'; +export { executeTriggerActions } from './execute_trigger_actions'; + +export const CONTEXT_MENU_TRIGGER = 'CONTEXT_MENU_TRIGGER'; +export const APPLY_FILTER_TRIGGER = 'FITLER_TRIGGER'; + +import { createRegistry } from '../create_registry'; +import { Trigger } from '../types'; + +export const triggerRegistry = createRegistry(); + +triggerRegistry.set(CONTEXT_MENU_TRIGGER, { + id: CONTEXT_MENU_TRIGGER, + title: 'Context menu', + actionIds: [], +}); + +triggerRegistry.set(APPLY_FILTER_TRIGGER, { + id: APPLY_FILTER_TRIGGER, + title: 'Filter click', + actionIds: [], +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/triggers/trigger_registry.test.ts b/src/legacy/core_plugins/embeddable_api/public/triggers/trigger_registry.test.ts new file mode 100644 index 0000000000000..f457b42118887 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/triggers/trigger_registry.test.ts @@ -0,0 +1,74 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import '../np_core.test.mocks'; + +import { triggerRegistry } from '../triggers'; +import { HELLO_WORLD_ACTION_ID } from '../test_samples'; +import { attachAction } from './attach_action'; +import { detachAction } from './detach_action'; + +beforeAll(() => { + triggerRegistry.reset(); +}); + +afterAll(() => { + triggerRegistry.reset(); +}); + +test('TriggerRegistry adding and getting a new trigger', async () => { + const testTrigger = { + id: 'MYTRIGGER', + title: 'My trigger', + actionIds: ['123'], + }; + triggerRegistry.set(testTrigger.id, testTrigger); + + expect(triggerRegistry.get('MYTRIGGER')).toBe(testTrigger); +}); + +test('TriggerRegistry attach a trigger to an action', async () => { + attachAction(triggerRegistry, { triggerId: 'MYTRIGGER', actionId: HELLO_WORLD_ACTION_ID }); + const trigger = triggerRegistry.get('MYTRIGGER'); + expect(trigger).toBeDefined(); + if (trigger) { + expect(trigger.actionIds).toEqual(['123', HELLO_WORLD_ACTION_ID]); + } +}); + +test('TriggerRegistry dettach a trigger from an action', async () => { + detachAction(triggerRegistry, { triggerId: 'MYTRIGGER', actionId: HELLO_WORLD_ACTION_ID }); + const trigger = triggerRegistry.get('MYTRIGGER'); + expect(trigger).toBeDefined(); + if (trigger) { + expect(trigger.actionIds).toEqual(['123']); + } +}); + +test('TriggerRegistry dettach an invalid trigger from an action throws an error', async () => { + expect(() => + detachAction(triggerRegistry, { triggerId: 'i do not exist', actionId: HELLO_WORLD_ACTION_ID }) + ).toThrowError(); +}); + +test('TriggerRegistry attach an invalid trigger from an action throws an error', async () => { + expect(() => + attachAction(triggerRegistry, { triggerId: 'i do not exist', actionId: HELLO_WORLD_ACTION_ID }) + ).toThrowError(); +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/types.ts b/src/legacy/core_plugins/embeddable_api/public/types.ts new file mode 100644 index 0000000000000..b4d4a92491021 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/types.ts @@ -0,0 +1,70 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export interface Trigger { + id: string; + title?: string; + description?: string; + actionIds: string[]; +} + +// TODO: use the official base Registry interface when available +export interface IRegistry { + get(id: string): T | undefined; + length(): number; + set(id: string, item: T): void; + reset(): void; + getAll(): T[]; +} + +export interface PropertySpec { + displayName: string; + accessPath: string; + id: string; + description: string; + value?: string; +} + +export interface OutputSpec { + [id: string]: PropertySpec; +} + +export enum ViewMode { + EDIT = 'edit', + VIEW = 'view', +} +export interface TimeRange { + to: string; + from: string; +} + +export interface RefreshConfig { + pause: boolean; + value: number; +} + +export enum QueryLanguageType { + KUERY = 'kuery', + LUCENE = 'lucene', +} + +export interface Query { + language: QueryLanguageType; + query: string; +} diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js index f4adcbf5cb7e3..a8246288eabc7 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js @@ -325,7 +325,13 @@ test('field name change', async () => { // ensure that after async loading is complete the DynamicOptionsSwitch is disabled, because this is not a "string" field expect(component.find('[data-test-subj="listControlDynamicOptionsSwitch"][disabled=true]')).toHaveLength(0); await update(); + + + /* + The issue with enzyme@3.9.0 -> the fix has not been released yet -> https://github.com/airbnb/enzyme/pull/2027 + TODO: Enable the expectation after the next patch released expect(component.find('[data-test-subj="listControlDynamicOptionsSwitch"][disabled=true]')).toHaveLength(1); + */ component.setProps({ controlParams diff --git a/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.js b/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.js index 68a93dea671df..4a18fbb530559 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.js +++ b/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.js @@ -85,11 +85,13 @@ class ListControl extends Control { } const ancestorValues = this.getAncestorValues(); - if (_.isEqual(ancestorValues, this.lastAncestorValues)) { + if (_.isEqual(ancestorValues, this.lastAncestorValues) + && _.isEqual(query, this.lastQuery)) { // short circuit to avoid fetching options list for same ancestor values return; } this.lastAncestorValues = ancestorValues; + this.lastQuery = query; ancestorFilters = this.getAncestorFilters(); } diff --git a/src/legacy/core_plugins/input_control_vis/public/vis_controller.js b/src/legacy/core_plugins/input_control_vis/public/vis_controller.js index 4e1b8bee95b46..07ec0a690b4da 100644 --- a/src/legacy/core_plugins/input_control_vis/public/vis_controller.js +++ b/src/legacy/core_plugins/input_control_vis/public/vis_controller.js @@ -31,7 +31,9 @@ class VisController { this.controls = []; this.queryBarUpdateHandler = this.updateControlsFromKbn.bind(this); - this.vis.API.queryFilter.on('update', this.queryBarUpdateHandler); + + this.updateSubsciption = this.vis.API.queryFilter.getUpdates$() + .subscribe(this.queryBarUpdateHandler); } async render(visData, visParams, status) { @@ -46,7 +48,7 @@ class VisController { } destroy() { - this.vis.API.queryFilter.off('update', this.queryBarUpdateHandler); + this.updateSubsciption.unsubscribe(); unmountComponentAtNode(this.el); } diff --git a/src/legacy/core_plugins/interpreter/common/index.ts b/src/legacy/core_plugins/interpreter/common/index.ts new file mode 100644 index 0000000000000..13ee31e920f1d --- /dev/null +++ b/src/legacy/core_plugins/interpreter/common/index.ts @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { + Datatable, + DatatableColumn, + DatatableRow, + DatatableColumnType, + ExpressionImage, + Filter, + InterpreterErrorType, + isDatatable, + KibanaContext, + KibanaDatatable, + PointSeries, + PointSeriesColumns, + PointSeriesColumn, + PointSeriesColumnName, + Render, + Style, +} from './types'; diff --git a/src/legacy/core_plugins/interpreter/common/types/boolean.js b/src/legacy/core_plugins/interpreter/common/types/boolean.js deleted file mode 100644 index cc5f0a79e39a8..0000000000000 --- a/src/legacy/core_plugins/interpreter/common/types/boolean.js +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export const boolean = () => ({ - name: 'boolean', - from: { - null: () => false, - number: n => Boolean(n), - string: s => Boolean(s), - }, - to: { - render: value => { - const text = `${value}`; - return { - type: 'render', - as: 'text', - value: { text }, - }; - }, - datatable: value => ({ - type: 'datatable', - columns: [{ name: 'value', type: 'boolean' }], - rows: [{ value }], - }), - }, -}); diff --git a/src/legacy/core_plugins/interpreter/common/types/boolean.ts b/src/legacy/core_plugins/interpreter/common/types/boolean.ts new file mode 100644 index 0000000000000..6b68678cf1184 --- /dev/null +++ b/src/legacy/core_plugins/interpreter/common/types/boolean.ts @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ExpressionType } from '../../types'; +import { Datatable } from './datatable'; +import { Render } from './render'; + +const name = 'boolean'; + +export const boolean = (): ExpressionType => ({ + name, + from: { + null: () => false, + number: n => Boolean(n), + string: s => Boolean(s), + }, + to: { + render: (value): Render<{ text: string }> => { + const text = `${value}`; + return { + type: 'render', + as: 'text', + value: { text }, + }; + }, + datatable: (value): Datatable => ({ + type: 'datatable', + columns: [{ name: 'value', type: name }], + rows: [{ value }], + }), + }, +}); diff --git a/src/legacy/core_plugins/interpreter/common/types/datatable.js b/src/legacy/core_plugins/interpreter/common/types/datatable.js deleted file mode 100644 index 1a2e3ab83c2e9..0000000000000 --- a/src/legacy/core_plugins/interpreter/common/types/datatable.js +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { map, pick, zipObject } from 'lodash'; - -export const datatable = () => ({ - name: 'datatable', - validate: datatable => { - // TODO: Check columns types. Only string, boolean, number, date, allowed for now. - if (!datatable.columns) { - throw new Error('datatable must have a columns array, even if it is empty'); - } - - if (!datatable.rows) throw new Error('datatable must have a rows array, even if it is empty'); - }, - serialize: datatable => { - const { columns, rows } = datatable; - return { - ...datatable, - rows: rows.map(row => { - return columns.map(column => row[column.name]); - }), - }; - }, - deserialize: datatable => { - const { columns, rows } = datatable; - return { - ...datatable, - rows: rows.map(row => { - return zipObject(map(columns, 'name'), row); - }), - }; - }, - from: { - null: () => { - return { - type: 'datatable', - rows: [], - columns: [], - }; - }, - pointseries: context => { - return { - type: 'datatable', - rows: context.rows, - columns: map(context.columns, (val, name) => { - return { name: name, type: val.type, role: val.role }; - }), - }; - }, - }, - to: { - render: datatable => { - return { - type: 'render', - as: 'table', - value: { - datatable, - paginate: true, - perPage: 10, - showHeader: true, - }, - }; - }, - pointseries: datatable => { - // datatable columns are an array that looks like [{ name: "one", type: "string" }, { name: "two", type: "string" }] - // rows look like [{ one: 1, two: 2}, { one: 3, two: 4}, ...] - const validFields = ['x', 'y', 'color', 'size', 'text']; - const columns = datatable.columns.filter(column => validFields.includes(column.name)); - const rows = datatable.rows.map(row => pick(row, validFields)); - - return { - type: 'pointseries', - columns: columns.reduce((acc, column) => { - /* pointseries columns are an object that looks like this - * { - * x: { type: "string", expression: "x", role: "dimension" }, - * y: { type: "string", expression: "y", role: "dimension" } - * } - */ - acc[column.name] = { - type: column.type, - expression: column.name, - role: 'dimension', - }; - - return acc; - }, {}), - rows, - }; - }, - }, -}); diff --git a/src/legacy/core_plugins/interpreter/common/types/datatable.ts b/src/legacy/core_plugins/interpreter/common/types/datatable.ts new file mode 100644 index 0000000000000..bc3eee640854f --- /dev/null +++ b/src/legacy/core_plugins/interpreter/common/types/datatable.ts @@ -0,0 +1,162 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { map, pick, zipObject } from 'lodash'; + +import { ExpressionType } from '../../types'; +import { PointSeries } from './pointseries'; +import { Render } from './render'; + +const name = 'datatable'; + +/** + * A Utility function that Typescript can use to determine if an object is a Datatable. + * @param datatable + */ +export const isDatatable = (datatable: any): datatable is Datatable => + !!datatable && datatable.type === 'datatable'; + +/** + * This type represents the `type` of any `DatatableColumn` in a `Datatable`. + */ +export type DatatableColumnType = 'string' | 'number' | 'boolean' | 'date' | 'null'; + +/** + * This type represents a `DatatableRow` in a `Datatable`. + */ +export type DatatableRow = Record; + +/** + * This type represents the shape of a column in a `Datatable`. + */ +export interface DatatableColumn { + name: string; + type: DatatableColumnType; +} + +/** + * A `Datatable` in Canvas is a unique structure that represents tabulated data. + */ +export interface Datatable { + type: typeof name; + columns: DatatableColumn[]; + rows: DatatableRow[]; +} + +interface SerializedDatatable extends Datatable { + rows: string[][]; +} + +interface RenderedDatatable { + datatable: Datatable; + paginate: boolean; + perPage: number; + showHeader: boolean; +} + +export const datatable = (): ExpressionType => ({ + name, + validate: table => { + // TODO: Check columns types. Only string, boolean, number, date, allowed for now. + if (!table.columns) { + throw new Error('datatable must have a columns array, even if it is empty'); + } + + if (!table.rows) { + throw new Error('datatable must have a rows array, even if it is empty'); + } + }, + serialize: table => { + const { columns, rows } = table; + return { + ...table, + rows: rows.map(row => { + return columns.map(column => row[column.name]); + }), + }; + }, + deserialize: table => { + const { columns, rows } = table; + return { + ...table, + rows: rows.map((row: any) => { + return zipObject(map(columns, 'name'), row); + }), + }; + }, + from: { + null: () => { + return { + type: name, + rows: [], + columns: [], + }; + }, + pointseries: (context: PointSeries) => { + return { + type: name, + rows: context.rows, + columns: map(context.columns, (val, colName) => { + return { name: colName!, type: val.type }; + }), + }; + }, + }, + to: { + render: (table): Render => { + return { + type: 'render', + as: 'table', + value: { + datatable: table, + paginate: true, + perPage: 10, + showHeader: true, + }, + }; + }, + pointseries: (table): PointSeries => { + // datatable columns are an array that looks like [{ name: "one", type: "string" }, { name: "two", type: "string" }] + // rows look like [{ one: 1, two: 2}, { one: 3, two: 4}, ...] + const validFields = ['x', 'y', 'color', 'size', 'text']; + const columns = table.columns.filter(column => validFields.includes(column.name)); + const rows = table.rows.map(row => pick(row, validFields)); + + return { + type: 'pointseries', + columns: columns.reduce((acc: Record, column) => { + /* pointseries columns are an object that looks like this + * { + * x: { type: "string", expression: "x", role: "dimension" }, + * y: { type: "string", expression: "y", role: "dimension" } + * } + */ + acc[column.name] = { + type: column.type, + expression: column.name, + role: 'dimension', + }; + + return acc; + }, {}), + rows, + }; + }, + }, +}); diff --git a/src/legacy/core_plugins/interpreter/common/types/error.js b/src/legacy/core_plugins/interpreter/common/types/error.js deleted file mode 100644 index 1415a065d810e..0000000000000 --- a/src/legacy/core_plugins/interpreter/common/types/error.js +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export const error = () => ({ - name: 'error', - to: { - render: input => { - const { error, info } = input; - return { - type: 'render', - as: 'error', - value: { - error, - info, - }, - }; - }, - }, -}); diff --git a/src/legacy/core_plugins/interpreter/common/types/error.ts b/src/legacy/core_plugins/interpreter/common/types/error.ts new file mode 100644 index 0000000000000..761c3ec468267 --- /dev/null +++ b/src/legacy/core_plugins/interpreter/common/types/error.ts @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ExpressionType } from '../../types'; +import { Render } from './render'; + +const name = 'error'; + +// TODO: Improve typings on this interface [#38553] +export interface InterpreterErrorType { + type: typeof name; + error: unknown; + info: unknown; +} + +export const error = (): ExpressionType => ({ + name, + to: { + render: (input): Render> => { + return { + type: 'render', + as: name, + value: { + error: input.error, + info: input.info, + }, + }; + }, + }, +}); diff --git a/src/legacy/core_plugins/interpreter/common/types/filter.js b/src/legacy/core_plugins/interpreter/common/types/filter.js deleted file mode 100644 index 484050671b2f9..0000000000000 --- a/src/legacy/core_plugins/interpreter/common/types/filter.js +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export const filter = () => ({ - name: 'filter', - from: { - null: () => { - return { - type: 'filter', - // Any meta data you wish to pass along. - meta: {}, - // And filters. If you need an "or", create a filter type for it. - and: [], - }; - }, - }, -}); diff --git a/src/legacy/core_plugins/interpreter/common/types/filter.ts b/src/legacy/core_plugins/interpreter/common/types/filter.ts new file mode 100644 index 0000000000000..3fd63086cd7a3 --- /dev/null +++ b/src/legacy/core_plugins/interpreter/common/types/filter.ts @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ExpressionType } from '../../types'; + +const name = 'filter'; + +/** + * Represents an object that is a Filter. + */ +export interface Filter { + type?: string; + value?: string; + column?: string; + and: Filter[]; + to?: string; + from?: string; + query?: string | null; +} + +export const filter = (): ExpressionType => ({ + name, + from: { + null: () => { + return { + type: name, + // Any meta data you wish to pass along. + meta: {}, + // And filters. If you need an "or", create a filter type for it. + and: [], + }; + }, + }, +}); diff --git a/src/legacy/core_plugins/interpreter/common/types/image.js b/src/legacy/core_plugins/interpreter/common/types/image.js deleted file mode 100644 index 7666451145f5d..0000000000000 --- a/src/legacy/core_plugins/interpreter/common/types/image.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export const image = () => ({ - name: 'image', - to: { - render: input => { - return { - type: 'render', - as: 'image', - value: input, - }; - }, - }, -}); diff --git a/src/legacy/core_plugins/interpreter/common/types/image.ts b/src/legacy/core_plugins/interpreter/common/types/image.ts new file mode 100644 index 0000000000000..b75ec4b5f237c --- /dev/null +++ b/src/legacy/core_plugins/interpreter/common/types/image.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ExpressionType } from '../../types'; +import { Render } from './render'; + +const name = 'image'; + +export interface ExpressionImage { + type: 'image'; + mode: string; + dataurl: string; +} + +export const image = (): ExpressionType => ({ + name, + to: { + render: (input): Render> => { + return { + type: 'render', + as: 'image', + value: input, + }; + }, + }, +}); diff --git a/src/legacy/core_plugins/interpreter/common/types/index.js b/src/legacy/core_plugins/interpreter/common/types/index.js deleted file mode 100644 index a06feada6ceaa..0000000000000 --- a/src/legacy/core_plugins/interpreter/common/types/index.js +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { boolean } from './boolean'; -import { datatable } from './datatable'; -import { error } from './error'; -import { filter } from './filter'; -import { image } from './image'; -import { nullType } from './null'; -import { number } from './number'; -import { pointseries } from './pointseries'; -import { render } from './render'; -import { shape } from './shape'; -import { string } from './string'; -import { style } from './style'; -import { kibanaContext } from './kibana_context'; -import { kibanaDatatable } from './kibana_datatable'; - -export const typeSpecs = [ - boolean, - datatable, - error, - filter, - image, - number, - nullType, - pointseries, - render, - shape, - string, - style, - kibanaContext, - kibanaDatatable, -]; diff --git a/src/legacy/core_plugins/interpreter/common/types/index.ts b/src/legacy/core_plugins/interpreter/common/types/index.ts new file mode 100644 index 0000000000000..a6ac385804468 --- /dev/null +++ b/src/legacy/core_plugins/interpreter/common/types/index.ts @@ -0,0 +1,61 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { boolean } from './boolean'; +import { datatable } from './datatable'; +import { error } from './error'; +import { filter } from './filter'; +import { image } from './image'; +import { nullType } from './null'; +import { number } from './number'; +import { pointseries } from './pointseries'; +import { render } from './render'; +import { shape } from './shape'; +import { string } from './string'; +import { style } from './style'; +import { kibanaContext } from './kibana_context'; +import { kibanaDatatable } from './kibana_datatable'; + +export const typeSpecs = [ + boolean, + datatable, + error, + filter, + image, + number, + nullType, + pointseries, + render, + shape, + string, + style, + kibanaContext, + kibanaDatatable, +]; + +// Types +export * from './datatable'; +export * from './error'; +export * from './filter'; +export * from './image'; +export * from './kibana_context'; +export * from './kibana_datatable'; +export * from './pointseries'; +export * from './render'; +export * from './style'; diff --git a/src/legacy/core_plugins/interpreter/common/types/kibana_context.js b/src/legacy/core_plugins/interpreter/common/types/kibana_context.js deleted file mode 100644 index b186ae135788d..0000000000000 --- a/src/legacy/core_plugins/interpreter/common/types/kibana_context.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export const kibanaContext = () => ({ - name: 'kibana_context', - from: { - null: () => { - return { - type: 'kibana_context', - }; - }, - }, - to: { - null: () => { - return { - type: 'null', - }; - }, - } -}); diff --git a/src/legacy/core_plugins/interpreter/common/types/kibana_context.ts b/src/legacy/core_plugins/interpreter/common/types/kibana_context.ts new file mode 100644 index 0000000000000..4cd64d92743b0 --- /dev/null +++ b/src/legacy/core_plugins/interpreter/common/types/kibana_context.ts @@ -0,0 +1,47 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Filters, Query, TimeRange } from 'ui/visualize'; + +const name = 'kibana_context'; + +export interface KibanaContext { + type: typeof name; + query?: Query; + filters?: Filters; + timeRange?: TimeRange; +} + +export const kibanaContext = () => ({ + name, + from: { + null: () => { + return { + type: name, + }; + }, + }, + to: { + null: () => { + return { + type: 'null', + }; + }, + }, +}); diff --git a/src/legacy/core_plugins/interpreter/common/types/kibana_datatable.js b/src/legacy/core_plugins/interpreter/common/types/kibana_datatable.js deleted file mode 100644 index a87615099e7ca..0000000000000 --- a/src/legacy/core_plugins/interpreter/common/types/kibana_datatable.js +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { map } from 'lodash'; - -export const kibanaDatatable = () => ({ - name: 'kibana_datatable', - from: { - datatable: context => { - return { - type: 'kibana_datatable', - rows: context.rows, - columns: context.columns.map(column => { - return { - id: column.name, - name: column.name, - }; - }), - }; - }, - pointseries: context => { - const columns = map(context.columns, (column, name) => { - return { id: name, name, ...column }; - }); - return { - type: 'kibana_datatable', - rows: context.rows, - columns: columns, - }; - } - }, -}); diff --git a/src/legacy/core_plugins/interpreter/common/types/kibana_datatable.ts b/src/legacy/core_plugins/interpreter/common/types/kibana_datatable.ts new file mode 100644 index 0000000000000..d5622ff50dd83 --- /dev/null +++ b/src/legacy/core_plugins/interpreter/common/types/kibana_datatable.ts @@ -0,0 +1,67 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { map } from 'lodash'; + +const name = 'kibana_datatable'; + +interface Column { + id: string; + name: string; +} + +interface Row { + [key: string]: unknown; +} + +export interface KibanaDatatable { + type: typeof name; + columns: Column[]; + rows: Row[]; +} + +export const kibanaDatatable = () => ({ + name, + from: { + // TODO: import datatable types here instead of using any + datatable: (context: any) => { + return { + type: name, + rows: context.rows, + columns: context.columns.map((column: any) => { + return { + id: column.name, + name: column.name, + }; + }), + }; + }, + // TODO: import pointseries types here instead of using any + pointseries: (context: any) => { + const columns = map(context.columns, (column, n) => { + return { id: n, name: n, ...column }; + }); + return { + type: name, + rows: context.rows, + columns, + }; + }, + }, +}); diff --git a/src/legacy/core_plugins/interpreter/common/types/null.js b/src/legacy/core_plugins/interpreter/common/types/null.js deleted file mode 100644 index 2789ce330ac6c..0000000000000 --- a/src/legacy/core_plugins/interpreter/common/types/null.js +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export const nullType = () => ({ - name: 'null', - from: { - '*': () => null, - }, -}); diff --git a/src/legacy/core_plugins/interpreter/common/types/null.ts b/src/legacy/core_plugins/interpreter/common/types/null.ts new file mode 100644 index 0000000000000..98f75d688c863 --- /dev/null +++ b/src/legacy/core_plugins/interpreter/common/types/null.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ExpressionType } from '../../types'; + +const name = 'null'; + +export const nullType = (): ExpressionType => ({ + name, + from: { + '*': () => null, + }, +}); diff --git a/src/legacy/core_plugins/interpreter/common/types/number.js b/src/legacy/core_plugins/interpreter/common/types/number.js deleted file mode 100644 index 8f8f31ea8a2fb..0000000000000 --- a/src/legacy/core_plugins/interpreter/common/types/number.js +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export const number = () => ({ - name: 'number', - from: { - null: () => 0, - boolean: b => Number(b), - string: n => Number(n), - }, - to: { - render: value => { - const text = `${value}`; - return { - type: 'render', - as: 'text', - value: { text }, - }; - }, - datatable: value => ({ - type: 'datatable', - columns: [{ name: 'value', type: 'number' }], - rows: [{ value }], - }), - }, -}); diff --git a/src/legacy/core_plugins/interpreter/common/types/number.ts b/src/legacy/core_plugins/interpreter/common/types/number.ts new file mode 100644 index 0000000000000..aa8281a5eeb16 --- /dev/null +++ b/src/legacy/core_plugins/interpreter/common/types/number.ts @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ExpressionType } from '../../types'; +import { Datatable } from './datatable'; +import { Render } from './render'; + +const name = 'number'; + +export const number = (): ExpressionType => ({ + name, + from: { + null: () => 0, + boolean: b => Number(b), + string: n => Number(n), + }, + to: { + render: (value: number): Render<{ text: string }> => { + const text = `${value}`; + return { + type: 'render', + as: 'text', + value: { text }, + }; + }, + datatable: (value): Datatable => ({ + type: 'datatable', + columns: [{ name: 'value', type: 'number' }], + rows: [{ value }], + }), + }, +}); diff --git a/src/legacy/core_plugins/interpreter/common/types/pointseries.js b/src/legacy/core_plugins/interpreter/common/types/pointseries.js deleted file mode 100644 index 15fa849112ff7..0000000000000 --- a/src/legacy/core_plugins/interpreter/common/types/pointseries.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export const pointseries = () => ({ - name: 'pointseries', - from: { - null: () => { - return { - type: 'pointseries', - rows: [], - columns: {}, - }; - }, - }, - to: { - render: (pointseries, types) => { - const datatable = types.datatable.from(pointseries, types); - return { - type: 'render', - as: 'table', - value: { - datatable, - showHeader: true, - }, - }; - }, - }, -}); diff --git a/src/legacy/core_plugins/interpreter/common/types/pointseries.ts b/src/legacy/core_plugins/interpreter/common/types/pointseries.ts new file mode 100644 index 0000000000000..bedbd596c5c17 --- /dev/null +++ b/src/legacy/core_plugins/interpreter/common/types/pointseries.ts @@ -0,0 +1,81 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ExpressionType } from '../../types'; +import { Datatable } from './datatable'; +import { Render } from './render'; + +const name = 'pointseries'; + +/** + * Allowed column names in a PointSeries + */ +export type PointSeriesColumnName = 'x' | 'y' | 'color' | 'size' | 'text'; + +/** + * Column in a PointSeries + */ +export interface PointSeriesColumn { + type: 'number' | 'string'; + role: 'measure' | 'dimension'; + expression: string; +} + +/** + * Represents a collection of valid Columns in a PointSeries + */ +export type PointSeriesColumns = Record | {}; + +/** + * A `PointSeries` is a unique structure that represents dots on a chart. + */ +export interface PointSeries { + type: typeof name; + columns: PointSeriesColumns; + rows: Array>; +} + +export const pointseries = (): ExpressionType => ({ + name, + from: { + null: () => { + return { + type: name, + rows: [], + columns: {}, + }; + }, + }, + to: { + render: ( + pseries: PointSeries, + types + ): Render<{ datatable: Datatable; showHeader: boolean }> => { + const datatable: Datatable = types.datatable.from(pseries, types); + return { + type: 'render', + as: 'table', + value: { + datatable, + showHeader: true, + }, + }; + }, + }, +}); diff --git a/src/legacy/core_plugins/interpreter/common/types/render.js b/src/legacy/core_plugins/interpreter/common/types/render.js deleted file mode 100644 index 99ce3ca7d1cd7..0000000000000 --- a/src/legacy/core_plugins/interpreter/common/types/render.js +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export const render = () => ({ - name: 'render', - from: { - '*': v => ({ - type: 'render', - as: 'debug', - value: v, - }), - }, -}); diff --git a/src/legacy/core_plugins/interpreter/common/types/render.ts b/src/legacy/core_plugins/interpreter/common/types/render.ts new file mode 100644 index 0000000000000..80b04381e423e --- /dev/null +++ b/src/legacy/core_plugins/interpreter/common/types/render.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ExpressionType } from '../../types'; + +const name = 'render'; + +/** + * Represents an object that is intended to be rendered. + */ +export interface Render { + type: typeof name; + as: string; + value: T; +} + +export const render = (): ExpressionType> => ({ + name, + from: { + '*': (v: T): Render => ({ + type: name, + as: 'debug', + value: v, + }), + }, +}); diff --git a/src/legacy/core_plugins/interpreter/common/types/shape.js b/src/legacy/core_plugins/interpreter/common/types/shape.js deleted file mode 100644 index 1ed7a111268d1..0000000000000 --- a/src/legacy/core_plugins/interpreter/common/types/shape.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export const shape = () => ({ - name: 'shape', - to: { - render: input => { - return { - type: 'render', - as: 'shape', - value: input, - }; - }, - }, -}); diff --git a/src/legacy/core_plugins/interpreter/common/types/shape.ts b/src/legacy/core_plugins/interpreter/common/types/shape.ts new file mode 100644 index 0000000000000..3f51fb8e7873d --- /dev/null +++ b/src/legacy/core_plugins/interpreter/common/types/shape.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ExpressionType } from '../../types'; +import { Render } from './render'; + +const name = 'shape'; + +export const shape = (): ExpressionType => ({ + name, + to: { + render: (input: T): Render => { + return { + type: 'render', + as: name, + value: input, + }; + }, + }, +}); diff --git a/src/legacy/core_plugins/interpreter/common/types/string.js b/src/legacy/core_plugins/interpreter/common/types/string.js deleted file mode 100644 index 90e6b17cc9dcf..0000000000000 --- a/src/legacy/core_plugins/interpreter/common/types/string.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export const string = () => ({ - name: 'string', - from: { - null: () => '', - boolean: b => String(b), - number: n => String(n), - }, - to: { - render: text => { - return { - type: 'render', - as: 'text', - value: { text }, - }; - }, - datatable: value => ({ - type: 'datatable', - columns: [{ name: 'value', type: 'string' }], - rows: [{ value }], - }), - }, -}); diff --git a/src/legacy/core_plugins/interpreter/common/types/string.ts b/src/legacy/core_plugins/interpreter/common/types/string.ts new file mode 100644 index 0000000000000..63be3d989caae --- /dev/null +++ b/src/legacy/core_plugins/interpreter/common/types/string.ts @@ -0,0 +1,47 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ExpressionType } from '../../types'; +import { Datatable } from './datatable'; +import { Render } from './render'; + +const name = 'string'; + +export const string = (): ExpressionType => ({ + name, + from: { + null: () => '', + boolean: b => String(b), + number: n => String(n), + }, + to: { + render: (text: T): Render<{ text: T }> => { + return { + type: 'render', + as: 'text', + value: { text }, + }; + }, + datatable: (value): Datatable => ({ + type: 'datatable', + columns: [{ name: 'value', type: 'string' }], + rows: [{ value }], + }), + }, +}); diff --git a/src/legacy/core_plugins/interpreter/common/types/style.js b/src/legacy/core_plugins/interpreter/common/types/style.js deleted file mode 100644 index 97057b415a475..0000000000000 --- a/src/legacy/core_plugins/interpreter/common/types/style.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export const style = () => ({ - name: 'style', - from: { - null: () => { - return { - type: 'style', - spec: {}, - css: '', - }; - }, - }, -}); diff --git a/src/legacy/core_plugins/interpreter/common/types/style.ts b/src/legacy/core_plugins/interpreter/common/types/style.ts new file mode 100644 index 0000000000000..b74dd658e9dce --- /dev/null +++ b/src/legacy/core_plugins/interpreter/common/types/style.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ExpressionType } from '../../types'; + +const name = 'style'; + +export interface Style { + type: typeof name; + spec: any; + css: string; +} + +export const style = (): ExpressionType => ({ + name, + from: { + null: () => { + return { + type: 'style', + spec: {}, + css: '', + }; + }, + }, +}); diff --git a/src/legacy/core_plugins/interpreter/public/functions/esaggs.js b/src/legacy/core_plugins/interpreter/public/functions/esaggs.js deleted file mode 100644 index 12f2a54223f88..0000000000000 --- a/src/legacy/core_plugins/interpreter/public/functions/esaggs.js +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { get } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { CourierRequestHandlerProvider } from 'ui/vis/request_handlers/courier'; -import { AggConfigs } from 'ui/vis/agg_configs'; - -// need to get rid of angular from these -import { IndexPatternsProvider } from 'ui/index_patterns'; -import { SearchSourceProvider } from 'ui/courier/search_source'; -import { FilterBarQueryFilterProvider } from 'ui/filter_bar/query_filter'; - -import chrome from 'ui/chrome'; - -const courierRequestHandlerProvider = CourierRequestHandlerProvider; -const courierRequestHandler = courierRequestHandlerProvider().handler; - -export const esaggs = () => ({ - name: 'esaggs', - type: 'kibana_datatable', - context: { - types: [ - 'kibana_context', - 'null', - ], - }, - help: i18n.translate('interpreter.functions.esaggs.help', { defaultMessage: 'Run AggConfig aggregation' }), - args: { - index: { - types: ['string', 'null'], - default: null, - }, - metricsAtAllLevels: { - types: ['boolean'], - default: false, - }, - partialRows: { - types: ['boolean'], - default: false, - }, - aggConfigs: { - types: ['string'], - default: '""', - }, - }, - async fn(context, args, handlers) { - const $injector = await chrome.dangerouslyGetActiveInjector(); - const Private = $injector.get('Private'); - const indexPatterns = Private(IndexPatternsProvider); - const SearchSource = Private(SearchSourceProvider); - const queryFilter = Private(FilterBarQueryFilterProvider); - - const aggConfigsState = JSON.parse(args.aggConfigs); - const indexPattern = await indexPatterns.get(args.index); - const aggs = new AggConfigs(indexPattern, aggConfigsState); - - // we should move searchSource creation inside courier request handler - const searchSource = new SearchSource(); - searchSource.setField('index', indexPattern); - searchSource.setField('size', 0); - - const response = await courierRequestHandler({ - searchSource: searchSource, - aggs: aggs, - timeRange: get(context, 'timeRange', null), - query: get(context, 'query', null), - filters: get(context, 'filters', null), - forceFetch: true, - metricsAtAllLevels: args.metricsAtAllLevels, - partialRows: args.partialRows, - inspectorAdapters: handlers.inspectorAdapters, - queryFilter, - }); - - return { - type: 'kibana_datatable', - rows: response.rows, - columns: response.columns.map(column => ({ - id: column.id, - name: column.name, - })), - }; - }, -}); diff --git a/src/legacy/core_plugins/interpreter/public/functions/esaggs.ts b/src/legacy/core_plugins/interpreter/public/functions/esaggs.ts new file mode 100644 index 0000000000000..c64464030f02b --- /dev/null +++ b/src/legacy/core_plugins/interpreter/public/functions/esaggs.ts @@ -0,0 +1,124 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { get } from 'lodash'; +import { i18n } from '@kbn/i18n'; +// @ts-ignore +import { CourierRequestHandlerProvider } from 'ui/vis/request_handlers/courier'; +// @ts-ignore +import { AggConfigs } from 'ui/vis/agg_configs.js'; + +// need to get rid of angular from these +// @ts-ignore +import { IndexPatternsProvider } from 'ui/index_patterns'; +// @ts-ignore +import { SearchSourceProvider } from 'ui/courier/search_source'; +import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter'; + +import chrome from 'ui/chrome'; + +const courierRequestHandlerProvider = CourierRequestHandlerProvider; +const courierRequestHandler = courierRequestHandlerProvider().handler; + +import { ExpressionFunction } from '../../types'; +import { KibanaContext, KibanaDatatable } from '../../common/types'; + +const name = 'esaggs'; + +type Context = KibanaContext | null; + +interface Arguments { + index: string | null; + metricsAtAllLevels: boolean; + partialRows: boolean; + aggConfigs: string; +} + +type Return = Promise; + +export const esaggs = (): ExpressionFunction => ({ + name, + type: 'kibana_datatable', + context: { + types: ['kibana_context', 'null'], + }, + help: i18n.translate('interpreter.functions.esaggs.help', { + defaultMessage: 'Run AggConfig aggregation', + }), + args: { + index: { + types: ['string', 'null'], + default: null, + help: '', + }, + metricsAtAllLevels: { + types: ['boolean'], + default: false, + help: '', + }, + partialRows: { + types: ['boolean'], + default: false, + help: '', + }, + aggConfigs: { + types: ['string'], + default: '""', + help: '', + }, + }, + async fn(context, args, handlers) { + const $injector = await chrome.dangerouslyGetActiveInjector(); + const Private: Function = $injector.get('Private'); + const indexPatterns = Private(IndexPatternsProvider); + const SearchSource = Private(SearchSourceProvider); + const queryFilter = Private(FilterBarQueryFilterProvider); + + const aggConfigsState = JSON.parse(args.aggConfigs); + const indexPattern = await indexPatterns.get(args.index); + const aggs = new AggConfigs(indexPattern, aggConfigsState); + + // we should move searchSource creation inside courier request handler + const searchSource = new SearchSource(); + searchSource.setField('index', indexPattern); + searchSource.setField('size', 0); + + const response: Pick = await courierRequestHandler({ + searchSource, + aggs, + timeRange: get(context, 'timeRange', null), + query: get(context, 'query', null), + filters: get(context, 'filters', null), + forceFetch: true, + metricsAtAllLevels: args.metricsAtAllLevels, + partialRows: args.partialRows, + inspectorAdapters: handlers.inspectorAdapters, + queryFilter, + }); + + return { + type: 'kibana_datatable', + rows: response.rows, + columns: response.columns.map(column => ({ + id: column.id, + name: column.name, + })), + }; + }, +}); diff --git a/src/legacy/core_plugins/interpreter/public/functions/visualization.js b/src/legacy/core_plugins/interpreter/public/functions/visualization.js index 14831f7ff6e9b..614712fda5f4c 100644 --- a/src/legacy/core_plugins/interpreter/public/functions/visualization.js +++ b/src/legacy/core_plugins/interpreter/public/functions/visualization.js @@ -24,7 +24,7 @@ import { VisRequestHandlersRegistryProvider as RequestHandlersProvider } from 'u import { VisResponseHandlersRegistryProvider as ResponseHandlerProvider } from 'ui/registry/vis_response_handlers'; import { VisTypesRegistryProvider } from 'ui/registry/vis_types'; import { IndexPatternsProvider } from 'ui/index_patterns'; -import { FilterBarQueryFilterProvider } from 'ui/filter_bar/query_filter'; +import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter'; import { PersistedState } from 'ui/persisted_state'; function getHandler(from, type) { diff --git a/src/legacy/core_plugins/interpreter/public/index.ts b/src/legacy/core_plugins/interpreter/public/index.ts new file mode 100644 index 0000000000000..5b715b5843cd9 --- /dev/null +++ b/src/legacy/core_plugins/interpreter/public/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from '../types'; diff --git a/src/legacy/core_plugins/interpreter/types/arguments.ts b/src/legacy/core_plugins/interpreter/types/arguments.ts new file mode 100644 index 0000000000000..20bec9359a593 --- /dev/null +++ b/src/legacy/core_plugins/interpreter/types/arguments.ts @@ -0,0 +1,142 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { KnownTypeToString, TypeString, UnmappedTypeStrings } from './common'; + +/** + * This type represents all of the possible combinations of properties of an + * Argument in an Expression Function. The presence or absence of certain fields + * influence the shape and presence of others within each `arg` in the specification. + */ +export type ArgumentType = + | SingleArgumentType + | MultipleArgumentType + | UnresolvedSingleArgumentType + | UnresolvedMultipleArgumentType; + +/** + * Map the type within the the generic array to a string-based + * representation of the type. + */ +// prettier-ignore +type ArrayTypeToArgumentString = + T extends Array ? TypeString : + T extends null ? 'null' : + never; + +/** + * Map the return type of the function within the generic to a + * string-based representation of the return type. + */ +// prettier-ignore +type UnresolvedTypeToArgumentString = + T extends (...args: any) => infer ElementType ? TypeString : + T extends null ? 'null' : + never; + +/** + * Map the array-based return type of the function within the generic to a + * string-based representation of the return type. + */ +// prettier-ignore +type UnresolvedArrayTypeToArgumentString = + T extends Array<(...args: any) => infer ElementType> ? TypeString : + T extends (...args: any) => infer ElementType ? ArrayTypeToArgumentString : + T extends null ? 'null' : + never; + +/** A type containing properties common to all Function Arguments. */ +interface BaseArgumentType { + /** Alternate names for the Function valid for use in the Expression Editor */ + aliases?: string[]; + /** Help text for the Argument to be displayed in the Expression Editor */ + help: string; + /** Default options for the Argument */ + options?: T[]; + /** + * Is this Argument required? + * @default false + */ + required?: boolean; + /** + * If false, the Argument is supplied as a function to be invoked in the + * implementation, rather than a value. + * @default true + */ + resolve?: boolean; + /** Names of types that are valid values of the Argument. */ + types?: string[]; + /** The optional default value of the Argument. */ + default?: T | string; + /** + * If true, multiple values may be supplied to the Argument. + * @default false + */ + multi?: boolean; +} + +/** + * The `types` array in a `FunctionSpec` should contain string + * representations of the `ArgumentsSpec` types: + * + * `someArgument: boolean | string` results in `types: ['boolean', 'string']` + */ +type SingleArgumentType = BaseArgumentType & { + multi?: false; + resolve?: true; + types?: Array | UnmappedTypeStrings>; +}; + +/** + * If the `multi` property on the argument is true, the `types` array should + * contain string representations of the `ArgumentsSpec` array types: + * + * `someArgument: boolean[] | string[]` results in: `types: ['boolean', 'string']` + */ +type MultipleArgumentType = BaseArgumentType & { + multi: true; + resolve?: true; + types?: Array | UnmappedTypeStrings>; +}; + +/** + * If the `resolve` property on the arugument is false, the `types` array, if + * present, should contain string representations of the result of the argument + * function: + * + * `someArgument: () => string` results in `types: ['string']` + */ +type UnresolvedSingleArgumentType = BaseArgumentType & { + multi?: false; + resolve: false; + types?: Array | UnmappedTypeStrings>; +}; + +/** + * If the `resolve` property on the arugument is false, the `types` array, if + * present, should contain string representations of the result of the argument + * function: + * + * `someArgument: () => string[]` results in `types: ['string']` + */ +type UnresolvedMultipleArgumentType = BaseArgumentType & { + multi: true; + resolve: false; + types?: Array | UnmappedTypeStrings>; +}; diff --git a/src/legacy/core_plugins/interpreter/types/common.ts b/src/legacy/core_plugins/interpreter/types/common.ts new file mode 100644 index 0000000000000..55a3ed579c686 --- /dev/null +++ b/src/legacy/core_plugins/interpreter/types/common.ts @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * This can convert a type into a known Expression string representation of + * that type. For example, `TypeToString` will resolve to `'datatable'`. + * This allows Expression Functions to continue to specify their type in a + * simple string format. + */ +export type TypeToString = KnownTypeToString | UnmappedTypeStrings; + +/** + * Map the type of the generic to a string-based representation of the type. + * + * If the provided generic is its own type interface, we use the value of + * the `type` key as a string literal type for it. + */ +// prettier-ignore +export type KnownTypeToString = + T extends string ? 'string' : + T extends boolean ? 'boolean' : + T extends number ? 'number' : + T extends null ? 'null' : + T extends { type: string } ? T['type'] : + never; + +/** + * If the type extends a Promise, we still need to return the string representation: + * + * `someArgument: Promise` results in `types: ['boolean', 'string']` + */ +export type TypeString = KnownTypeToString>; + +/** + * Types used in Expressions that don't map to a primitive cleanly: + * + * `date` is typed as a number or string, and represents a date + */ +export type UnmappedTypeStrings = 'date' | 'filter'; + +/** + * Utility type: extracts returned type from a Promise. + */ +export type UnwrapPromise = T extends Promise ? P : T; diff --git a/src/legacy/core_plugins/interpreter/types/functions.ts b/src/legacy/core_plugins/interpreter/types/functions.ts new file mode 100644 index 0000000000000..d640e351c5640 --- /dev/null +++ b/src/legacy/core_plugins/interpreter/types/functions.ts @@ -0,0 +1,47 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ArgumentType } from './arguments'; +import { TypeToString, UnwrapPromise } from './common'; + +/** + * A generic type which represents an Expression Function definition. + */ +export interface ExpressionFunction { + /** Arguments for the Function */ + args: { [key in keyof Arguments]: ArgumentType }; + aliases?: string[]; + context?: { + types: Array>; + }; + /** Help text displayed in the Expression editor */ + help: string; + /** The name of the Function */ + name: Name; + /** The type of the Function */ + type?: TypeToString>; + /** The implementation of the Function */ + fn(context: Context, args: Arguments, handlers: FunctionHandlers): Return; +} + +// TODO: Handlers can be passed to the `fn` property of the Function. At the moment, these Functions +// are not strongly defined. +interface FunctionHandlers { + [key: string]: (...args: any) => any; +} diff --git a/src/legacy/core_plugins/interpreter/types/index.ts b/src/legacy/core_plugins/interpreter/types/index.ts new file mode 100644 index 0000000000000..6383214a0c42c --- /dev/null +++ b/src/legacy/core_plugins/interpreter/types/index.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { ArgumentType } from './arguments'; +export { + TypeToString, + KnownTypeToString, + TypeString, + UnmappedTypeStrings, + UnwrapPromise, +} from './common'; +export { ExpressionFunction } from './functions'; +export { ExpressionType } from './types'; +export * from '../common/types'; diff --git a/src/legacy/core_plugins/interpreter/types/types.ts b/src/legacy/core_plugins/interpreter/types/types.ts new file mode 100644 index 0000000000000..a6c8a564a4484 --- /dev/null +++ b/src/legacy/core_plugins/interpreter/types/types.ts @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * A generic type which represents a custom Expression Type Definition that's + * registered to the Interpreter. + */ +export interface ExpressionType { + name: Name; + validate?: (type: any) => void | Error; + serialize?: (type: Type) => SerializedType; + deserialize?: (type: SerializedType) => Type; + // TODO: Update typings for the `availableTypes` parameter once interfaces for this + // have been added elsewhere in the interpreter. + from?: Record) => Type>; + to?: Record) => unknown>; +} diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/area.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/area.js index 52b74c5d61426..a7ac4b680f327 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/area.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/area.js @@ -18,17 +18,19 @@ */ import { VisFactoryProvider } from 'ui/vis/vis_factory'; +import { i18n } from '@kbn/i18n'; import { Schemas } from 'ui/vis/editors/default/schemas'; import pointSeriesTemplate from './editors/point_series.html'; -export default function PointSeriesVisType(Private, i18n) { +export default function PointSeriesVisType(Private) { const VisFactory = Private(VisFactoryProvider); return VisFactory.createVislibVisualization({ name: 'area', - title: i18n('kbnVislibVisTypes.area.areaTitle', { defaultMessage: 'Area' }), + title: i18n.translate('kbnVislibVisTypes.area.areaTitle', { defaultMessage: 'Area' }), icon: 'visArea', - description: i18n('kbnVislibVisTypes.area.areaDescription', { defaultMessage: 'Emphasize the quantity beneath a line chart' }), + description: i18n.translate( + 'kbnVislibVisTypes.area.areaDescription', { defaultMessage: 'Emphasize the quantity beneath a line chart' }), visConfig: { defaults: { type: 'area', @@ -135,7 +137,7 @@ export default function PointSeriesVisType(Private, i18n) { { group: 'metrics', name: 'metric', - title: i18n('kbnVislibVisTypes.area.metricsTitle', { defaultMessage: 'Y-Axis' }), + title: i18n.translate('kbnVislibVisTypes.area.metricsTitle', { defaultMessage: 'Y-Axis' }), aggFilter: ['!geo_centroid', '!geo_bounds'], min: 1, defaults: [ @@ -145,7 +147,7 @@ export default function PointSeriesVisType(Private, i18n) { { group: 'metrics', name: 'radius', - title: i18n('kbnVislibVisTypes.area.radiusTitle', { defaultMessage: 'Dot Size' }), + title: i18n.translate('kbnVislibVisTypes.area.radiusTitle', { defaultMessage: 'Dot Size' }), min: 0, max: 1, aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality'] @@ -153,7 +155,7 @@ export default function PointSeriesVisType(Private, i18n) { { group: 'buckets', name: 'segment', - title: i18n('kbnVislibVisTypes.area.segmentTitle', { defaultMessage: 'X-Axis' }), + title: i18n.translate('kbnVislibVisTypes.area.segmentTitle', { defaultMessage: 'X-Axis' }), min: 0, max: 1, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] @@ -161,7 +163,7 @@ export default function PointSeriesVisType(Private, i18n) { { group: 'buckets', name: 'group', - title: i18n('kbnVislibVisTypes.area.groupTitle', { defaultMessage: 'Split Series' }), + title: i18n.translate('kbnVislibVisTypes.area.groupTitle', { defaultMessage: 'Split Series' }), min: 0, max: 3, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] @@ -169,7 +171,7 @@ export default function PointSeriesVisType(Private, i18n) { { group: 'buckets', name: 'split', - title: i18n('kbnVislibVisTypes.area.splitTitle', { defaultMessage: 'Split Chart' }), + title: i18n.translate('kbnVislibVisTypes.area.splitTitle', { defaultMessage: 'Split Chart' }), min: 0, max: 1, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/gauge_options.html b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/gauge_options.html index 9d29f390ac358..37be3cc365e33 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/gauge_options.html +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/gauge_options.html @@ -31,16 +31,17 @@
-   - +
diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/gauge_options.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/gauge_options.js index b036741291eb5..2baba439cdd68 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/gauge_options.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/gauge_options.js @@ -18,11 +18,12 @@ */ import { uiModules } from 'ui/modules'; +import { i18n } from '@kbn/i18n'; import gaugeOptionsTemplate from './gauge_options.html'; import _ from 'lodash'; const module = uiModules.get('kibana'); -module.directive('gaugeOptions', function (i18n) { +module.directive('gaugeOptions', function () { return { restrict: 'E', template: gaugeOptionsTemplate, @@ -107,7 +108,7 @@ module.directive('gaugeOptions', function (i18n) { $scope.customColors = true; }); - $scope.requiredText = i18n('kbnVislibVisTypes.controls.gaugeOptions.requiredText', { + $scope.requiredText = i18n.translate('kbnVislibVisTypes.controls.gaugeOptions.requiredText', { defaultMessage: 'Required:' }); diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/heatmap_options.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/heatmap_options.js index 6a4839abb5354..847116f461be6 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/heatmap_options.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/heatmap_options.js @@ -18,11 +18,12 @@ */ import { uiModules } from 'ui/modules'; +import { i18n } from '@kbn/i18n'; import heatmapOptionsTemplate from './heatmap_options.html'; import _ from 'lodash'; const module = uiModules.get('kibana'); -module.directive('heatmapOptions', function (i18n) { +module.directive('heatmapOptions', function () { return { restrict: 'E', template: heatmapOptionsTemplate, @@ -89,7 +90,7 @@ module.directive('heatmapOptions', function (i18n) { $scope.customColors = true; }); - $scope.requiredText = i18n('kbnVislibVisTypes.controls.heatmapOptions.requiredText', { + $scope.requiredText = i18n.translate('kbnVislibVisTypes.controls.heatmapOptions.requiredText', { defaultMessage: 'Required:' }); } diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/gauge.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/gauge.js index 88c788f52f003..d421936d6e105 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/gauge.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/gauge.js @@ -18,18 +18,19 @@ */ import { VisFactoryProvider } from 'ui/vis/vis_factory'; +import { i18n } from '@kbn/i18n'; import { Schemas } from 'ui/vis/editors/default/schemas'; import gaugeTemplate from './editors/gauge.html'; import { vislibColorMaps } from 'ui/vislib/components/color/colormaps'; -export default function GaugeVisType(Private, i18n) { +export default function GaugeVisType(Private) { const VisFactory = Private(VisFactoryProvider); return VisFactory.createVislibVisualization({ name: 'gauge', - title: i18n('kbnVislibVisTypes.gauge.gaugeTitle', { defaultMessage: 'Gauge' }), + title: i18n.translate('kbnVislibVisTypes.gauge.gaugeTitle', { defaultMessage: 'Gauge' }), icon: 'visGauge', - description: i18n('kbnVislibVisTypes.gauge.gaugeDescription', { + description: i18n.translate('kbnVislibVisTypes.gauge.gaugeDescription', { defaultMessage: 'Gauges indicate the status of a metric. Use it to show how a metric\'s value relates to reference threshold values.' }), visConfig: { @@ -39,7 +40,7 @@ export default function GaugeVisType(Private, i18n) { addLegend: true, isDisplayWarning: false, gauge: { - verticalSplit: false, + alignment: 'automatic', extendRange: true, percentageMode: false, gaugeType: 'Arc', @@ -85,6 +86,19 @@ export default function GaugeVisType(Private, i18n) { collections: { gaugeTypes: ['Arc', 'Circle'], gaugeColorMode: ['None', 'Labels', 'Background'], + alignments: [ + { + id: 'automatic', + label: i18n.translate('kbnVislibVisTypes.gauge.alignmentAutomaticTitle', { defaultMessage: 'Automatic' }) + }, + { + id: 'horizontal', + label: i18n.translate('kbnVislibVisTypes.gauge.alignmentHorizontalTitle', { defaultMessage: 'Horizontal' }) + }, + { + id: 'vertical', + label: i18n.translate('kbnVislibVisTypes.gauge.alignmentVerticalTitle', { defaultMessage: 'Vertical' }) }, + ], scales: ['linear', 'log', 'square root'], colorSchemas: Object.values(vislibColorMaps).map(value => ({ id: value.id, label: value.label })), }, @@ -93,7 +107,7 @@ export default function GaugeVisType(Private, i18n) { { group: 'metrics', name: 'metric', - title: i18n('kbnVislibVisTypes.gauge.metricTitle', { defaultMessage: 'Metric' }), + title: i18n.translate('kbnVislibVisTypes.gauge.metricTitle', { defaultMessage: 'Metric' }), min: 1, aggFilter: [ '!std_dev', '!geo_centroid', '!percentiles', '!percentile_ranks', @@ -105,7 +119,7 @@ export default function GaugeVisType(Private, i18n) { { group: 'buckets', name: 'group', - title: i18n('kbnVislibVisTypes.gauge.groupTitle', { defaultMessage: 'Split Group' }), + title: i18n.translate('kbnVislibVisTypes.gauge.groupTitle', { defaultMessage: 'Split Group' }), min: 0, max: 1, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/goal.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/goal.js index 5ebbf4ee5a50e..af783326b2e84 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/goal.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/goal.js @@ -18,18 +18,19 @@ */ import { VisFactoryProvider } from 'ui/vis/vis_factory'; +import { i18n } from '@kbn/i18n'; import { Schemas } from 'ui/vis/editors/default/schemas'; import gaugeTemplate from './editors/gauge.html'; import { vislibColorMaps } from 'ui/vislib/components/color/colormaps'; -export default function GoalVisType(Private, i18n) { +export default function GoalVisType(Private) { const VisFactory = Private(VisFactoryProvider); return VisFactory.createVislibVisualization({ name: 'goal', - title: i18n('kbnVislibVisTypes.goal.goalTitle', { defaultMessage: 'Goal' }), + title: i18n.translate('kbnVislibVisTypes.goal.goalTitle', { defaultMessage: 'Goal' }), icon: 'visGoal', - description: i18n('kbnVislibVisTypes.goal.goalDescription', { + description: i18n.translate('kbnVislibVisTypes.goal.goalDescription', { defaultMessage: 'A goal chart indicates how close you are to your final goal.' }), visConfig: { @@ -89,7 +90,7 @@ export default function GoalVisType(Private, i18n) { { group: 'metrics', name: 'metric', - title: i18n('kbnVislibVisTypes.goal.metricTitle', { defaultMessage: 'Metric' }), + title: i18n.translate('kbnVislibVisTypes.goal.metricTitle', { defaultMessage: 'Metric' }), min: 1, aggFilter: [ '!std_dev', '!geo_centroid', '!percentiles', '!percentile_ranks', @@ -101,7 +102,7 @@ export default function GoalVisType(Private, i18n) { { group: 'buckets', name: 'group', - title: i18n('kbnVislibVisTypes.goal.groupTitle', { defaultMessage: 'Split Group' }), + title: i18n.translate('kbnVislibVisTypes.goal.groupTitle', { defaultMessage: 'Split Group' }), min: 0, max: 1, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/heatmap.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/heatmap.js index fb9f2d01a9eed..5bae66e44c934 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/heatmap.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/heatmap.js @@ -18,18 +18,19 @@ */ import { VisFactoryProvider } from 'ui/vis/vis_factory'; +import { i18n } from '@kbn/i18n'; import { Schemas } from 'ui/vis/editors/default/schemas'; import heatmapTemplate from './editors/heatmap.html'; import { vislibColorMaps } from 'ui/vislib/components/color/colormaps'; -export default function HeatmapVisType(Private, i18n) { +export default function HeatmapVisType(Private) { const VisFactory = Private(VisFactoryProvider); return VisFactory.createVislibVisualization({ name: 'heatmap', - title: i18n('kbnVislibVisTypes.heatmap.heatmapTitle', { defaultMessage: 'Heat Map' }), + title: i18n.translate('kbnVislibVisTypes.heatmap.heatmapTitle', { defaultMessage: 'Heat Map' }), icon: 'visHeatmap', - description: i18n('kbnVislibVisTypes.heatmap.heatmapDescription', { defaultMessage: 'Shade cells within a matrix' }), + description: i18n.translate('kbnVislibVisTypes.heatmap.heatmapDescription', { defaultMessage: 'Shade cells within a matrix' }), visConfig: { defaults: { type: 'heatmap', @@ -84,7 +85,7 @@ export default function HeatmapVisType(Private, i18n) { { group: 'metrics', name: 'metric', - title: i18n('kbnVislibVisTypes.heatmap.metricTitle', { defaultMessage: 'Value' }), + title: i18n.translate('kbnVislibVisTypes.heatmap.metricTitle', { defaultMessage: 'Value' }), min: 1, max: 1, aggFilter: ['count', 'avg', 'median', 'sum', 'min', 'max', 'cardinality', 'std_dev', 'top_hits'], @@ -95,7 +96,7 @@ export default function HeatmapVisType(Private, i18n) { { group: 'buckets', name: 'segment', - title: i18n('kbnVislibVisTypes.heatmap.segmentTitle', { defaultMessage: 'X-Axis' }), + title: i18n.translate('kbnVislibVisTypes.heatmap.segmentTitle', { defaultMessage: 'X-Axis' }), min: 0, max: 1, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] @@ -103,7 +104,7 @@ export default function HeatmapVisType(Private, i18n) { { group: 'buckets', name: 'group', - title: i18n('kbnVislibVisTypes.heatmap.groupTitle', { defaultMessage: 'Y-Axis' }), + title: i18n.translate('kbnVislibVisTypes.heatmap.groupTitle', { defaultMessage: 'Y-Axis' }), min: 0, max: 1, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] @@ -111,7 +112,7 @@ export default function HeatmapVisType(Private, i18n) { { group: 'buckets', name: 'split', - title: i18n('kbnVislibVisTypes.heatmap.splitTitle', { defaultMessage: 'Split Chart' }), + title: i18n.translate('kbnVislibVisTypes.heatmap.splitTitle', { defaultMessage: 'Split Chart' }), min: 0, max: 1, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/histogram.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/histogram.js index aee08d0ff23b7..1c2e216a6656c 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/histogram.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/histogram.js @@ -18,17 +18,18 @@ */ import { VisFactoryProvider } from 'ui/vis/vis_factory'; +import { i18n } from '@kbn/i18n'; import { Schemas } from 'ui/vis/editors/default/schemas'; import pointSeriesTemplate from './editors/point_series.html'; -export default function PointSeriesVisType(Private, i18n) { +export default function PointSeriesVisType(Private) { const VisFactory = Private(VisFactoryProvider); return VisFactory.createVislibVisualization({ name: 'histogram', - title: i18n('kbnVislibVisTypes.histogram.histogramTitle', { defaultMessage: 'Vertical Bar' }), + title: i18n.translate('kbnVislibVisTypes.histogram.histogramTitle', { defaultMessage: 'Vertical Bar' }), icon: 'visBarVertical', - description: i18n('kbnVislibVisTypes.histogram.histogramDescription', + description: i18n.translate('kbnVislibVisTypes.histogram.histogramDescription', { defaultMessage: 'Assign a continuous variable to each axis' } ), visConfig: { @@ -138,7 +139,7 @@ export default function PointSeriesVisType(Private, i18n) { { group: 'metrics', name: 'metric', - title: i18n('kbnVislibVisTypes.histogram.metricTitle', { defaultMessage: 'Y-Axis' }), + title: i18n.translate('kbnVislibVisTypes.histogram.metricTitle', { defaultMessage: 'Y-Axis' }), min: 1, aggFilter: ['!geo_centroid', '!geo_bounds'], defaults: [ @@ -148,7 +149,7 @@ export default function PointSeriesVisType(Private, i18n) { { group: 'metrics', name: 'radius', - title: i18n('kbnVislibVisTypes.histogram.radiusTitle', { defaultMessage: 'Dot Size' }), + title: i18n.translate('kbnVislibVisTypes.histogram.radiusTitle', { defaultMessage: 'Dot Size' }), min: 0, max: 1, aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality'] @@ -156,7 +157,7 @@ export default function PointSeriesVisType(Private, i18n) { { group: 'buckets', name: 'segment', - title: i18n('kbnVislibVisTypes.histogram.segmentTitle', { defaultMessage: 'X-Axis' }), + title: i18n.translate('kbnVislibVisTypes.histogram.segmentTitle', { defaultMessage: 'X-Axis' }), min: 0, max: 1, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] @@ -164,7 +165,7 @@ export default function PointSeriesVisType(Private, i18n) { { group: 'buckets', name: 'group', - title: i18n('kbnVislibVisTypes.histogram.groupTitle', { defaultMessage: 'Split Series' }), + title: i18n.translate('kbnVislibVisTypes.histogram.groupTitle', { defaultMessage: 'Split Series' }), min: 0, max: 3, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] @@ -172,7 +173,7 @@ export default function PointSeriesVisType(Private, i18n) { { group: 'buckets', name: 'split', - title: i18n('kbnVislibVisTypes.histogram.splitTitle', { defaultMessage: 'Split Chart' }), + title: i18n.translate('kbnVislibVisTypes.histogram.splitTitle', { defaultMessage: 'Split Chart' }), min: 0, max: 1, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/horizontal_bar.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/horizontal_bar.js index b3a1b3d23373d..6b8540b7ac360 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/horizontal_bar.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/horizontal_bar.js @@ -18,17 +18,18 @@ */ import { VisFactoryProvider } from 'ui/vis/vis_factory'; +import { i18n } from '@kbn/i18n'; import { Schemas } from 'ui/vis/editors/default/schemas'; import pointSeriesTemplate from './editors/point_series.html'; -export default function PointSeriesVisType(Private, i18n) { +export default function PointSeriesVisType(Private) { const VisFactory = Private(VisFactoryProvider); return VisFactory.createVislibVisualization({ name: 'horizontal_bar', - title: i18n('kbnVislibVisTypes.horizontalBar.horizontalBarTitle', { defaultMessage: 'Horizontal Bar' }), + title: i18n.translate('kbnVislibVisTypes.horizontalBar.horizontalBarTitle', { defaultMessage: 'Horizontal Bar' }), icon: 'visBarHorizontal', - description: i18n('kbnVislibVisTypes.horizontalBar.horizontalBarDescription', + description: i18n.translate('kbnVislibVisTypes.horizontalBar.horizontalBarDescription', { defaultMessage: 'Assign a continuous variable to each axis' } ), visConfig: { @@ -140,7 +141,7 @@ export default function PointSeriesVisType(Private, i18n) { { group: 'metrics', name: 'metric', - title: i18n('kbnVislibVisTypes.horizontalBar.metricTitle', { defaultMessage: 'Y-Axis' }), + title: i18n.translate('kbnVislibVisTypes.horizontalBar.metricTitle', { defaultMessage: 'Y-Axis' }), min: 1, aggFilter: ['!geo_centroid', '!geo_bounds'], defaults: [ @@ -150,7 +151,7 @@ export default function PointSeriesVisType(Private, i18n) { { group: 'metrics', name: 'radius', - title: i18n('kbnVislibVisTypes.horizontalBar.radiusTitle', { defaultMessage: 'Dot Size' }), + title: i18n.translate('kbnVislibVisTypes.horizontalBar.radiusTitle', { defaultMessage: 'Dot Size' }), min: 0, max: 1, aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality'] @@ -158,7 +159,7 @@ export default function PointSeriesVisType(Private, i18n) { { group: 'buckets', name: 'segment', - title: i18n('kbnVislibVisTypes.horizontalBar.segmentTitle', { defaultMessage: 'X-Axis' }), + title: i18n.translate('kbnVislibVisTypes.horizontalBar.segmentTitle', { defaultMessage: 'X-Axis' }), min: 0, max: 1, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] @@ -166,7 +167,7 @@ export default function PointSeriesVisType(Private, i18n) { { group: 'buckets', name: 'group', - title: i18n('kbnVislibVisTypes.horizontalBar.groupTitle', { defaultMessage: 'Split Series' }), + title: i18n.translate('kbnVislibVisTypes.horizontalBar.groupTitle', { defaultMessage: 'Split Series' }), min: 0, max: 3, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] @@ -174,7 +175,7 @@ export default function PointSeriesVisType(Private, i18n) { { group: 'buckets', name: 'split', - title: i18n('kbnVislibVisTypes.horizontalBar.splitTitle', { defaultMessage: 'Split Chart' }), + title: i18n.translate('kbnVislibVisTypes.horizontalBar.splitTitle', { defaultMessage: 'Split Chart' }), min: 0, max: 1, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/line.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/line.js index 25cb19f47c6eb..11a598b141241 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/line.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/line.js @@ -18,17 +18,18 @@ */ import { VisFactoryProvider } from 'ui/vis/vis_factory'; +import { i18n } from '@kbn/i18n'; import { Schemas } from 'ui/vis/editors/default/schemas'; import pointSeriesTemplate from './editors/point_series.html'; -export default function PointSeriesVisType(Private, i18n) { +export default function PointSeriesVisType(Private) { const VisFactory = Private(VisFactoryProvider); return VisFactory.createVislibVisualization({ name: 'line', - title: i18n('kbnVislibVisTypes.line.lineTitle', { defaultMessage: 'Line' }), + title: i18n.translate('kbnVislibVisTypes.line.lineTitle', { defaultMessage: 'Line' }), icon: 'visLine', - description: i18n('kbnVislibVisTypes.line.lineDescription', { defaultMessage: 'Emphasize trends' }), + description: i18n.translate('kbnVislibVisTypes.line.lineDescription', { defaultMessage: 'Emphasize trends' }), visConfig: { defaults: { type: 'line', @@ -136,7 +137,7 @@ export default function PointSeriesVisType(Private, i18n) { { group: 'metrics', name: 'metric', - title: i18n('kbnVislibVisTypes.line.metricTitle', { defaultMessage: 'Y-Axis' }), + title: i18n.translate('kbnVislibVisTypes.line.metricTitle', { defaultMessage: 'Y-Axis' }), min: 1, aggFilter: ['!geo_centroid', '!geo_bounds'], defaults: [ @@ -146,7 +147,7 @@ export default function PointSeriesVisType(Private, i18n) { { group: 'metrics', name: 'radius', - title: i18n('kbnVislibVisTypes.line.radiusTitle', { defaultMessage: 'Dot Size' }), + title: i18n.translate('kbnVislibVisTypes.line.radiusTitle', { defaultMessage: 'Dot Size' }), min: 0, max: 1, aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality', 'top_hits'] @@ -154,7 +155,7 @@ export default function PointSeriesVisType(Private, i18n) { { group: 'buckets', name: 'segment', - title: i18n('kbnVislibVisTypes.line.segmentTitle', { defaultMessage: 'X-Axis' }), + title: i18n.translate('kbnVislibVisTypes.line.segmentTitle', { defaultMessage: 'X-Axis' }), min: 0, max: 1, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] @@ -162,7 +163,7 @@ export default function PointSeriesVisType(Private, i18n) { { group: 'buckets', name: 'group', - title: i18n('kbnVislibVisTypes.line.groupTitle', { defaultMessage: 'Split Series' }), + title: i18n.translate('kbnVislibVisTypes.line.groupTitle', { defaultMessage: 'Split Series' }), min: 0, max: 3, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] @@ -170,7 +171,7 @@ export default function PointSeriesVisType(Private, i18n) { { group: 'buckets', name: 'split', - title: i18n('kbnVislibVisTypes.line.splitTitle', { defaultMessage: 'Split Chart' }), + title: i18n.translate('kbnVislibVisTypes.line.splitTitle', { defaultMessage: 'Split Chart' }), min: 0, max: 1, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/pie.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/pie.js index e9a700863dbda..d40002e5b9d6e 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/pie.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/pie.js @@ -18,17 +18,18 @@ */ import { VisFactoryProvider } from 'ui/vis/vis_factory'; +import { i18n } from '@kbn/i18n'; import { Schemas } from 'ui/vis/editors/default/schemas'; import pieTemplate from './editors/pie.html'; -export default function HistogramVisType(Private, i18n) { +export default function HistogramVisType(Private) { const VisFactory = Private(VisFactoryProvider); return VisFactory.createVislibVisualization({ name: 'pie', - title: i18n('kbnVislibVisTypes.pie.pieTitle', { defaultMessage: 'Pie' }), + title: i18n.translate('kbnVislibVisTypes.pie.pieTitle', { defaultMessage: 'Pie' }), icon: 'visPie', - description: i18n('kbnVislibVisTypes.pie.pieDescription', { defaultMessage: 'Compare parts of a whole' }), + description: i18n.translate('kbnVislibVisTypes.pie.pieDescription', { defaultMessage: 'Compare parts of a whole' }), visConfig: { defaults: { type: 'pie', @@ -68,7 +69,7 @@ export default function HistogramVisType(Private, i18n) { { group: 'metrics', name: 'metric', - title: i18n('kbnVislibVisTypes.pie.metricTitle', { defaultMessage: 'Slice Size' }), + title: i18n.translate('kbnVislibVisTypes.pie.metricTitle', { defaultMessage: 'Slice Size' }), min: 1, max: 1, aggFilter: ['sum', 'count', 'cardinality', 'top_hits'], @@ -80,7 +81,7 @@ export default function HistogramVisType(Private, i18n) { group: 'buckets', name: 'segment', icon: 'fa fa-scissors', - title: i18n('kbnVislibVisTypes.pie.segmentTitle', { defaultMessage: 'Split Slices' }), + title: i18n.translate('kbnVislibVisTypes.pie.segmentTitle', { defaultMessage: 'Split Slices' }), min: 0, max: Infinity, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] @@ -89,7 +90,7 @@ export default function HistogramVisType(Private, i18n) { group: 'buckets', name: 'split', icon: 'fa fa-th', - title: i18n('kbnVislibVisTypes.pie.splitTitle', { defaultMessage: 'Split Chart' }), + title: i18n.translate('kbnVislibVisTypes.pie.splitTitle', { defaultMessage: 'Split Chart' }), mustBeFirst: true, min: 0, max: 1, diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/pie_fn.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/pie_fn.js index b47a9c4b7c569..4b67179da4bbd 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/pie_fn.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/pie_fn.js @@ -18,7 +18,7 @@ */ import { functionsRegistry } from 'plugins/interpreter/registries'; -import { VislibSlicesResponseHandlerProvider as vislibSlicesResponseHandler } from 'ui/vis/response_handlers/vislib'; +import { vislibSlicesResponseHandlerProvider as vislibSlicesResponseHandler } from 'ui/vis/response_handlers/vislib'; import { i18n } from '@kbn/i18n'; export const kibanaPie = () => ({ diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/pie_fn.test.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/pie_fn.test.js index 8d9c79961919f..5fc68a42cb81d 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/pie_fn.test.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/pie_fn.test.js @@ -35,7 +35,7 @@ const mockResponseHandler = jest.fn().mockReturnValue(Promise.resolve({ }, })); jest.mock('ui/vis/response_handlers/vislib', () => ({ - VislibSlicesResponseHandlerProvider: () => ({ handler: mockResponseHandler }), + vislibSlicesResponseHandlerProvider: () => ({ handler: mockResponseHandler }), })); describe('interpreter/functions#pie', () => { diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/vislib_fn.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/vislib_fn.js index 4eac35b759e0b..5c1282faea8e5 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/vislib_fn.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/vislib_fn.js @@ -19,8 +19,7 @@ import { functionsRegistry } from 'plugins/interpreter/registries'; import { i18n } from '@kbn/i18n'; -import { VislibSeriesResponseHandlerProvider } from 'ui/vis/response_handlers/vislib'; -import chrome from 'ui/chrome'; +import { vislibSeriesResponseHandlerProvider } from 'ui/vis/response_handlers/vislib'; export const vislib = () => ({ name: 'vislib', @@ -40,9 +39,7 @@ export const vislib = () => ({ }, }, async fn(context, args) { - const $injector = await chrome.dangerouslyGetActiveInjector(); - const Private = $injector.get('Private'); - const responseHandler = Private(VislibSeriesResponseHandlerProvider).handler; + const responseHandler = vislibSeriesResponseHandlerProvider().handler; const visConfigParams = JSON.parse(args.visConfig); const convertedData = await responseHandler(context, visConfigParams.dimensions); diff --git a/src/legacy/core_plugins/kibana/common/field_formats/types/__tests__/date_nanos.js b/src/legacy/core_plugins/kibana/common/field_formats/types/__tests__/date_nanos.js new file mode 100644 index 0000000000000..9bc75bf9f6c87 --- /dev/null +++ b/src/legacy/core_plugins/kibana/common/field_formats/types/__tests__/date_nanos.js @@ -0,0 +1,102 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import moment from 'moment-timezone'; +import { createDateNanosFormat, analysePatternForFract, formatWithNanos } from '../date_nanos'; +import { FieldFormat } from '../../../../../../ui/field_formats/field_format'; + +const DateFormat = createDateNanosFormat(FieldFormat); + +describe('Date Nanos Format', function () { + let convert; + let mockConfig; + + beforeEach(function () { + mockConfig = {}; + mockConfig.dateNanosFormat = 'MMMM Do YYYY, HH:mm:ss.SSSSSSSSS'; + mockConfig['dateFormat:tz'] = 'Browser'; + const getConfig = (key) => mockConfig[key]; + const date = new DateFormat({}, getConfig); + + convert = date.convert.bind(date); + }); + + + it('should inject fractional seconds into formatted timestamp', function () { + [{ + input: '2019-05-20T14:04:56.357001234Z', + pattern: 'MMM D, YYYY @ HH:mm:ss.SSSSSSSSS', + expected: 'May 20, 2019 @ 14:04:56.357001234', + }, { + input: '2019-05-05T14:04:56.357111234Z', + pattern: 'MMM D, YYYY @ HH:mm:ss.SSSSSSSSS', + expected: 'May 5, 2019 @ 14:04:56.357111234', + }, { + input: '2019-05-05T14:04:56.357Z', + pattern: 'MMM D, YYYY @ HH:mm:ss.SSSSSSSSS', + expected: 'May 5, 2019 @ 14:04:56.357000000', + }, { + input: '2019-05-05T14:04:56Z', + pattern: 'MMM D, YYYY @ HH:mm:ss.SSSSSSSSS', + expected: 'May 5, 2019 @ 14:04:56.000000000', + }, { + input: '2019-05-05T14:04:56.201900001Z', + pattern: 'MMM D, YYYY @ HH:mm:ss SSSS', + expected: 'May 5, 2019 @ 14:04:56 2019', + }, { + input: '2019-05-05T14:04:56.201900001Z', + pattern: 'SSSSSSSSS', + expected: '201900001', + }].forEach(fixture => { + const fracPattern = analysePatternForFract(fixture.pattern); + const momentDate = moment(fixture.input).utc(); + const value = formatWithNanos(momentDate, fixture.input, fracPattern); + expect(value).to.be(fixture.expected); + }); + }); + + it('decoding an undefined or null date should return an empty string', function () { + expect(convert(null)).to.be('-'); + expect(convert(undefined)).to.be('-'); + }); + + it('should clear the memoization cache after changing the date', function () { + function setDefaultTimezone() { + moment.tz.setDefault(mockConfig['dateFormat:tz']); + } + + const dateTime = '2019-05-05T14:04:56.201900001Z'; + + mockConfig['dateFormat:tz'] = 'America/Chicago'; + setDefaultTimezone(); + const chicagoTime = convert(dateTime); + + mockConfig['dateFormat:tz'] = 'America/Phoenix'; + setDefaultTimezone(); + const phoenixTime = convert(dateTime); + + expect(chicagoTime).not.to.equal(phoenixTime); + }); + + it('should return the value itself when it cannot successfully be formatted', function () { + const dateMath = 'now+1M/d'; + expect(convert(dateMath)).to.be(dateMath); + }); +}); diff --git a/src/legacy/core_plugins/kibana/common/field_formats/types/date_nanos.js b/src/legacy/core_plugins/kibana/common/field_formats/types/date_nanos.js new file mode 100644 index 0000000000000..b5b1a798f6249 --- /dev/null +++ b/src/legacy/core_plugins/kibana/common/field_formats/types/date_nanos.js @@ -0,0 +1,115 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import moment from 'moment'; +import _ from 'lodash'; + +/** + * Analyse the given moment.js format pattern for the fractional sec part (S,SS,SSS...) + * returning length, match, pattern and an escaped pattern, that excludes the fractional + * part when formatting with moment.js -> e.g. [SSS] + */ +export function analysePatternForFract(pattern) { + + const fracSecMatch = pattern.match('S+'); //extract fractional seconds sub-pattern + return { + length: fracSecMatch[0] ? fracSecMatch[0].length : 0, + patternNanos: fracSecMatch[0], + pattern, + patternEscaped: fracSecMatch[0] ? pattern.replace(fracSecMatch[0], `[${fracSecMatch[0]}]`) : '', + }; +} + +/** + * Format a given moment.js date object + * Since momentjs would loose the exact value for fractional seconds with a higher resolution than + * milliseconds, the fractional pattern is replaced by the fractional value of the raw timestamp + */ +export function formatWithNanos(dateMomentObj, valRaw, fracPatternObj) { + + if (fracPatternObj.length <= 3) { + //S,SS,SSS is formatted correctly by moment.js + return dateMomentObj.format(fracPatternObj.pattern); + + } else { + //Beyond SSS the precise value of the raw datetime string is used + const valFormatted = dateMomentObj.format(fracPatternObj.patternEscaped); + //Extract fractional value of ES formatted timestamp, zero pad if necessary: + //2020-05-18T20:45:05.957Z -> 957000000 + //2020-05-18T20:45:05.957000123Z -> 957000123 + //we do not need to take care of the year 10000 bug since max year of date_nanos is 2262 + const valNanos = valRaw + .substr(20, valRaw.length - 21) //remove timezone(Z) + .padEnd(9, '0') //pad shorter fractionals + .substr(0, fracPatternObj.patternNanos.length); + return valFormatted.replace(fracPatternObj.patternNanos, valNanos); + } +} + +export function createDateNanosFormat(FieldFormat) { + return class DateNanosFormat extends FieldFormat { + constructor(params, getConfig) { + super(params); + + this.getConfig = getConfig; + } + + getParamDefaults() { + return { + pattern: this.getConfig('dateNanosFormat'), + timezone: this.getConfig('dateFormat:tz'), + }; + } + + _convert(val) { + // don't give away our ref to converter so + // we can hot-swap when config changes + const pattern = this.param('pattern'); + const timezone = this.param('timezone'); + const fractPattern = analysePatternForFract(pattern); + + const timezoneChanged = this._timeZone !== timezone; + const datePatternChanged = this._memoizedPattern !== pattern; + if (timezoneChanged || datePatternChanged) { + this._timeZone = timezone; + this._memoizedPattern = pattern; + + this._memoizedConverter = _.memoize(function converter(val) { + if (val === null || val === undefined) { + return '-'; + } + + const date = moment(val); + + if (date.isValid()) { + return formatWithNanos(date, val, fractPattern); + } else { + return val; + } + }); + } + + return this._memoizedConverter(val); + } + + static id = 'date_nanos'; + static title = 'Date Nanos'; + static fieldType = 'date'; + }; +} diff --git a/src/legacy/core_plugins/kibana/common/tutorials/instruction_variant.js b/src/legacy/core_plugins/kibana/common/tutorials/instruction_variant.js index 684915ac37fa2..0c3be21044241 100644 --- a/src/legacy/core_plugins/kibana/common/tutorials/instruction_variant.js +++ b/src/legacy/core_plugins/kibana/common/tutorials/instruction_variant.js @@ -32,6 +32,7 @@ export const INSTRUCTION_VARIANT = { JS: 'js', GO: 'go', JAVA: 'java', + DOTNET: 'dotnet', LINUX: 'linux', }; @@ -50,6 +51,7 @@ const DISPLAY_MAP = { [INSTRUCTION_VARIANT.JS]: 'RUM (JS)', [INSTRUCTION_VARIANT.GO]: 'Go', [INSTRUCTION_VARIANT.JAVA]: 'Java', + [INSTRUCTION_VARIANT.DOTNET]: '.NET', [INSTRUCTION_VARIANT.LINUX]: 'Linux', }; diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index d58ebec0be334..64a8f4eec1199 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -201,7 +201,7 @@ export default function (kibana) { defaultSearchField: 'url', isImportableAndExportable: true, getTitle(obj) { - return obj.attributes.url; + return `/goto/${encodeURIComponent(obj.id)}`; }, }, config: { @@ -240,9 +240,7 @@ export default function (kibana) { migrations, }, - uiCapabilities: async function (server) { - const { savedObjects } = server; - + uiCapabilities: async function () { return { discover: { show: true, @@ -275,14 +273,11 @@ export default function (kibana) { indexPatterns: { save: true, }, - savedObjectsManagement: savedObjects.types.reduce((acc, type) => ({ - ...acc, - [type]: { - delete: true, - edit: true, - read: true, - } - }), {}), + savedObjectsManagement: { + delete: true, + edit: true, + read: true, + }, management: { /* * Management settings correspond to management section/link ids, and should not be changed diff --git a/src/legacy/core_plugins/kibana/migrations.js b/src/legacy/core_plugins/kibana/migrations.js index 2ef85b9165b94..4cb5055e24202 100644 --- a/src/legacy/core_plugins/kibana/migrations.js +++ b/src/legacy/core_plugins/kibana/migrations.js @@ -134,8 +134,6 @@ const migrateDateHistogramAggregation = doc => { return doc; }; -const executeMigrations710 = flow(migratePercentileRankAggregation, migrateDateHistogramAggregation); - function removeDateHistogramTimeZones(doc) { const visStateJSON = get(doc, 'attributes.visState'); if (visStateJSON) { @@ -163,6 +161,158 @@ function removeDateHistogramTimeZones(doc) { return doc; } +// migrate gauge verticalSplit to alignment +// https://github.com/elastic/kibana/issues/34636 +function migrateGaugeVerticalSplitToAlignment(doc) { + const visStateJSON = get(doc, 'attributes.visState'); + + if (visStateJSON) { + try { + const visState = JSON.parse(visStateJSON); + if (visState && visState.type === 'gauge') { + + visState.params.gauge.alignment = visState.params.gauge.verticalSplit ? 'vertical' : 'horizontal'; + delete visState.params.gauge.verticalSplit; + return { + ...doc, + attributes: { + ...doc.attributes, + visState: JSON.stringify(visState), + }, + }; + } + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + } + } + return doc; +} +// Migrate filters (string -> { query: string, language: lucene }) +/* + Enabling KQL in TSVB causes problems with savedObject visualizations when these are saved with filters. + In a visualisation type of saved object, if the visState param is of type metric, the filter is saved as a string that is not interpretted correctly as a lucene query in the visualization itself. + We need to transform the filter string into an object containing the original string as a query and specify the query language as lucene. + For Metrics visualizations (param.type === "metric"), filters can be applied to each series object in the series array within the SavedObject.visState.params object. + Path to the series array is thus: + attributes.visState. +*/ +function transformFilterStringToQueryObject(doc) { + // Migrate filters + // If any filters exist and they are a string, we assume it to be lucene and transform the filter into an object accordingly + const newDoc = cloneDeep(doc); + const visStateJSON = get(doc, 'attributes.visState'); + if (visStateJSON) { + let visState; + try { + visState = JSON.parse(visStateJSON); + } catch (e) { + // let it go, the data is invalid and we'll leave it as is + } + if (visState) { + const visType = get(visState, 'params.type'); + const tsvbTypes = ['metric', 'markdown', 'top_n', 'gauge', 'table', 'timeseries']; + if (tsvbTypes.indexOf(visType) === -1) { + // skip + return doc; + } + // migrate the params fitler + const params = get(visState, 'params'); + if (params.filter && typeof params.filter === 'string') { + const paramsFilterObject = { + query: params.filter, + language: 'lucene', + }; + params.filter = paramsFilterObject; + } + + // migrate the annotations query string: + const annotations = get(visState, 'params.annotations') || []; + annotations.forEach((item) => { + if (!item.query_string) { + // we don't need to transform anything if there isn't a filter at all + return; + } + if (typeof item.query_string === 'string') { + const itemQueryStringObject = { + query: item.query_string, + language: 'lucene', + }; + item.query_string = itemQueryStringObject; + } + }); + // migrate the series filters + const series = get(visState, 'params.series') || []; + series.forEach((item) => { + if (!item.filter) { + // we don't need to transform anything if there isn't a filter at all + return; + } + // series item filter + if (typeof item.filter === 'string') { + const itemfilterObject = { + query: item.filter, + language: 'lucene', + }; + item.filter = itemfilterObject; + } + // series item split filters filter + if (item.split_filters) { + const splitFilters = get(item, 'split_filters') || []; + splitFilters.forEach((filter) => { + if (!filter.filter) { + // we don't need to transform anything if there isn't a filter at all + return; + } + if (typeof filter.filter === 'string') { + const filterfilterObject = { + query: filter.filter, + language: 'lucene', + }; + filter.filter = filterfilterObject; + } + }); + } + }); + newDoc.attributes.visState = JSON.stringify(visState); + } + } + return newDoc; +} + +function migrateFiltersAggQuery(doc) { + const visStateJSON = get(doc, 'attributes.visState'); + + if (visStateJSON) { + try { + const visState = JSON.parse(visStateJSON); + if (visState && visState.aggs) { + visState.aggs.forEach((agg) => { + if (agg.type !== 'filters') return; + + agg.params.filters.forEach((filter) => { + if (filter.input.language) return filter; + filter.input.language = 'lucene'; + }); + }); + + return { + ...doc, + attributes: { + ...doc.attributes, + visState: JSON.stringify(visState), + }, + }; + } + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + } + } + return doc; +} + +const executeMigrations720 = flow(migratePercentileRankAggregation, migrateDateHistogramAggregation); +const executeMigrations730 = flow(migrateGaugeVerticalSplitToAlignment, transformFilterStringToQueryObject, migrateFiltersAggQuery); + export const migrations = { 'index-pattern': { '6.5.0': (doc) => { @@ -264,7 +414,8 @@ export const migrations = { } }, '7.0.1': removeDateHistogramTimeZones, - '7.1.0': doc => executeMigrations710(doc) + '7.2.0': doc => executeMigrations720(doc), + '7.3.0': executeMigrations730, }, dashboard: { '7.0.0': (doc) => { diff --git a/src/legacy/core_plugins/kibana/migrations.test.js b/src/legacy/core_plugins/kibana/migrations.test.js index 5a862460eb8ed..6c6337450c882 100644 --- a/src/legacy/core_plugins/kibana/migrations.test.js +++ b/src/legacy/core_plugins/kibana/migrations.test.js @@ -718,7 +718,7 @@ Object { }); describe('date histogram custom interval removal', () => { - const migrate = doc => migrations.visualization['7.1.0'](doc); + const migrate = doc => migrations.visualization['7.2.0'](doc); let doc; beforeEach(() => { doc = { @@ -843,6 +843,211 @@ Object { expect(aggs[3]).not.toHaveProperty('params.customInterval'); }); }); + describe('7.3.0', () => { + const migrate = doc => migrations.visualization['7.3.0'](doc); + + it('migrates type = gauge verticalSplit: false to alignment: vertical', () => { + const migratedDoc = migrate({ + attributes: { + visState: JSON.stringify({ type: 'gauge', params: { gauge: { verticalSplit: false } } }), + }, + }); + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "visState": "{\\"type\\":\\"gauge\\",\\"params\\":{\\"gauge\\":{\\"alignment\\":\\"horizontal\\"}}}", + }, +} +`); + }); + + it('migrates type = gauge verticalSplit: false to alignment: horizontal', () => { + const migratedDoc = migrate({ + attributes: { + visState: JSON.stringify({ type: 'gauge', params: { gauge: { verticalSplit: true } } }), + }, + }); + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "visState": "{\\"type\\":\\"gauge\\",\\"params\\":{\\"gauge\\":{\\"alignment\\":\\"vertical\\"}}}", + }, +} +`); + }); + + it('doesnt migrate type = gauge containing invalid visState object', () => { + const migratedDoc = migrate({ + attributes: { + visState: JSON.stringify({ type: 'gauge' }), + }, + }); + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "visState": "{\\"type\\":\\"gauge\\"}", + }, +} +`); + }); + + describe('filters agg query migration', () => { + const doc = { + attributes: { + visState: JSON.stringify({ + aggs: [ + { + type: 'filters', + params: { + filters: [ + { + input: { + query: 'response:200', + }, + label: '', + }, + { + input: { + query: 'response:404', + }, + label: 'bad response', + }, + { + input: { + query: { + exists: { + field: 'phpmemory', + }, + }, + }, + label: '', + }, + ], + }, + }, + ], + }), + }, + }; + + it('should add language property to filters without one, assuming lucene', () => { + const migrationResult = migrate(doc); + expect(migrationResult).toEqual({ + attributes: { + visState: JSON.stringify({ + aggs: [ + { + type: 'filters', + params: { + filters: [ + { + input: { + query: 'response:200', + language: 'lucene', + }, + label: '', + }, + { + input: { + query: 'response:404', + language: 'lucene', + }, + label: 'bad response', + }, + { + input: { + query: { + exists: { + field: 'phpmemory', + }, + }, + language: 'lucene', + }, + label: '', + }, + ], + }, + }, + ], + }), + }, + }); + }); + }); + }); + describe('7.3.0 tsvb', () => { + const migrate = doc => migrations.visualization['7.3.0'](doc); + const generateDoc = ({ params }) => ({ + attributes: { + title: 'My Vis', + description: 'This is my super cool vis.', + visState: JSON.stringify({ params }), + uiStateJSON: '{}', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: '{}', + }, + }, + }); + it('should change series item filters from a string into an object', () => { + const params = { type: 'metric', series: [{ filter: 'Filter Bytes Test:>1000' }] }; + const testDoc1 = generateDoc({ params }); + const migratedTestDoc1 = migrate(testDoc1); + const series = JSON.parse(migratedTestDoc1.attributes.visState).params.series; + expect(series[0].filter).toHaveProperty('query'); + expect(series[0].filter).toHaveProperty('language'); + }); + it('should not change a series item filter string in the object after migration', () => { + const markdownParams = { + type: 'markdown', + series: [ + { + filter: 'Filter Bytes Test:>1000', + split_filters: [{ filter: 'bytes:>1000' }], + } + ] + }; + const markdownDoc = generateDoc({ params: markdownParams }); + const migratedMarkdownDoc = migrate(markdownDoc); + const markdownSeries = JSON.parse(migratedMarkdownDoc.attributes.visState).params.series; + expect(markdownSeries[0].filter.query).toBe(JSON.parse(markdownDoc.attributes.visState).params.series[0].filter); + expect(markdownSeries[0].split_filters[0].filter.query) + .toBe(JSON.parse(markdownDoc.attributes.visState).params.series[0].split_filters[0].filter); + }); + it('should change series item filters from a string into an object for all filters', () => { + const params = { + type: 'timeseries', + filter: 'bytes:>1000', + series: [ + { + filter: 'Filter Bytes Test:>1000', + split_filters: [{ filter: 'bytes:>1000' }], + } + ], + annotations: [{ query_string: 'bytes:>1000' }], + }; + const timeSeriesDoc = generateDoc({ params: params }); + const migratedtimeSeriesDoc = migrate(timeSeriesDoc); + const timeSeriesParams = JSON.parse(migratedtimeSeriesDoc.attributes.visState).params; + expect(Object.keys(timeSeriesParams.series[0].filter)).toEqual(expect.arrayContaining(['query', 'language'])); + expect(Object.keys(timeSeriesParams.series[0].split_filters[0].filter)).toEqual(expect.arrayContaining(['query', 'language'])); + expect(Object.keys(timeSeriesParams.annotations[0].query_string)).toEqual(expect.arrayContaining(['query', 'language'])); + }); + it('should not fail on a metric visualization without a filter in a series item', () => { + const params = { type: 'metric', series: [{}, {}, {}] }; + const testDoc1 = generateDoc({ params }); + const migratedTestDoc1 = migrate(testDoc1); + const series = JSON.parse(migratedTestDoc1.attributes.visState).params.series; + expect(series[2]).not.toHaveProperty('filter.query'); + }); + it('should not migrate a visualization of unknown type', () => { + const params = { type: 'unknown', series: [{ filter: 'foo:bar' }] }; + const doc = generateDoc({ params }); + const migratedDoc = migrate(doc); + const series = JSON.parse(migratedDoc.attributes.visState).params.series; + expect(series[0].filter).toEqual(params.series[0].filter); + }); + }); }); describe('dashboard', () => { diff --git a/src/legacy/core_plugins/kibana/public/context/api/__tests__/anchor.js b/src/legacy/core_plugins/kibana/public/context/api/__tests__/anchor.js index 9af9d8032fb27..4437271566420 100644 --- a/src/legacy/core_plugins/kibana/public/context/api/__tests__/anchor.js +++ b/src/legacy/core_plugins/kibana/public/context/api/__tests__/anchor.js @@ -129,7 +129,6 @@ describe('context app', function () { constant_score: { filter: { ids: { - type: 'doc', values: ['id'], }, } diff --git a/src/legacy/core_plugins/kibana/public/context/api/anchor.js b/src/legacy/core_plugins/kibana/public/context/api/anchor.js index c1d4823346b16..e4fe17290f1eb 100644 --- a/src/legacy/core_plugins/kibana/public/context/api/anchor.js +++ b/src/legacy/core_plugins/kibana/public/context/api/anchor.js @@ -19,9 +19,11 @@ import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; + import { SearchSourceProvider } from 'ui/courier'; -export function fetchAnchorProvider(indexPatterns, Private, i18n) { +export function fetchAnchorProvider(indexPatterns, Private) { const SearchSource = Private(SearchSourceProvider); return async function fetchAnchor( @@ -41,7 +43,6 @@ export function fetchAnchorProvider(indexPatterns, Private, i18n) { constant_score: { filter: { ids: { - type: anchorType, values: [anchorId], }, }, @@ -54,7 +55,7 @@ export function fetchAnchorProvider(indexPatterns, Private, i18n) { const response = await searchSource.fetch(); if (_.get(response, ['hits', 'total'], 0) < 1) { - throw new Error(i18n('kbn.context.failedToLoadAnchorDocumentErrorDescription', { + throw new Error(i18n.translate('kbn.context.failedToLoadAnchorDocumentErrorDescription', { defaultMessage: 'Failed to load anchor document.' })); } diff --git a/src/legacy/core_plugins/kibana/public/context/app.js b/src/legacy/core_plugins/kibana/public/context/app.js index 3e3bfdb603b81..60af9db918280 100644 --- a/src/legacy/core_plugins/kibana/public/context/app.js +++ b/src/legacy/core_plugins/kibana/public/context/app.js @@ -22,7 +22,6 @@ import _ from 'lodash'; import { callAfterBindingsWorkaround } from 'ui/compat'; import { uiModules } from 'ui/modules'; import contextAppTemplate from './app.html'; -import 'ui/filter_bar'; import './components/loading_button'; import './components/size_picker/size_picker'; import { getFirstSortableField } from './api/utils/sorting'; @@ -39,6 +38,9 @@ import { } from './query'; import { timefilter } from 'ui/timefilter'; +import { data } from 'plugins/data'; +data.filter.loadLegacyDirectives(); + const module = uiModules.get('apps/context', [ 'elasticsearch', 'kibana', diff --git a/src/legacy/core_plugins/kibana/public/context/index.js b/src/legacy/core_plugins/kibana/public/context/index.js index 8cce2c4e73e36..5dfd6552771bb 100644 --- a/src/legacy/core_plugins/kibana/public/context/index.js +++ b/src/legacy/core_plugins/kibana/public/context/index.js @@ -19,15 +19,14 @@ import _ from 'lodash'; -import { FilterBarQueryFilterProvider } from 'ui/filter_bar/query_filter'; -import 'ui/listen'; +import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter'; import uiRoutes from 'ui/routes'; import { i18n } from '@kbn/i18n'; import './app'; import contextAppRouteTemplate from './index.html'; import { getRootBreadcrumbs } from '../discover/breadcrumbs'; -import { getNewPlatform } from 'ui/new_platform'; +import { npStart } from 'ui/new_platform'; uiRoutes .when('/context/:indexPatternId/:type/:id*', { @@ -63,7 +62,6 @@ function ContextAppRouteController( $routeParams, $scope, AppState, - chrome, config, indexPattern, Private, @@ -79,14 +77,20 @@ function ContextAppRouteController( 'contextAppRoute.state.successorCount', ], () => this.state.save(true)); - $scope.$listen(queryFilter, 'update', () => { - this.filters = _.cloneDeep(queryFilter.getFilters()); + const updateSubsciption = queryFilter.getUpdates$().subscribe({ + next: () => { + this.filters = _.cloneDeep(queryFilter.getFilters()); + } + }); + + $scope.$on('$destroy', function () { + updateSubsciption.unsubscribe(); }); this.anchorType = $routeParams.type; this.anchorId = $routeParams.id; this.indexPattern = indexPattern; - this.discoverUrl = getNewPlatform().start.core.chrome.navLinks.get('kibana:discover').url; + this.discoverUrl = npStart.core.chrome.navLinks.get('kibana:discover').url; this.filters = _.cloneDeep(queryFilter.getFilters()); } diff --git a/src/legacy/core_plugins/kibana/public/context/query/actions.js b/src/legacy/core_plugins/kibana/public/context/query/actions.js index 17bfdae00ac0f..06035767b680e 100644 --- a/src/legacy/core_plugins/kibana/public/context/query/actions.js +++ b/src/legacy/core_plugins/kibana/public/context/query/actions.js @@ -18,6 +18,7 @@ */ import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; import React from 'react'; import { MarkdownSimple } from 'ui/markdown'; import { toastNotifications } from 'ui/notify'; @@ -27,7 +28,7 @@ import { fetchContextProvider } from '../api/context'; import { QueryParameterActionsProvider } from '../query_parameters'; import { FAILURE_REASONS, LOADING_STATUS } from './constants'; -export function QueryActionsProvider(courier, Private, Promise, i18n) { +export function QueryActionsProvider(Private, Promise) { const fetchAnchor = Private(fetchAnchorProvider); const { fetchPredecessors, fetchSuccessors } = Private(fetchContextProvider); const { @@ -81,7 +82,7 @@ export function QueryActionsProvider(courier, Private, Promise, i18n) { (error) => { setFailedStatus(state)('anchor', { error }); toastNotifications.addDanger({ - title: i18n('kbn.context.unableToLoadAnchorDocumentDescription', { + title: i18n.translate('kbn.context.unableToLoadAnchorDocumentDescription', { defaultMessage: 'Unable to load the anchor document' }), text: {error.message}, @@ -126,7 +127,7 @@ export function QueryActionsProvider(courier, Private, Promise, i18n) { (error) => { setFailedStatus(state)('predecessors', { error }); toastNotifications.addDanger({ - title: i18n('kbn.context.unableToLoadDocumentDescription', { + title: i18n.translate('kbn.context.unableToLoadDocumentDescription', { defaultMessage: 'Unable to load documents' }), text: {error.message}, diff --git a/src/legacy/core_plugins/kibana/public/context/query_parameters/actions.js b/src/legacy/core_plugins/kibana/public/context/query_parameters/actions.js index c9a7793948f14..fb314d3695bf2 100644 --- a/src/legacy/core_plugins/kibana/public/context/query_parameters/actions.js +++ b/src/legacy/core_plugins/kibana/public/context/query_parameters/actions.js @@ -19,7 +19,7 @@ import _ from 'lodash'; -import { FilterBarQueryFilterProvider } from 'ui/filter_bar/query_filter'; +import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter'; import { FilterManagerProvider } from 'ui/filter_manager'; import { MAX_CONTEXT_SIZE, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_app_state_mock.js b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_app_state_mock.js deleted file mode 100644 index 8a0e65cf47521..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_app_state_mock.js +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * A poor excuse for a mock just to get some basic tests to run in jest without requiring the injector. - * This could be improved if we extract the appState and state classes externally of their angular providers. - * @return {AppStateMock} - */ -export function getAppStateMock() { - class AppStateMock { - constructor(defaults) { - Object.assign(this, defaults); - } - - on() {} - off() {} - toJSON() { return ''; } - save() {} - } - - return AppStateMock; -} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_app_state_mock.ts b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_app_state_mock.ts new file mode 100644 index 0000000000000..1f2094d68063d --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_app_state_mock.ts @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { AppStateClass } from 'ui/state_management/app_state'; + +/** + * A poor excuse for a mock just to get some basic tests to run in jest without requiring the injector. + * This could be improved if we extract the appState and state classes externally of their angular providers. + * @return {AppStateMock} + */ +export function getAppStateMock(): AppStateClass { + class AppStateMock { + constructor(defaults: any) { + Object.assign(this, defaults); + } + + on() {} + off() {} + toJSON() { + return ''; + } + save() {} + translateHashToRison(stateHashOrRison: string | string[]) { + return stateHashOrRison; + } + getQueryParamName() { + return ''; + } + } + + return AppStateMock; +} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_embeddable_factories_mock.js b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_embeddable_factories_mock.js deleted file mode 100644 index c73576e3efe2b..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_embeddable_factories_mock.js +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/* global jest */ -export function getEmbeddableFactoryMock(config) { - const embeddableFactoryMockDefaults = { - create: jest.fn(() => Promise.resolve({})), - }; - return Object.assign(embeddableFactoryMockDefaults, config); -} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_embeddable_factories_mock.ts b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_embeddable_factories_mock.ts new file mode 100644 index 0000000000000..357ab307c3f12 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_embeddable_factories_mock.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* global jest */ +export function getEmbeddableFactoryMock(config?: any) { + const embeddableFactoryMockDefaults = { + create: jest.fn(() => Promise.resolve({})), + }; + return Object.assign(embeddableFactoryMockDefaults, config); +} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_saved_dashboard_mock.js b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_saved_dashboard_mock.js deleted file mode 100644 index d11436dead80c..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_saved_dashboard_mock.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - - -export function getSavedDashboardMock(config) { - const defaults = { - id: '123', - title: 'my dashboard', - panelsJSON: '[]', - searchSource: { - getOwnField: (param) => param - } - }; - return Object.assign(defaults, config); -} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_saved_dashboard_mock.ts b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_saved_dashboard_mock.ts new file mode 100644 index 0000000000000..0fc74f30a997c --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_saved_dashboard_mock.ts @@ -0,0 +1,45 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObjectDashboard } from '../saved_dashboard/saved_dashboard'; + +export function getSavedDashboardMock( + config?: Partial +): SavedObjectDashboard { + return { + id: '123', + title: 'my dashboard', + panelsJSON: '[]', + searchSource: { + getOwnField: (param: any) => param, + setField: () => {}, + }, + copyOnSave: false, + timeRestore: false, + timeTo: 'now', + timeFrom: 'now-15m', + optionsJSON: '', + lastSavedTitle: '', + destroy: () => {}, + save: () => { + return Promise.resolve('123'); + }, + ...config, + }; +} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/index.js b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/dashboard/__tests__/index.js rename to src/legacy/core_plugins/kibana/public/dashboard/__tests__/index.ts diff --git a/src/legacy/core_plugins/kibana/public/dashboard/actions/embeddables.ts b/src/legacy/core_plugins/kibana/public/dashboard/actions/embeddables.ts index 21fd2baee21b7..f1ec479877506 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/actions/embeddables.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/actions/embeddables.ts @@ -23,8 +23,9 @@ import _ from 'lodash'; import { createAction } from 'redux-actions'; import { EmbeddableMetadata, EmbeddableState } from 'ui/embeddable'; import { getEmbeddableCustomization, getPanel } from '../../selectors'; -import { PanelId, PanelState } from '../selectors'; +import { PanelId } from '../selectors'; import { updatePanel } from './panels'; +import { SavedDashboardPanel } from '../types'; import { KibanaAction, KibanaThunk } from '../../selectors/types'; @@ -113,7 +114,7 @@ export function embeddableStateChanged(changeData: { const customization = getEmbeddableCustomization(getState(), panelId); if (!_.isEqual(embeddableState.customization, customization)) { const originalPanelState = getPanel(getState(), panelId); - const newPanelState: PanelState = { + const newPanelState: SavedDashboardPanel = { ...originalPanelState, embeddableConfig: _.cloneDeep(embeddableState.customization), }; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/actions/metadata.ts b/src/legacy/core_plugins/kibana/public/dashboard/actions/metadata.ts index 6b2236ea04dca..eea16f02c9a4f 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/actions/metadata.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/actions/metadata.ts @@ -39,7 +39,5 @@ export interface UpdateDescriptionAction export type MetadataActions = UpdateDescriptionAction | UpdateTitleAction; -export const updateDescription = createAction( - MetadataActionTypeKeys.UPDATE_DESCRIPTION -); -export const updateTitle = createAction(MetadataActionTypeKeys.UPDATE_TITLE); +export const updateDescription = createAction(MetadataActionTypeKeys.UPDATE_DESCRIPTION); +export const updateTitle = createAction(MetadataActionTypeKeys.UPDATE_TITLE); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/actions/panels.ts b/src/legacy/core_plugins/kibana/public/dashboard/actions/panels.ts index d29e8da9e2eca..c4c41e53a545c 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/actions/panels.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/actions/panels.ts @@ -21,7 +21,8 @@ import { createAction } from 'redux-actions'; import { KibanaAction } from '../../selectors/types'; -import { PanelId, PanelState, PanelStateMap } from '../selectors'; +import { PanelId } from '../selectors'; +import { SavedDashboardPanel, SavedDashboardPanelMap } from '../types'; export enum PanelActionTypeKeys { DELETE_PANEL = 'DELETE_PANEL', @@ -36,10 +37,10 @@ export interface DeletePanelAction extends KibanaAction {} export interface UpdatePanelAction - extends KibanaAction {} + extends KibanaAction {} export interface UpdatePanelsAction - extends KibanaAction {} + extends KibanaAction {} export interface ResetPanelTitleAction extends KibanaAction {} @@ -53,7 +54,7 @@ export interface SetPanelTitleAction extends KibanaAction {} export interface SetPanelsAction - extends KibanaAction {} + extends KibanaAction {} export type PanelActions = | DeletePanelAction @@ -64,10 +65,10 @@ export type PanelActions = | SetPanelsAction; export const deletePanel = createAction(PanelActionTypeKeys.DELETE_PANEL); -export const updatePanel = createAction(PanelActionTypeKeys.UPDATE_PANEL); +export const updatePanel = createAction(PanelActionTypeKeys.UPDATE_PANEL); export const resetPanelTitle = createAction(PanelActionTypeKeys.RESET_PANEL_TITLE); export const setPanelTitle = createAction( PanelActionTypeKeys.SET_PANEL_TITLE ); -export const updatePanels = createAction(PanelActionTypeKeys.UPDATE_PANELS); -export const setPanels = createAction(PanelActionTypeKeys.SET_PANELS); +export const updatePanels = createAction(PanelActionTypeKeys.UPDATE_PANELS); +export const setPanels = createAction(PanelActionTypeKeys.SET_PANELS); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/actions/view.ts b/src/legacy/core_plugins/kibana/public/dashboard/actions/view.ts index 6b2bbc5fefaad..70e9592c3e0e6 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/actions/view.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/actions/view.ts @@ -108,4 +108,4 @@ export const updateRefreshConfig = createAction( ViewActionTypeKeys.UPDATE_REFRESH_CONFIG ); export const updateFilters = createAction(ViewActionTypeKeys.UPDATE_FILTERS); -export const updateQuery = createAction(ViewActionTypeKeys.UPDATE_QUERY); +export const updateQuery = createAction(ViewActionTypeKeys.UPDATE_QUERY); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.js b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.js index 637ff6efd66cd..1e8a6ecb3bf59 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.js @@ -18,6 +18,7 @@ */ import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; import React from 'react'; import angular from 'angular'; import { uiModules } from 'ui/modules'; @@ -25,16 +26,13 @@ import chrome from 'ui/chrome'; import { wrapInI18nContext } from 'ui/i18n'; import { toastNotifications } from 'ui/notify'; -import 'ui/listen'; -import 'ui/apply_filters'; - import { panelActionsStore } from './store/panel_actions_store'; import { getDashboardTitle } from './dashboard_strings'; import { DashboardViewMode } from './dashboard_view_mode'; import { TopNavIds } from './top_nav/top_nav_ids'; import { ConfirmationButtonTypes } from 'ui/modals/confirm_modal'; -import { FilterBarQueryFilterProvider } from 'ui/filter_bar/query_filter'; +import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter'; import { DocTitleProvider } from 'ui/doc_title'; import { getTopNavConfig } from './top_nav/get_top_nav_config'; import { DashboardConstants, createDashboardEditUrl } from './dashboard_constants'; @@ -58,8 +56,6 @@ import { getUnhashableStatesProvider } from 'ui/state_management/state_hashing'; import { DashboardViewportProvider } from './viewport/dashboard_viewport_provider'; -import { data } from 'plugins/data'; -data.search.loadLegacyDirectives(); const app = uiModules.get('app/dashboard', [ 'elasticsearch', @@ -92,8 +88,7 @@ app.directive('dashboardApp', function ($injector) { $routeParams, getAppState, dashboardConfig, - localStorage, - i18n, + localStorage ) { const filterManager = Private(FilterManagerProvider); const queryFilter = Private(FilterBarQueryFilterProvider); @@ -115,7 +110,7 @@ app.directive('dashboardApp', function ($injector) { const dashboardStateManager = new DashboardStateManager({ savedDashboard: dash, - AppState, + AppStateClass: AppState, hideWriteControls: dashboardConfig.getHideWriteControls(), addFilter: ({ field, value, operator, index }) => { filterActions.addFilter(field, value, operator, index, dashboardStateManager.getAppState(), filterManager); @@ -124,8 +119,6 @@ app.directive('dashboardApp', function ($injector) { $scope.getDashboardState = () => dashboardStateManager; $scope.appState = dashboardStateManager.getAppState(); - $scope.refreshInterval = timefilter.getRefreshInterval(); - // The 'previouslyStored' check is so we only update the time filter on dashboard open, not during // normal cross app navigation. @@ -205,7 +198,7 @@ app.directive('dashboardApp', function ($injector) { const updateBreadcrumbs = () => { chrome.breadcrumbs.set([ { - text: i18n('kbn.dashboard.dashboardAppBreadcrumbsTitle', { + text: i18n.translate('kbn.dashboard.dashboardAppBreadcrumbsTitle', { defaultMessage: 'Dashboard', }), href: $scope.landingPageUrl() @@ -336,20 +329,20 @@ app.directive('dashboardApp', function ($injector) { } confirmModal( - i18n('kbn.dashboard.changeViewModeConfirmModal.discardChangesDescription', + i18n.translate('kbn.dashboard.changeViewModeConfirmModal.discardChangesDescription', { defaultMessage: `Once you discard your changes, there's no getting them back.` } ), { onConfirm: revertChangesAndExitEditMode, onCancel: _.noop, - confirmButtonText: i18n('kbn.dashboard.changeViewModeConfirmModal.confirmButtonLabel', + confirmButtonText: i18n.translate('kbn.dashboard.changeViewModeConfirmModal.confirmButtonLabel', { defaultMessage: 'Discard changes' } ), - cancelButtonText: i18n('kbn.dashboard.changeViewModeConfirmModal.cancelButtonLabel', + cancelButtonText: i18n.translate('kbn.dashboard.changeViewModeConfirmModal.cancelButtonLabel', { defaultMessage: 'Continue editing' } ), defaultFocusedButton: ConfirmationButtonTypes.CANCEL, - title: i18n('kbn.dashboard.changeViewModeConfirmModal.discardChangesTitle', + title: i18n.translate('kbn.dashboard.changeViewModeConfirmModal.discardChangesTitle', { defaultMessage: 'Discard changes to dashboard?' } ) } @@ -373,7 +366,7 @@ app.directive('dashboardApp', function ($injector) { .then(function (id) { if (id) { toastNotifications.addSuccess({ - title: i18n('kbn.dashboard.dashboardWasSavedSuccessMessage', + title: i18n.translate('kbn.dashboard.dashboardWasSavedSuccessMessage', { defaultMessage: `Dashboard '{dashTitle}' was saved`, values: { dashTitle: dash.title }, @@ -392,7 +385,7 @@ app.directive('dashboardApp', function ($injector) { return { id }; }).catch((error) => { toastNotifications.addDanger({ - title: i18n('kbn.dashboard.dashboardWasNotSavedDangerMessage', + title: i18n.translate('kbn.dashboard.dashboardWasNotSavedDangerMessage', { defaultMessage: `Dashboard '{dashTitle}' was not saved. Error: {errorMessage}`, values: { @@ -519,15 +512,20 @@ app.directive('dashboardApp', function ($injector) { updateViewMode(dashboardStateManager.getViewMode()); // update root source when filters update - $scope.$listen(queryFilter, 'update', function () { - $scope.model.filters = queryFilter.getFilters(); - dashboardStateManager.applyFilters($scope.model.query, $scope.model.filters); + this.updateSubscription = queryFilter.getUpdates$().subscribe({ + next: () => { + $scope.model.filters = queryFilter.getFilters(); + dashboardStateManager.applyFilters($scope.model.query, $scope.model.filters); + } }); // update data when filters fire fetch event - $scope.$listen(queryFilter, 'fetch', $scope.refresh); + + this.fetchSubscription = queryFilter.getFetches$().subscribe($scope.refresh); $scope.$on('$destroy', () => { + this.updateSubscription.unsubscribe(); + this.fetchSubscription.unsubscribe(); dashboardStateManager.destroy(); }); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state.test.js b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state.test.js deleted file mode 100644 index 25511dc8ddb35..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state.test.js +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { DashboardStateManager } from './dashboard_state_manager'; -import { DashboardViewMode } from './dashboard_view_mode'; -import { embeddableIsInitialized, setPanels } from './actions'; -import { getAppStateMock, getSavedDashboardMock } from './__tests__'; -import { store } from '../store'; - -jest.mock('ui/chrome', () => ({ getKibanaVersion: () => '6.0.0' }), { virtual: true }); - - -describe('DashboardState', function () { - let dashboardState; - const savedDashboard = getSavedDashboardMock(); - const mockTimefilter = { - time: {}, - setTime: function (time) { this.time = time; }, - }; - const mockIndexPattern = { id: 'index1' }; - - function initDashboardState() { - dashboardState = new DashboardStateManager({ - savedDashboard, - AppState: getAppStateMock(), - hideWriteControls: false, - }); - } - - describe('syncTimefilterWithDashboard', function () { - test('syncs quick time', function () { - savedDashboard.timeRestore = true; - savedDashboard.timeFrom = 'now/w'; - savedDashboard.timeTo = 'now/w'; - - mockTimefilter.time.from = '2015-09-19 06:31:44.000'; - mockTimefilter.time.to = '2015-09-29 06:31:44.000'; - - initDashboardState(); - dashboardState.syncTimefilterWithDashboard(mockTimefilter); - - expect(mockTimefilter.time.to).toBe('now/w'); - expect(mockTimefilter.time.from).toBe('now/w'); - }); - - test('syncs relative time', function () { - savedDashboard.timeRestore = true; - savedDashboard.timeFrom = 'now-13d'; - savedDashboard.timeTo = 'now'; - - mockTimefilter.time.from = '2015-09-19 06:31:44.000'; - mockTimefilter.time.to = '2015-09-29 06:31:44.000'; - - initDashboardState(); - dashboardState.syncTimefilterWithDashboard(mockTimefilter); - - expect(mockTimefilter.time.to).toBe('now'); - expect(mockTimefilter.time.from).toBe('now-13d'); - }); - - test('syncs absolute time', function () { - savedDashboard.timeRestore = true; - savedDashboard.timeFrom = '2015-09-19 06:31:44.000'; - savedDashboard.timeTo = '2015-09-29 06:31:44.000'; - - mockTimefilter.time.from = 'now/w'; - mockTimefilter.time.to = 'now/w'; - - initDashboardState(); - dashboardState.syncTimefilterWithDashboard(mockTimefilter); - - expect(mockTimefilter.time.to).toBe(savedDashboard.timeTo); - expect(mockTimefilter.time.from).toBe(savedDashboard.timeFrom); - }); - }); - - describe('isDirty', function () { - beforeAll(() => { - initDashboardState(); - }); - - test('getIsDirty is true if isDirty is true and editing', () => { - dashboardState.switchViewMode(DashboardViewMode.EDIT); - dashboardState.isDirty = true; - expect(dashboardState.getIsDirty()).toBeTruthy(); - }); - - test('getIsDirty is false if isDirty is true and editing', () => { - dashboardState.switchViewMode(DashboardViewMode.VIEW); - dashboardState.isDirty = true; - expect(dashboardState.getIsDirty()).toBeFalsy(); - }); - }); - - describe('panelIndexPatternMapping', function () { - beforeAll(() => { - initDashboardState(); - }); - - function simulateNewEmbeddableWithIndexPatterns({ panelId, indexPatterns }) { - store.dispatch(setPanels({ [panelId]: { panelIndex: panelId } })); - const metadata = { title: 'my embeddable title', editUrl: 'editme', indexPatterns }; - store.dispatch(embeddableIsInitialized({ metadata, panelId: panelId })); - } - - test('initially has no index patterns', () => { - expect(dashboardState.getPanelIndexPatterns().length).toBe(0); - }); - - test('registers index pattern when an embeddable is initialized with one', async () => { - simulateNewEmbeddableWithIndexPatterns({ panelId: 'foo1', indexPatterns: [mockIndexPattern] }); - await new Promise(resolve => process.nextTick(resolve)); - expect(dashboardState.getPanelIndexPatterns().length).toBe(1); - }); - - test('registers unique index patterns', async () => { - simulateNewEmbeddableWithIndexPatterns({ panelId: 'foo2', indexPatterns: [mockIndexPattern] }); - await new Promise(resolve => process.nextTick(resolve)); - expect(dashboardState.getPanelIndexPatterns().length).toBe(1); - }); - - test('does not register undefined index pattern for panels with no index pattern', async () => { - simulateNewEmbeddableWithIndexPatterns({ panelId: 'foo2' }); - await new Promise(resolve => process.nextTick(resolve)); - expect(dashboardState.getPanelIndexPatterns().length).toBe(1); - }); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state.test.ts new file mode 100644 index 0000000000000..88312629da5d9 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state.test.ts @@ -0,0 +1,183 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { DashboardStateManager } from './dashboard_state_manager'; +import { DashboardViewMode } from './dashboard_view_mode'; +import { embeddableIsInitialized, setPanels } from './actions'; +import { getAppStateMock, getSavedDashboardMock } from './__tests__'; +import { store } from '../store'; +import { AppStateClass } from 'ui/state_management/app_state'; +import { DashboardAppState } from './types'; +import { IndexPattern } from 'ui/index_patterns'; +import { Timefilter } from 'ui/timefilter'; + +jest.mock('ui/chrome', () => ({ getKibanaVersion: () => '6.0.0' }), { virtual: true }); + +describe('DashboardState', function() { + let dashboardState: DashboardStateManager; + const savedDashboard = getSavedDashboardMock(); + const mockTimefilter: Timefilter = { + time: { to: 'now', from: 'now-15m' }, + setTime(time) { + this.time = time; + }, + getTime() { + return this.time; + }, + disableAutoRefreshSelector: jest.fn(), + setRefreshInterval: jest.fn(), + getRefreshInterval: jest.fn(), + disableTimeRangeSelector: jest.fn(), + enableAutoRefreshSelector: jest.fn(), + off: jest.fn(), + on: jest.fn(), + }; + const mockIndexPattern: IndexPattern = { id: 'index1', fields: [], title: 'hi' }; + + function initDashboardState() { + dashboardState = new DashboardStateManager({ + savedDashboard, + AppStateClass: getAppStateMock() as AppStateClass, + hideWriteControls: false, + addFilter: () => {}, + }); + } + + describe('syncTimefilterWithDashboard', function() { + test('syncs quick time', function() { + savedDashboard.timeRestore = true; + savedDashboard.timeFrom = 'now/w'; + savedDashboard.timeTo = 'now/w'; + + mockTimefilter.time.from = '2015-09-19 06:31:44.000'; + mockTimefilter.time.to = '2015-09-29 06:31:44.000'; + + initDashboardState(); + dashboardState.syncTimefilterWithDashboard(mockTimefilter); + + expect(mockTimefilter.time.to).toBe('now/w'); + expect(mockTimefilter.time.from).toBe('now/w'); + }); + + test('syncs relative time', function() { + savedDashboard.timeRestore = true; + savedDashboard.timeFrom = 'now-13d'; + savedDashboard.timeTo = 'now'; + + mockTimefilter.time.from = '2015-09-19 06:31:44.000'; + mockTimefilter.time.to = '2015-09-29 06:31:44.000'; + + initDashboardState(); + dashboardState.syncTimefilterWithDashboard(mockTimefilter); + + expect(mockTimefilter.time.to).toBe('now'); + expect(mockTimefilter.time.from).toBe('now-13d'); + }); + + test('syncs absolute time', function() { + savedDashboard.timeRestore = true; + savedDashboard.timeFrom = '2015-09-19 06:31:44.000'; + savedDashboard.timeTo = '2015-09-29 06:31:44.000'; + + mockTimefilter.time.from = 'now/w'; + mockTimefilter.time.to = 'now/w'; + + initDashboardState(); + dashboardState.syncTimefilterWithDashboard(mockTimefilter); + + expect(mockTimefilter.time.to).toBe(savedDashboard.timeTo); + expect(mockTimefilter.time.from).toBe(savedDashboard.timeFrom); + }); + }); + + describe('isDirty', function() { + beforeAll(() => { + initDashboardState(); + }); + + test('getIsDirty is true if isDirty is true and editing', () => { + dashboardState.switchViewMode(DashboardViewMode.EDIT); + dashboardState.isDirty = true; + expect(dashboardState.getIsDirty()).toBeTruthy(); + }); + + test('getIsDirty is false if isDirty is true and editing', () => { + dashboardState.switchViewMode(DashboardViewMode.VIEW); + dashboardState.isDirty = true; + expect(dashboardState.getIsDirty()).toBeFalsy(); + }); + }); + + describe('panelIndexPatternMapping', function() { + beforeAll(() => { + initDashboardState(); + }); + + function simulateNewEmbeddableWithIndexPatterns({ + panelId, + indexPatterns, + }: { + panelId: string; + indexPatterns?: IndexPattern[]; + }) { + store.dispatch( + setPanels({ + [panelId]: { + id: '123', + panelIndex: panelId, + version: '1', + type: 'hi', + embeddableConfig: {}, + gridData: { x: 1, y: 1, h: 1, w: 1, i: '1' }, + }, + }) + ); + const metadata = { title: 'my embeddable title', editUrl: 'editme', indexPatterns }; + store.dispatch(embeddableIsInitialized({ metadata, panelId })); + } + + test('initially has no index patterns', () => { + expect(dashboardState.getPanelIndexPatterns().length).toBe(0); + }); + + test('registers index pattern when an embeddable is initialized with one', async () => { + simulateNewEmbeddableWithIndexPatterns({ + panelId: 'foo1', + indexPatterns: [mockIndexPattern], + }); + await new Promise(resolve => process.nextTick(resolve)); + expect(dashboardState.getPanelIndexPatterns().length).toBe(1); + }); + + test('registers unique index patterns', async () => { + simulateNewEmbeddableWithIndexPatterns({ + panelId: 'foo2', + indexPatterns: [mockIndexPattern], + }); + await new Promise(resolve => process.nextTick(resolve)); + expect(dashboardState.getPanelIndexPatterns().length).toBe(1); + }); + + test('does not register undefined index pattern for panels with no index pattern', async () => { + simulateNewEmbeddableWithIndexPatterns({ panelId: 'foo2' }); + await new Promise(resolve => process.nextTick(resolve)); + expect(dashboardState.getPanelIndexPatterns().length).toBe(1); + }); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state_manager.js b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state_manager.js deleted file mode 100644 index e5d43c8de94b9..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state_manager.js +++ /dev/null @@ -1,622 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { i18n } from '@kbn/i18n'; -import _ from 'lodash'; - -import { DashboardViewMode } from './dashboard_view_mode'; -import { FilterUtils } from './lib/filter_utils'; -import { PanelUtils } from './panel/panel_utils'; -import { store } from '../store'; -import { - updateViewMode, - setPanels, - updateUseMargins, - updateIsFullScreenMode, - minimizePanel, - updateTitle, - updateDescription, - updateHidePanelTitles, - updateTimeRange, - updateRefreshConfig, - clearStagedFilters, - updateFilters, - updateQuery, - closeContextMenu, - requestReload, -} from './actions'; -import { stateMonitorFactory } from 'ui/state_management/state_monitor_factory'; -import { createPanelState } from './panel'; -import { getAppStateDefaults, migrateAppState } from './lib'; -import { - getViewMode, - getFullScreenMode, - getPanels, - getPanel, - getTitle, - getDescription, - getUseMargins, - getHidePanelTitles, - getStagedFilters, - getEmbeddables, - getEmbeddableMetadata, - getQuery, - getFilters, -} from '../selectors'; - -/** - * Dashboard state manager handles connecting angular and redux state between the angular and react portions of the - * app. There are two "sources of truth" that need to stay in sync - AppState (aka the `_a` portion of the url) and - * the Store. They aren't complete duplicates of each other as AppState has state that the Store doesn't, and vice - * versa. They should be as decoupled as possible so updating the store won't affect bwc of urls. - */ -export class DashboardStateManager { - /** - * - * @param {SavedDashboard} savedDashboard - * @param {AppState} AppState The AppState class to use when instantiating a new AppState instance. - * @param {boolean} hideWriteControls true if write controls should be hidden. - * @param {function} addFilter a function that can be used to add a filter bar filter - */ - constructor({ savedDashboard, AppState, hideWriteControls, addFilter }) { - this.savedDashboard = savedDashboard; - this.hideWriteControls = hideWriteControls; - this.addFilter = addFilter; - - this.stateDefaults = getAppStateDefaults(this.savedDashboard, this.hideWriteControls); - - this.appState = new AppState(this.stateDefaults); - - // Initializing appState does two things - first it translates the defaults into AppState, second it updates - // appState based on the URL (the url trumps the defaults). This means if we update the state format at all and - // want to handle BWC, we must not only migrate the data stored with saved Dashboard, but also any old state in the - // url. - migrateAppState(this.appState); - - this.isDirty = false; - - // We can't compare the filters stored on this.appState to this.savedDashboard because in order to apply - // the filters to the visualizations, we need to save it on the dashboard. We keep track of the original - // filter state in order to let the user know if their filters changed and provide this specific information - // in the 'lose changes' warning message. - this.lastSavedDashboardFilters = this.getFilterState(); - - // A mapping of panel index to the index pattern it uses. - this.panelIndexPatternMapping = {}; - - PanelUtils.initPanelIndexes(this.getPanels()); - - this.createStateMonitor(); - - store.dispatch(closeContextMenu()); - - // Always start out with all panels minimized when a dashboard is first loaded. - store.dispatch(minimizePanel()); - this._pushAppStateChangesToStore(); - - this.changeListeners = []; - - this.unsubscribe = store.subscribe(() => this._handleStoreChanges()); - this.stateMonitor.onChange(status => { - this.changeListeners.forEach(listener => listener(status)); - this._pushAppStateChangesToStore(); - }); - } - - registerChangeListener(callback) { - this.changeListeners.push(callback); - } - - _areStoreAndAppStatePanelsEqual() { - const state = store.getState(); - const storePanels = getPanels(store.getState()); - const appStatePanels = this.getPanels(); - - if (Object.values(storePanels).length !== appStatePanels.length) { - return false; - } - - return appStatePanels.every((appStatePanel) => { - const storePanel = getPanel(state, appStatePanel.panelIndex); - return _.isEqual(appStatePanel, storePanel); - }); - } - - /** - * Time is part of global state so we need to deal with it outside of _pushAppStateChangesToStore. - * @param {String|Object} newTimeFilter.to -- either a string representing an absolute time in utc format, - * or a relative time (now-15m), or a moment object - * @param {String|Object} newTimeFilter.from - either a string representing an absolute or a relative time, or a - * moment object - */ - handleTimeChange(newTimeFilter) { - store.dispatch(updateTimeRange({ - from: FilterUtils.convertTimeToUTCString(newTimeFilter.from), - to: FilterUtils.convertTimeToUTCString(newTimeFilter.to), - })); - } - - handleRefreshConfigChange({ pause, value }) { - store.dispatch(updateRefreshConfig({ - isPaused: pause, - interval: value, - })); - } - - /** - * Changes made to app state outside of direct calls to this class will need to be propagated to the store. - * @private - */ - _pushAppStateChangesToStore() { - // We need these checks, or you can get into a loop where a change is triggered by the store, which updates - // AppState, which then dispatches the change here, which will end up triggering setState warnings. - if (!this._areStoreAndAppStatePanelsEqual()) { - // Translate appState panels data into the data expected by redux, copying the panel objects as we do so - // because the panels inside appState can be mutated, while redux state should never be mutated directly. - const panelsMap = this.getPanels().reduce((acc, panel) => { - acc[panel.panelIndex] = _.cloneDeep(panel); - return acc; - }, {}); - store.dispatch(setPanels(panelsMap)); - } - - const state = store.getState(); - - if (getTitle(state) !== this.getTitle()) { - store.dispatch(updateTitle(this.getTitle())); - } - - if (getDescription(state) !== this.getDescription()) { - store.dispatch(updateDescription(this.getDescription())); - } - - if (getViewMode(state) !== this.getViewMode()) { - store.dispatch(updateViewMode(this.getViewMode())); - } - - if (getUseMargins(state) !== this.getUseMargins()) { - store.dispatch(updateUseMargins(this.getUseMargins())); - } - - if (getHidePanelTitles(state) !== this.getHidePanelTitles()) { - store.dispatch(updateHidePanelTitles(this.getHidePanelTitles())); - } - - if (getFullScreenMode(state) !== this.getFullScreenMode()) { - store.dispatch(updateIsFullScreenMode(this.getFullScreenMode())); - } - - if (getTitle(state) !== this.getTitle()) { - store.dispatch(updateTitle(this.getTitle())); - } - - if (getDescription(state) !== this.getDescription()) { - store.dispatch(updateDescription(this.getDescription())); - } - - if (getQuery(state) !== this.getQuery()) { - store.dispatch(updateQuery(this.getQuery())); - } - - this._pushFiltersToStore(); - } - - _pushFiltersToStore() { - const state = store.getState(); - const dashboardFilters = this.getDashboardFilterBars(); - if (!_.isEqual( - FilterUtils.cleanFiltersForComparison(dashboardFilters), - FilterUtils.cleanFiltersForComparison(getFilters(state)) - )) { - store.dispatch(updateFilters(dashboardFilters)); - } - } - - requestReload() { - store.dispatch(requestReload()); - } - - _handleStoreChanges() { - let dirty = false; - if (!this._areStoreAndAppStatePanelsEqual()) { - const panels = getPanels(store.getState()); - this.appState.panels = []; - this.panelIndexPatternMapping = {}; - Object.values(panels).map(panel => { - this.appState.panels.push(_.cloneDeep(panel)); - }); - dirty = true; - } - - _.forEach(getEmbeddables(store.getState()), (embeddable, panelId) => { - if (embeddable.initialized && !this.panelIndexPatternMapping.hasOwnProperty(panelId)) { - const embeddableMetadata = getEmbeddableMetadata(store.getState(), panelId); - if (embeddableMetadata.indexPatterns) { - this.panelIndexPatternMapping[panelId] = _.compact(embeddableMetadata.indexPatterns); - this.dirty = true; - } - } - }); - - const stagedFilters = getStagedFilters(store.getState()); - stagedFilters.forEach(filter => { - this.addFilter(filter); - }); - if (stagedFilters.length > 0) { - this.saveState(); - store.dispatch(clearStagedFilters()); - } - - const fullScreen = getFullScreenMode(store.getState()); - if (fullScreen !== this.getFullScreenMode()) { - this.setFullScreenMode(fullScreen); - } - - this.changeListeners.forEach(listener => listener({ dirty })); - this.saveState(); - } - - getFullScreenMode() { - return this.appState.fullScreenMode; - } - - setFullScreenMode(fullScreenMode) { - this.appState.fullScreenMode = fullScreenMode; - this.saveState(); - } - - getPanelIndexPatterns() { - const indexPatterns = _.flatten(Object.values(this.panelIndexPatternMapping)); - return _.uniq(indexPatterns, 'id'); - } - - /** - * Resets the state back to the last saved version of the dashboard. - */ - resetState() { - // In order to show the correct warning, we have to store the unsaved - // title on the dashboard object. We should fix this at some point, but this is how all the other object - // save panels work at the moment. - this.savedDashboard.title = this.savedDashboard.lastSavedTitle; - - // appState.reset uses the internal defaults to reset the state, but some of the default settings (e.g. the panels - // array) point to the same object that is stored on appState and is getting modified. - // The right way to fix this might be to ensure the defaults object stored on state is a deep - // clone, but given how much code uses the state object, I determined that to be too risky of a change for - // now. TODO: revisit this! - this.stateDefaults = getAppStateDefaults(this.savedDashboard, this.hideWriteControls); - // The original query won't be restored by the above because the query on this.savedDashboard is applied - // in place in order for it to affect the visualizations. - this.stateDefaults.query = this.lastSavedDashboardFilters.query; - // Need to make a copy to ensure they are not overwritten. - this.stateDefaults.filters = [...this.getLastSavedFilterBars()]; - - this.isDirty = false; - this.appState.setDefaults(this.stateDefaults); - this.appState.reset(); - this.stateMonitor.setInitialState(this.appState.toJSON()); - } - - /** - * Returns an object which contains the current filter state of this.savedDashboard. - * @returns {{timeTo: String, timeFrom: String, filterBars: Array, query: Object}} - */ - getFilterState() { - return { - timeTo: this.savedDashboard.timeTo, - timeFrom: this.savedDashboard.timeFrom, - filterBars: this.getDashboardFilterBars(), - query: this.getDashboardQuery() - }; - } - - getTitle() { - return this.appState.title; - } - - getDescription() { - return this.appState.description; - } - - setDescription(description) { - this.appState.description = description; - this.saveState(); - } - - setTitle(title) { - this.appState.title = title; - this.savedDashboard.title = title; - this.saveState(); - } - - getAppState() { - return this.appState; - } - - getQuery() { - return this.appState.query; - } - - getUseMargins() { - // Existing dashboards that don't define this should default to false. - return this.appState.options.useMargins === undefined ? false : this.appState.options.useMargins; - } - - setUseMargins(useMargins) { - this.appState.options.useMargins = useMargins; - this.saveState(); - } - - getHidePanelTitles() { - return this.appState.options.hidePanelTitles; - } - - setHidePanelTitles(hidePanelTitles) { - this.appState.options.hidePanelTitles = hidePanelTitles; - this.saveState(); - } - - getTimeRestore() { - return this.appState.timeRestore; - } - - setTimeRestore(timeRestore) { - this.appState.timeRestore = timeRestore; - this.saveState(); - } - - /** - * @returns {boolean} - */ - getIsTimeSavedWithDashboard() { - return this.savedDashboard.timeRestore; - } - - getDashboardFilterBars() { - return FilterUtils.getFilterBarsForDashboard(this.savedDashboard); - } - - getDashboardQuery() { - return FilterUtils.getQueryFilterForDashboard(this.savedDashboard); - } - - getLastSavedFilterBars() { - return this.lastSavedDashboardFilters.filterBars; - } - - getLastSavedQuery() { - return this.lastSavedDashboardFilters.query; - } - - /** - * @returns {boolean} True if the query changed since the last time the dashboard was saved, or if it's a - * new dashboard, if the query differs from the default. - */ - getQueryChanged() { - const currentQuery = this.appState.query; - const lastSavedQuery = this.getLastSavedQuery(); - - const isLegacyStringQuery = ( - _.isString(lastSavedQuery) - && _.isPlainObject(currentQuery) - && _.has(currentQuery, 'query') - ); - if (isLegacyStringQuery) { - return lastSavedQuery !== currentQuery.query; - } - - return !_.isEqual(currentQuery, lastSavedQuery); - } - - /** - * @returns {boolean} True if the filter bar state has changed since the last time the dashboard was saved, - * or if it's a new dashboard, if the query differs from the default. - */ - getFilterBarChanged() { - return !_.isEqual( - FilterUtils.cleanFiltersForComparison(this.appState.filters), - FilterUtils.cleanFiltersForComparison(this.getLastSavedFilterBars()) - ); - } - - /** - * @param timeFilter - * @returns {boolean} True if the time state has changed since the time saved with the dashboard. - */ - getTimeChanged(timeFilter) { - return ( - !FilterUtils.areTimesEqual(this.lastSavedDashboardFilters.timeFrom, timeFilter.getTime().from) || - !FilterUtils.areTimesEqual(this.lastSavedDashboardFilters.timeTo, timeFilter.getTime().to) - ); - } - - /** - * - * @returns {DashboardViewMode} - */ - getViewMode() { - return this.hideWriteControls ? DashboardViewMode.VIEW : this.appState.viewMode; - } - - /** - * @returns {boolean} - */ - getIsViewMode() { - return this.getViewMode() === DashboardViewMode.VIEW; - } - - /** - * @returns {boolean} - */ - getIsEditMode() { - return this.getViewMode() === DashboardViewMode.EDIT; - } - - /** - * - * @returns {boolean} True if the dashboard has changed since the last save (or, is new). - */ - getIsDirty(timeFilter) { - // Filter bar comparison is done manually (see cleanFiltersForComparison for the reason) and time picker - // changes are not tracked by the state monitor. - const hasTimeFilterChanged = timeFilter ? this.getFiltersChanged(timeFilter) : false; - return this.getIsEditMode() && (this.isDirty || hasTimeFilterChanged); - } - - getPanels() { - return this.appState.panels; - } - - updatePanel(panelIndex, panelAttributes) { - const panel = this.getPanels().find((panel) => panel.panelIndex === panelIndex); - Object.assign(panel, panelAttributes); - this.saveState(); - return panel; - } - - /** - * Creates and initializes a basic panel, adding it to the state. - * @param {number} id - * @param {string} type - */ - addNewPanel = (id, type) => { - const maxPanelIndex = PanelUtils.getMaxPanelIndex(this.getPanels()); - const newPanel = createPanelState(id, type, maxPanelIndex, this.getPanels()); - this.getPanels().push(newPanel); - this.saveState(); - } - - removePanel(panelIndex) { - _.remove(this.getPanels(), (panel) => { - if (panel.panelIndex === panelIndex) { - delete this.panelIndexPatternMapping[panelIndex]; - return true; - } else { - return false; - } - }); - this.saveState(); - } - - /** - * @param timeFilter - * @returns {Array.} An array of user friendly strings indicating the filter types that have changed. - */ - getChangedFilterTypes(timeFilter) { - const changedFilters = []; - if (this.getFilterBarChanged()) { - changedFilters.push('filter'); - } - if (this.getQueryChanged()) { - changedFilters.push('query'); - } - if (this.savedDashboard.timeRestore && this.getTimeChanged(timeFilter)) { - changedFilters.push('time range'); - } - return changedFilters; - } - - /** - * @return {boolean} True if filters (query, filter bar filters, and time picker if time is stored - * with the dashboard) have changed since the last saved state (or if the dashboard hasn't been saved, - * the default state). - */ - getFiltersChanged(timeFilter) { - return this.getChangedFilterTypes(timeFilter).length > 0; - } - - /** - * Updates timeFilter to match the time saved with the dashboard. - * @param {Object} timeFilter - * @param {func} timeFilter.setTime - * @param {func} timeFilter.setRefreshInterval - */ - syncTimefilterWithDashboard(timeFilter) { - if (!this.getIsTimeSavedWithDashboard()) { - throw new Error(i18n.translate('kbn.dashboard.stateManager.timeNotSavedWithDashboardErrorMessage', { - defaultMessage: 'The time is not saved with this dashboard so should not be synced.', - })); - } - - timeFilter.setTime({ - from: this.savedDashboard.timeFrom, - to: this.savedDashboard.timeTo, - }); - - if (this.savedDashboard.refreshInterval) { - timeFilter.setRefreshInterval(this.savedDashboard.refreshInterval); - } - } - - /** - * Saves the current application state to the URL. - */ - saveState() { - this.appState.save(); - } - - /** - * Applies the current filter state to the dashboard. - * @param filter {Array.} An array of filter bar filters. - */ - applyFilters(query, filters) { - this.appState.query = query; - this.savedDashboard.searchSource.setField('query', query); - this.savedDashboard.searchSource.setField('filter', filters); - this.saveState(); - // pinned filters go on global state, therefore are not propagated to store via app state and have to be pushed manually. - this._pushFiltersToStore(); - } - - /** - * Creates a state monitor and saves it to this.stateMonitor. Used to track unsaved changes made to appState. - */ - createStateMonitor() { - this.stateMonitor = stateMonitorFactory.create(this.appState, this.stateDefaults); - - this.stateMonitor.ignoreProps('viewMode'); - // Filters need to be compared manually because they sometimes have a $$hashkey stored on the object. - this.stateMonitor.ignoreProps('filters'); - // Query needs to be compared manually because saved legacy queries get migrated in app state automatically - this.stateMonitor.ignoreProps('query'); - - this.stateMonitor.onChange(status => { - this.isDirty = status.dirty; - }); - } - - /** - * @param newMode {DashboardViewMode} - */ - switchViewMode(newMode) { - this.appState.viewMode = newMode; - this.saveState(); - } - - /** - * Destroys and cleans up this object when it's no longer used. - */ - destroy() { - if (this.stateMonitor) { - this.stateMonitor.destroy(); - } - this.savedDashboard.destroy(); - this.unsubscribe(); - } -} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state_manager.ts b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state_manager.ts new file mode 100644 index 0000000000000..2dba212dbc484 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state_manager.ts @@ -0,0 +1,676 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import _ from 'lodash'; + +import { stateMonitorFactory, StateMonitor } from 'ui/state_management/state_monitor_factory'; +import { StaticIndexPattern } from 'ui/index_patterns'; +import { AppStateClass as TAppStateClass } from 'ui/state_management/app_state'; +import { TimeRange, Query } from 'ui/embeddable'; +import { Timefilter } from 'ui/timefilter'; +import { Filter } from '@kbn/es-query'; +import moment from 'moment'; +import { DashboardViewMode } from './dashboard_view_mode'; +import { FilterUtils } from './lib/filter_utils'; +import { PanelUtils } from './panel/panel_utils'; +import { store } from '../store'; + +import { + updateViewMode, + setPanels, + updateUseMargins, + updateIsFullScreenMode, + minimizePanel, + updateTitle, + updateDescription, + updateHidePanelTitles, + updateTimeRange, + updateRefreshConfig, + clearStagedFilters, + updateFilters, + updateQuery, + closeContextMenu, + requestReload, +} from './actions'; +import { createPanelState } from './panel'; +import { getAppStateDefaults, migrateAppState } from './lib'; +import { + getViewMode, + getFullScreenMode, + getPanels, + getPanel, + getTitle, + getDescription, + getUseMargins, + getHidePanelTitles, + getStagedFilters, + getEmbeddables, + getEmbeddableMetadata, + getQuery, + getFilters, +} from '../selectors'; +import { SavedObjectDashboard } from './saved_dashboard/saved_dashboard'; +import { + DashboardAppState, + SavedDashboardPanel, + SavedDashboardPanelMap, + StagedFilter, + DashboardAppStateParameters, +} from './types'; + +export type AddFilterFuntion = ({ field, value, operator, index }: StagedFilter) => void; + +/** + * Dashboard state manager handles connecting angular and redux state between the angular and react portions of the + * app. There are two "sources of truth" that need to stay in sync - AppState (aka the `_a` portion of the url) and + * the Store. They aren't complete duplicates of each other as AppState has state that the Store doesn't, and vice + * versa. They should be as decoupled as possible so updating the store won't affect bwc of urls. + */ +export class DashboardStateManager { + public savedDashboard: SavedObjectDashboard; + public appState: DashboardAppState; + public lastSavedDashboardFilters: { + timeTo?: string | moment.Moment; + timeFrom?: string | moment.Moment; + filterBars: Filter[]; + query: Query | string; + }; + private stateDefaults: DashboardAppStateParameters; + private hideWriteControls: boolean; + public isDirty: boolean; + private changeListeners: Array<(status: { dirty: boolean }) => void>; + private stateMonitor: StateMonitor; + private panelIndexPatternMapping: { [key: string]: StaticIndexPattern[] } = {}; + private addFilter: AddFilterFuntion; + private unsubscribe: () => void; + + /** + * + * @param savedDashboard + * @param AppState The AppState class to use when instantiating a new AppState instance. + * @param hideWriteControls true if write controls should be hidden. + * @param addFilter a function that can be used to add a filter bar filter + */ + constructor({ + savedDashboard, + AppStateClass, + hideWriteControls, + addFilter, + }: { + savedDashboard: SavedObjectDashboard; + AppStateClass: TAppStateClass; + hideWriteControls: boolean; + addFilter: AddFilterFuntion; + }) { + this.savedDashboard = savedDashboard; + this.hideWriteControls = hideWriteControls; + this.addFilter = addFilter; + + this.stateDefaults = getAppStateDefaults(this.savedDashboard, this.hideWriteControls); + + this.appState = new AppStateClass(this.stateDefaults); + + // Initializing appState does two things - first it translates the defaults into AppState, second it updates + // appState based on the URL (the url trumps the defaults). This means if we update the state format at all and + // want to handle BWC, we must not only migrate the data stored with saved Dashboard, but also any old state in the + // url. + migrateAppState(this.appState); + + this.isDirty = false; + + // We can't compare the filters stored on this.appState to this.savedDashboard because in order to apply + // the filters to the visualizations, we need to save it on the dashboard. We keep track of the original + // filter state in order to let the user know if their filters changed and provide this specific information + // in the 'lose changes' warning message. + this.lastSavedDashboardFilters = this.getFilterState(); + + // A mapping of panel index to the index pattern it uses. + this.panelIndexPatternMapping = {}; + + PanelUtils.initPanelIndexes(this.getPanels()); + + /** + * Creates a state monitor and saves it to this.stateMonitor. Used to track unsaved changes made to appState. + */ + this.stateMonitor = stateMonitorFactory.create( + this.appState, + this.stateDefaults + ); + + this.stateMonitor.ignoreProps('viewMode'); + // Filters need to be compared manually because they sometimes have a $$hashkey stored on the object. + this.stateMonitor.ignoreProps('filters'); + // Query needs to be compared manually because saved legacy queries get migrated in app state automatically + this.stateMonitor.ignoreProps('query'); + + this.stateMonitor.onChange((status: { dirty: boolean }) => { + this.isDirty = status.dirty; + }); + + store.dispatch(closeContextMenu()); + + // Always start out with all panels minimized when a dashboard is first loaded. + store.dispatch(minimizePanel()); + this.pushAppStateChangesToStore(); + + this.changeListeners = []; + + this.unsubscribe = store.subscribe(() => this.handleStoreChanges()); + this.stateMonitor.onChange((status: { dirty: boolean }) => { + this.changeListeners.forEach(listener => listener(status)); + this.pushAppStateChangesToStore(); + }); + } + + public registerChangeListener(callback: (status: { dirty: boolean }) => void) { + this.changeListeners.push(callback); + } + + private areStoreAndAppStatePanelsEqual() { + const state = store.getState(); + const storePanels = getPanels(store.getState()); + const appStatePanels = this.getPanels(); + + if (Object.values(storePanels).length !== appStatePanels.length) { + return false; + } + + return appStatePanels.every(appStatePanel => { + const storePanel = getPanel(state, appStatePanel.panelIndex); + return _.isEqual(appStatePanel, storePanel); + }); + } + + /** + * Time is part of global state so we need to deal with it outside of pushAppStateChangesToStore. + */ + public handleTimeChange(newTimeRange: TimeRange) { + const from = FilterUtils.convertTimeToUTCString(newTimeRange.from); + const to = FilterUtils.convertTimeToUTCString(newTimeRange.to); + store.dispatch( + updateTimeRange({ + from: from ? from.toString() : '', + to: to ? to.toString() : '', + }) + ); + } + + public handleRefreshConfigChange({ pause, value }: { pause: boolean; value: number }) { + store.dispatch( + updateRefreshConfig({ + isPaused: pause, + interval: value, + }) + ); + } + + /** + * Changes made to app state outside of direct calls to this class will need to be propagated to the store. + * @private + */ + private pushAppStateChangesToStore() { + // We need these checks, or you can get into a loop where a change is triggered by the store, which updates + // AppState, which then dispatches the change here, which will end up triggering setState warnings. + if (!this.areStoreAndAppStatePanelsEqual()) { + // Translate appState panels data into the data expected by redux, copying the panel objects as we do so + // because the panels inside appState can be mutated, while redux state should never be mutated directly. + const panelsMap = this.getPanels().reduce((acc: SavedDashboardPanelMap, panel) => { + acc[panel.panelIndex] = _.cloneDeep(panel); + return acc; + }, {}); + store.dispatch(setPanels(panelsMap)); + } + + const state = store.getState(); + + if (getTitle(state) !== this.getTitle()) { + store.dispatch(updateTitle(this.getTitle())); + } + + if (getDescription(state) !== this.getDescription()) { + store.dispatch(updateDescription(this.getDescription())); + } + + if (getViewMode(state) !== this.getViewMode()) { + store.dispatch(updateViewMode(this.getViewMode())); + } + + if (getUseMargins(state) !== this.getUseMargins()) { + store.dispatch(updateUseMargins(this.getUseMargins())); + } + + if (getHidePanelTitles(state) !== this.getHidePanelTitles()) { + store.dispatch(updateHidePanelTitles(this.getHidePanelTitles())); + } + + if (getFullScreenMode(state) !== this.getFullScreenMode()) { + store.dispatch(updateIsFullScreenMode(this.getFullScreenMode())); + } + + if (getTitle(state) !== this.getTitle()) { + store.dispatch(updateTitle(this.getTitle())); + } + + if (getDescription(state) !== this.getDescription()) { + store.dispatch(updateDescription(this.getDescription())); + } + + if (getQuery(state) !== this.getQuery()) { + store.dispatch(updateQuery(this.getQuery())); + } + + this._pushFiltersToStore(); + } + + _pushFiltersToStore() { + const state = store.getState(); + const dashboardFilters = this.getDashboardFilterBars(); + if ( + !_.isEqual( + FilterUtils.cleanFiltersForComparison(dashboardFilters), + FilterUtils.cleanFiltersForComparison(getFilters(state)) + ) + ) { + store.dispatch(updateFilters(dashboardFilters)); + } + } + + requestReload() { + store.dispatch(requestReload()); + } + + private handleStoreChanges() { + let dirty = false; + if (!this.areStoreAndAppStatePanelsEqual()) { + const panels: SavedDashboardPanelMap = getPanels(store.getState()); + this.appState.panels = []; + this.panelIndexPatternMapping = {}; + Object.values(panels).map((panel: SavedDashboardPanel) => { + this.appState.panels.push(_.cloneDeep(panel)); + }); + dirty = true; + } + + _.forEach(getEmbeddables(store.getState()), (embeddable, panelId) => { + if ( + panelId && + embeddable.initialized && + !this.panelIndexPatternMapping.hasOwnProperty(panelId) + ) { + const embeddableMetadata = getEmbeddableMetadata(store.getState(), panelId); + if (embeddableMetadata && embeddableMetadata.indexPatterns) { + this.panelIndexPatternMapping[panelId] = _.compact(embeddableMetadata.indexPatterns); + dirty = true; + } + } + }); + + const stagedFilters = getStagedFilters(store.getState()); + stagedFilters.forEach(filter => { + this.addFilter(filter); + }); + if (stagedFilters.length > 0) { + this.saveState(); + store.dispatch(clearStagedFilters()); + } + + const fullScreen = getFullScreenMode(store.getState()); + if (fullScreen !== this.getFullScreenMode()) { + this.setFullScreenMode(fullScreen); + } + + this.changeListeners.forEach(listener => listener({ dirty })); + this.saveState(); + } + + public getFullScreenMode() { + return this.appState.fullScreenMode; + } + + public setFullScreenMode(fullScreenMode: boolean) { + this.appState.fullScreenMode = fullScreenMode; + this.saveState(); + } + + public getPanelIndexPatterns() { + const indexPatterns = _.flatten(Object.values(this.panelIndexPatternMapping)); + return _.uniq(indexPatterns, 'id'); + } + + /** + * Resets the state back to the last saved version of the dashboard. + */ + public resetState() { + // In order to show the correct warning, we have to store the unsaved + // title on the dashboard object. We should fix this at some point, but this is how all the other object + // save panels work at the moment. + this.savedDashboard.title = this.savedDashboard.lastSavedTitle; + + // appState.reset uses the internal defaults to reset the state, but some of the default settings (e.g. the panels + // array) point to the same object that is stored on appState and is getting modified. + // The right way to fix this might be to ensure the defaults object stored on state is a deep + // clone, but given how much code uses the state object, I determined that to be too risky of a change for + // now. TODO: revisit this! + this.stateDefaults = getAppStateDefaults(this.savedDashboard, this.hideWriteControls); + // The original query won't be restored by the above because the query on this.savedDashboard is applied + // in place in order for it to affect the visualizations. + this.stateDefaults.query = this.lastSavedDashboardFilters.query; + // Need to make a copy to ensure they are not overwritten. + this.stateDefaults.filters = [...this.getLastSavedFilterBars()]; + + this.isDirty = false; + this.appState.setDefaults(this.stateDefaults); + this.appState.reset(); + this.stateMonitor.setInitialState(this.appState.toJSON()); + } + + /** + * Returns an object which contains the current filter state of this.savedDashboard. + * @returns {{timeTo: String, timeFrom: String, filterBars: Array, query: Object}} + */ + public getFilterState() { + return { + timeTo: this.savedDashboard.timeTo, + timeFrom: this.savedDashboard.timeFrom, + filterBars: this.getDashboardFilterBars(), + query: this.getDashboardQuery(), + }; + } + + public getTitle() { + return this.appState.title; + } + + public getDescription() { + return this.appState.description; + } + + public setDescription(description: string) { + this.appState.description = description; + this.saveState(); + } + + public setTitle(title: string) { + this.appState.title = title; + this.savedDashboard.title = title; + this.saveState(); + } + + public getAppState() { + return this.appState; + } + + public getQuery() { + return this.appState.query; + } + + public getUseMargins() { + // Existing dashboards that don't define this should default to false. + return this.appState.options.useMargins === undefined + ? false + : this.appState.options.useMargins; + } + + public setUseMargins(useMargins: boolean) { + this.appState.options.useMargins = useMargins; + this.saveState(); + } + + public getHidePanelTitles() { + return this.appState.options.hidePanelTitles; + } + + public setHidePanelTitles(hidePanelTitles: boolean) { + this.appState.options.hidePanelTitles = hidePanelTitles; + this.saveState(); + } + + public getTimeRestore() { + return this.appState.timeRestore; + } + + public setTimeRestore(timeRestore: boolean) { + this.appState.timeRestore = timeRestore; + this.saveState(); + } + + /** + * @returns {boolean} + */ + public getIsTimeSavedWithDashboard() { + return this.savedDashboard.timeRestore; + } + + public getDashboardFilterBars() { + return FilterUtils.getFilterBarsForDashboard(this.savedDashboard); + } + + public getDashboardQuery() { + return FilterUtils.getQueryFilterForDashboard(this.savedDashboard); + } + + public getLastSavedFilterBars(): Filter[] { + return this.lastSavedDashboardFilters.filterBars; + } + + public getLastSavedQuery(): Query | string { + return this.lastSavedDashboardFilters.query; + } + + /** + * @returns {boolean} True if the query changed since the last time the dashboard was saved, or if it's a + * new dashboard, if the query differs from the default. + */ + public getQueryChanged() { + const currentQuery = this.appState.query; + const lastSavedQuery = this.getLastSavedQuery(); + + const isLegacyStringQuery = + _.isString(lastSavedQuery) && _.isPlainObject(currentQuery) && _.has(currentQuery, 'query'); + if (isLegacyStringQuery) { + return (lastSavedQuery as string) !== (currentQuery as Query).query; + } + + return !_.isEqual(currentQuery, lastSavedQuery); + } + + /** + * @returns {boolean} True if the filter bar state has changed since the last time the dashboard was saved, + * or if it's a new dashboard, if the query differs from the default. + */ + public getFilterBarChanged() { + return !_.isEqual( + FilterUtils.cleanFiltersForComparison(this.appState.filters), + FilterUtils.cleanFiltersForComparison(this.getLastSavedFilterBars()) + ); + } + + /** + * @param timeFilter + * @returns {boolean} True if the time state has changed since the time saved with the dashboard. + */ + public getTimeChanged(timeFilter: Timefilter) { + return ( + !FilterUtils.areTimesEqual( + this.lastSavedDashboardFilters.timeFrom, + timeFilter.getTime().from + ) || + !FilterUtils.areTimesEqual(this.lastSavedDashboardFilters.timeTo, timeFilter.getTime().to) + ); + } + + /** + * + * @returns {DashboardViewMode} + */ + public getViewMode() { + return this.hideWriteControls ? DashboardViewMode.VIEW : this.appState.viewMode; + } + + /** + * @returns {boolean} + */ + public getIsViewMode() { + return this.getViewMode() === DashboardViewMode.VIEW; + } + + /** + * @returns {boolean} + */ + public getIsEditMode() { + return this.getViewMode() === DashboardViewMode.EDIT; + } + + /** + * + * @returns {boolean} True if the dashboard has changed since the last save (or, is new). + */ + public getIsDirty(timeFilter?: Timefilter) { + // Filter bar comparison is done manually (see cleanFiltersForComparison for the reason) and time picker + // changes are not tracked by the state monitor. + const hasTimeFilterChanged = timeFilter ? this.getFiltersChanged(timeFilter) : false; + return this.getIsEditMode() && (this.isDirty || hasTimeFilterChanged); + } + + public getPanels(): SavedDashboardPanel[] { + return this.appState.panels; + } + + public updatePanel(panelIndex: string, panelAttributes: any) { + const foundPanel = this.getPanels().find( + (panel: SavedDashboardPanel) => panel.panelIndex === panelIndex + ); + Object.assign(foundPanel, panelAttributes); + this.saveState(); + return foundPanel; + } + + /** + * Creates and initializes a basic panel, adding it to the state. + * @param {number} id + * @param {string} type + */ + public addNewPanel = (id: string, type: string) => { + const maxPanelIndex = PanelUtils.getMaxPanelIndex(this.getPanels()); + const newPanel = createPanelState(id, type, maxPanelIndex.toString(), this.getPanels()); + this.getPanels().push(newPanel); + this.saveState(); + }; + + public removePanel(panelIndex: string) { + _.remove(this.getPanels(), panel => { + if (panel.panelIndex === panelIndex) { + delete this.panelIndexPatternMapping[panelIndex]; + return true; + } else { + return false; + } + }); + this.saveState(); + } + + /** + * @param timeFilter + * @returns {Array.} An array of user friendly strings indicating the filter types that have changed. + */ + public getChangedFilterTypes(timeFilter: Timefilter) { + const changedFilters = []; + if (this.getFilterBarChanged()) { + changedFilters.push('filter'); + } + if (this.getQueryChanged()) { + changedFilters.push('query'); + } + if (this.savedDashboard.timeRestore && this.getTimeChanged(timeFilter)) { + changedFilters.push('time range'); + } + return changedFilters; + } + + /** + * @return True if filters (query, filter bar filters, and time picker if time is stored + * with the dashboard) have changed since the last saved state (or if the dashboard hasn't been saved, + * the default state). + */ + public getFiltersChanged(timeFilter: Timefilter) { + return this.getChangedFilterTypes(timeFilter).length > 0; + } + + /** + * Updates timeFilter to match the time saved with the dashboard. + */ + public syncTimefilterWithDashboard(timeFilter: Timefilter) { + if (!this.getIsTimeSavedWithDashboard()) { + throw new Error( + i18n.translate('kbn.dashboard.stateManager.timeNotSavedWithDashboardErrorMessage', { + defaultMessage: 'The time is not saved with this dashboard so should not be synced.', + }) + ); + } + + timeFilter.setTime({ + from: this.savedDashboard.timeFrom, + to: this.savedDashboard.timeTo, + }); + + if (this.savedDashboard.refreshInterval) { + timeFilter.setRefreshInterval(this.savedDashboard.refreshInterval); + } + } + + /** + * Saves the current application state to the URL. + */ + public saveState() { + this.appState.save(); + } + + /** + * Applies the current filter state to the dashboard. + * @param filter {Array.} An array of filter bar filters. + */ + public applyFilters(query: Query, filters: Filter[]) { + this.appState.query = query; + this.savedDashboard.searchSource.setField('query', query); + this.savedDashboard.searchSource.setField('filter', filters); + this.saveState(); + // pinned filters go on global state, therefore are not propagated to store via app state and have to be pushed manually. + this._pushFiltersToStore(); + } + + /** + * @param newMode {DashboardViewMode} + */ + public switchViewMode(newMode: DashboardViewMode) { + this.appState.viewMode = newMode; + this.saveState(); + } + + /** + * Destroys and cleans up this object when it's no longer used. + */ + public destroy() { + if (this.stateMonitor) { + this.stateMonitor.destroy(); + } + this.savedDashboard.destroy(); + this.unsubscribe(); + } +} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/grid/dashboard_grid.tsx b/src/legacy/core_plugins/kibana/public/dashboard/grid/dashboard_grid.tsx index bfbf3101c198e..4376b936df76f 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/grid/dashboard_grid.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/grid/dashboard_grid.tsx @@ -37,8 +37,12 @@ import { import { DashboardViewMode } from '../dashboard_view_mode'; import { DashboardPanel } from '../panel'; import { PanelUtils } from '../panel/panel_utils'; -import { PanelState, PanelStateMap, Pre61PanelState } from '../selectors/types'; -import { GridData } from '../types'; +import { + GridData, + SavedDashboardPanel, + Pre61SavedDashboardPanel, + SavedDashboardPanelMap, +} from '../types'; let lastValidGridSize = 0; @@ -117,10 +121,10 @@ const config = { monitorWidth: true }; const ResponsiveSizedGrid = sizeMe(config)(ResponsiveGrid); interface Props extends ReactIntl.InjectedIntlProps { - panels: PanelStateMap; + panels: SavedDashboardPanelMap; getEmbeddableFactory: (panelType: string) => EmbeddableFactory; dashboardViewMode: DashboardViewMode.EDIT | DashboardViewMode.VIEW; - onPanelsUpdated: (updatedPanels: PanelStateMap) => void; + onPanelsUpdated: (updatedPanels: SavedDashboardPanelMap) => void; maximizedPanelId?: string; useMargins: boolean; } @@ -182,18 +186,18 @@ class DashboardGridUi extends React.Component { : PanelUtils.parseVersion('6.0.0'); if (panelVersion.major < 6 || (panelVersion.major === 6 && panelVersion.minor < 1)) { - panel = PanelUtils.convertPanelDataPre_6_1(panel as Pre61PanelState); + panel = PanelUtils.convertPanelDataPre_6_1((panel as unknown) as Pre61SavedDashboardPanel); } if (panelVersion.major < 6 || (panelVersion.major === 6 && panelVersion.minor < 3)) { - PanelUtils.convertPanelDataPre_6_3(panel as PanelState, this.props.useMargins); + PanelUtils.convertPanelDataPre_6_3(panel as SavedDashboardPanel, this.props.useMargins); } - return (panel as PanelState).gridData; + return (panel as SavedDashboardPanel).gridData; }); } - public createEmbeddableFactoriesMap(panels: PanelStateMap) { + public createEmbeddableFactoriesMap(panels: SavedDashboardPanelMap) { Object.values(panels).map(panel => { if (!this.embeddableFactoryMap[panel.type]) { this.embeddableFactoryMap[panel.type] = this.props.getEmbeddableFactory(panel.type); @@ -211,17 +215,14 @@ class DashboardGridUi extends React.Component { public onLayoutChange = (layout: PanelLayout[]) => { const { onPanelsUpdated, panels } = this.props; - const updatedPanels = layout.reduce( - (updatedPanelsAcc, panelLayout) => { - updatedPanelsAcc[panelLayout.i] = { - ...panels[panelLayout.i], - panelIndex: panelLayout.i, - gridData: _.pick(panelLayout, ['x', 'y', 'w', 'h', 'i']), - }; - return updatedPanelsAcc; - }, - {} as PanelStateMap - ); + const updatedPanels = layout.reduce((updatedPanelsAcc: SavedDashboardPanelMap, panelLayout) => { + updatedPanelsAcc[panelLayout.i] = { + ...panels[panelLayout.i], + panelIndex: panelLayout.i, + gridData: _.pick(panelLayout, ['x', 'y', 'w', 'h', 'i']), + }; + return updatedPanelsAcc; + }, {}); onPanelsUpdated(updatedPanels); }; @@ -240,7 +241,9 @@ class DashboardGridUi extends React.Component { const { focusedPanelIndex } = this.state; // Part of our unofficial API - need to render in a consistent order for plugins. - const panelsInOrder = Object.keys(panels).map((key: string) => panels[key] as PanelState); + const panelsInOrder = Object.keys(panels).map( + (key: string) => panels[key] as SavedDashboardPanel + ); panelsInOrder.sort((panelA, panelB) => { if (panelA.gridData.y === panelB.gridData.y) { return panelA.gridData.x - panelB.gridData.x; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/grid/dashboard_grid_container.ts b/src/legacy/core_plugins/kibana/public/dashboard/grid/dashboard_grid_container.ts index a2625a3306e71..aaf994376759d 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/grid/dashboard_grid_container.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/grid/dashboard_grid_container.ts @@ -21,17 +21,18 @@ import { connect } from 'react-redux'; import { Dispatch } from 'redux'; import { updatePanels } from '../actions'; import { getPanels, getUseMargins, getViewMode } from '../selectors'; -import { DashboardViewMode, PanelStateMap } from '../selectors/types'; +import { DashboardViewMode } from '../selectors/types'; import { DashboardGrid } from './dashboard_grid'; +import { SavedDashboardPanelMap } from '../types'; interface DashboardGridContainerStateProps { - panels: PanelStateMap; + panels: SavedDashboardPanelMap; dashboardViewMode: DashboardViewMode; useMargins: boolean; } interface DashboardGridContainerDispatchProps { - onPanelsUpdated(updatedPanels: PanelStateMap): void; + onPanelsUpdated(updatedPanels: SavedDashboardPanelMap): void; } const mapStateToProps = ({ dashboard }: any): any => ({ @@ -41,7 +42,7 @@ const mapStateToProps = ({ dashboard }: any): any => ({ }); const mapDispatchToProps = (dispatch: Dispatch) => ({ - onPanelsUpdated: (updatedPanels: PanelStateMap) => dispatch(updatePanels(updatedPanels)), + onPanelsUpdated: (updatedPanels: SavedDashboardPanelMap) => dispatch(updatePanels(updatedPanels)), }); export const DashboardGridContainer = connect< diff --git a/src/legacy/core_plugins/kibana/public/dashboard/index.js b/src/legacy/core_plugins/kibana/public/dashboard/index.js index 3efc1bde5b053..1fac69157c697 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/index.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/index.js @@ -18,11 +18,11 @@ */ import './dashboard_app'; +import { i18n } from '@kbn/i18n'; import './saved_dashboard/saved_dashboards'; import './dashboard_config'; import uiRoutes from 'ui/routes'; import chrome from 'ui/chrome'; -import 'ui/filter_bar'; import { wrapInI18nContext } from 'ui/i18n'; import { toastNotifications } from 'ui/notify'; @@ -39,6 +39,10 @@ import { DashboardListing, EMPTY_FILTER } from './listing/dashboard_listing'; import { uiModules } from 'ui/modules'; import 'ui/capabilities/route_setup'; +import { data } from 'plugins/data'; +data.search.loadLegacyDirectives(); +data.filter.loadLegacyDirectives(); + const app = uiModules.get('app/dashboard', [ 'ngRoute', 'react', @@ -48,8 +52,8 @@ app.directive('dashboardListing', function (reactDirective) { return reactDirective(wrapInI18nContext(DashboardListing)); }); -function createNewDashboardCtrl($scope, i18n) { - $scope.visitVisualizeAppLinkText = i18n('kbn.dashboard.visitVisualizeAppLinkText', { +function createNewDashboardCtrl($scope) { + $scope.visitVisualizeAppLinkText = i18n.translate('kbn.dashboard.visitVisualizeAppLinkText', { defaultMessage: 'visit the Visualize app', }); } @@ -58,16 +62,16 @@ uiRoutes .defaults(/dashboard/, { requireDefaultIndex: true, requireUICapability: 'dashboard.show', - badge: (i18n, uiCapabilities) => { + badge: uiCapabilities => { if (uiCapabilities.dashboard.showWriteControls) { return undefined; } return { - text: i18n('kbn.dashboard.badge.readOnly.text', { + text: i18n.translate('kbn.dashboard.badge.readOnly.text', { defaultMessage: 'Read only', }), - tooltip: i18n('kbn.dashboard.badge.readOnly.tooltip', { + tooltip: i18n.translate('kbn.dashboard.badge.readOnly.tooltip', { defaultMessage: 'Unable to save dashboards', }), iconType: 'glasses' @@ -76,7 +80,7 @@ uiRoutes }) .when(DashboardConstants.LANDING_PAGE_PATH, { template: dashboardListingTemplate, - controller($injector, $location, $scope, Private, config, i18n) { + controller($injector, $location, $scope, Private, config) { const services = Private(SavedObjectRegistryProvider).byLoaderPropertiesName; const kbnUrl = $injector.get('kbnUrl'); const dashboardConfig = $injector.get('dashboardConfig'); @@ -100,7 +104,7 @@ uiRoutes $scope.hideWriteControls = dashboardConfig.getHideWriteControls(); $scope.initialFilter = ($location.search()).filter || EMPTY_FILTER; chrome.breadcrumbs.set([{ - text: i18n('kbn.dashboard.dashboardBreadcrumbsTitle', { + text: i18n.translate('kbn.dashboard.dashboardBreadcrumbsTitle', { defaultMessage: 'Dashboards', }), }]); @@ -148,7 +152,7 @@ uiRoutes template: dashboardTemplate, controller: createNewDashboardCtrl, resolve: { - dash: function (savedDashboards, $route, redirectWhenMissing, kbnUrl, AppState, i18n) { + dash: function (savedDashboards, $route, redirectWhenMissing, kbnUrl, AppState) { const id = $route.current.params.id; return savedDashboards.get(id) @@ -169,7 +173,7 @@ uiRoutes if (error instanceof SavedObjectNotFound && id === 'create') { // Note "new AppState" is necessary so the state in the url is preserved through the redirect. kbnUrl.redirect(DashboardConstants.CREATE_NEW_DASHBOARD_URL, {}, new AppState()); - toastNotifications.addWarning(i18n('kbn.dashboard.urlWasRemovedInSixZeroWarningMessage', + toastNotifications.addWarning(i18n.translate('kbn.dashboard.urlWasRemovedInSixZeroWarningMessage', { defaultMessage: 'The url "dashboard/create" was removed in 6.0. Please update your bookmarks.' } )); } else { @@ -183,13 +187,13 @@ uiRoutes } }); -FeatureCatalogueRegistryProvider.register((i18n) => { +FeatureCatalogueRegistryProvider.register(() => { return { id: 'dashboard', - title: i18n('kbn.dashboard.featureCatalogue.dashboardTitle', { + title: i18n.translate('kbn.dashboard.featureCatalogue.dashboardTitle', { defaultMessage: 'Dashboard', }), - description: i18n('kbn.dashboard.featureCatalogue.dashboardDescription', { + description: i18n.translate('kbn.dashboard.featureCatalogue.dashboardDescription', { defaultMessage: 'Display and share a collection of visualizations and saved searches.', }), icon: 'dashboardApp', diff --git a/src/legacy/core_plugins/kibana/public/dashboard/lib/filter_utils.js b/src/legacy/core_plugins/kibana/public/dashboard/lib/filter_utils.js deleted file mode 100644 index cb5b88ca0c6e4..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/lib/filter_utils.js +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import moment from 'moment'; - -/** - * @typedef {Object} QueryFilter - * @property query_string {Object} - * @property query_string.query {String} - */ - -export class FilterUtils { - /** - * - * @param filter - * @returns {Boolean} True if the filter is of the special query type - * (e.g. goes in the query input bar), false otherwise (e.g. is in the filter bar). - */ - static isQueryFilter(filter) { - return filter.query && !filter.meta; - } - - /** - * - * @param {SavedDashboard} dashboard - * @returns {Array.} An array of filters stored with the dashboard. Includes - * both query filters and filter bar filters. - */ - static getDashboardFilters(dashboard) { - return dashboard.searchSource.getOwnField('filter'); - } - - /** - * Grabs a saved query to use from the dashboard, or if none exists, creates a default one. - * @param {SavedDashboard} dashboard - * @returns {QueryFilter} - */ - static getQueryFilterForDashboard(dashboard) { - if (dashboard.searchSource.getOwnField('query')) { - return dashboard.searchSource.getOwnField('query'); - } - - const dashboardFilters = this.getDashboardFilters(dashboard); - const dashboardQueryFilter = _.find(dashboardFilters, this.isQueryFilter); - return dashboardQueryFilter ? dashboardQueryFilter.query : ''; - } - - /** - * Returns the filters for the dashboard that should appear in the filter bar area. - * @param {SavedDashboard} dashboard - * @return {Array.} Array of filters that should appear in the filter bar for the - * given dashboard - */ - static getFilterBarsForDashboard(dashboard) { - return _.reject(this.getDashboardFilters(dashboard), this.isQueryFilter); - } - - /** - * Converts the time to a utc formatted string. If the time is not valid (e.g. it might be in a relative format like - * 'now-15m', then it just returns what it was passed). - * @param time {string|Moment} - * @returns {string} the time represented in utc format, or if the time range was not able to be parsed into a moment - * object, it returns the same object it was given. - */ - static convertTimeToUTCString(time) { - if (moment(time).isValid()) { - return moment(time).utc(); - } else { - return time; - } - } - - /** - * Compares the two times, making sure they are in both compared in string format. Absolute times - * are sometimes stored as moment objects, but converted to strings when reloaded. Relative times are - * strings that are not convertible to moment objects. - * @param timeA {string|Moment} - * @param timeB {string|Moment} - * @returns {boolean} - */ - static areTimesEqual(timeA, timeB) { - return this.convertTimeToUTCString(timeA) === this.convertTimeToUTCString(timeB); - } - - /** - * Depending on how a dashboard is loaded, the filter object may contain a $$hashKey and $state that will throw - * off a filter comparison. This removes those variables. - * @param filters {Array.} - * @returns {Array.} - */ - static cleanFiltersForComparison(filters) { - return _.map(filters, (filter) => _.omit(filter, ['$$hashKey', '$state'])); - } -} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/lib/filter_utils.ts b/src/legacy/core_plugins/kibana/public/dashboard/lib/filter_utils.ts new file mode 100644 index 0000000000000..242b6c4f100ae --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/lib/filter_utils.ts @@ -0,0 +1,116 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; +import moment, { Moment } from 'moment'; +import { QueryFilter } from 'ui/filter_manager/query_filter'; +import { Filter } from '@kbn/es-query'; +import { SavedObjectDashboard } from '../saved_dashboard/saved_dashboard'; + +/** + * @typedef {Object} QueryFilter + * @property query_string {Object} + * @property query_string.query {String} + */ + +export class FilterUtils { + /** + * + * @param filter + * @returns {Boolean} True if the filter is of the special query type + * (e.g. goes in the query input bar), false otherwise (e.g. is in the filter bar). + */ + public static isQueryFilter(filter: Filter) { + return filter.query && !filter.meta; + } + + /** + * + * @param {SavedDashboard} dashboard + * @returns {Array.} An array of filters stored with the dashboard. Includes + * both query filters and filter bar filters. + */ + public static getDashboardFilters(dashboard: SavedObjectDashboard): Filter[] { + return dashboard.searchSource.getOwnField('filter'); + } + + /** + * Grabs a saved query to use from the dashboard, or if none exists, creates a default one. + * @param {SavedDashboard} dashboard + * @returns {QueryFilter} + */ + public static getQueryFilterForDashboard(dashboard: SavedObjectDashboard): QueryFilter | string { + if (dashboard.searchSource.getOwnField('query')) { + return dashboard.searchSource.getOwnField('query'); + } + + const dashboardFilters = this.getDashboardFilters(dashboard); + const dashboardQueryFilter = _.find(dashboardFilters, this.isQueryFilter); + return dashboardQueryFilter ? dashboardQueryFilter.query : ''; + } + + /** + * Returns the filters for the dashboard that should appear in the filter bar area. + * @param {SavedDashboard} dashboard + * @return {Array.} Array of filters that should appear in the filter bar for the + * given dashboard + */ + public static getFilterBarsForDashboard(dashboard: SavedObjectDashboard) { + return _.reject(this.getDashboardFilters(dashboard), this.isQueryFilter); + } + + /** + * Converts the time to a utc formatted string. If the time is not valid (e.g. it might be in a relative format like + * 'now-15m', then it just returns what it was passed). + * Note** Changing these moment objects to a utc string will actually cause a bug because it'll be in a format not + * expected by the time picker. This should get cleaned up and we should pick a single format to use everywhere. + * @param time {string|Moment} + * @returns {string} the time represented in utc format, or if the time range was not able to be parsed into a moment + * object, it returns the same object it was given. + */ + public static convertTimeToUTCString(time?: string | Moment): undefined | string | moment.Moment { + if (moment(time).isValid()) { + return moment(time).utc(); + } else { + return time; + } + } + + /** + * Compares the two times, making sure they are in both compared in string format. Absolute times + * are sometimes stored as moment objects, but converted to strings when reloaded. Relative times are + * strings that are not convertible to moment objects. + * @param timeA {string|Moment} + * @param timeB {string|Moment} + * @returns {boolean} + */ + public static areTimesEqual(timeA?: string | Moment, timeB?: string | Moment) { + return this.convertTimeToUTCString(timeA) === this.convertTimeToUTCString(timeB); + } + + /** + * Depending on how a dashboard is loaded, the filter object may contain a $$hashKey and $state that will throw + * off a filter comparison. This removes those variables. + * @param filters {Array.} + * @returns {Array.} + */ + public static cleanFiltersForComparison(filters: Filter[]) { + return _.map(filters, filter => _.omit(filter, ['$$hashKey', '$state'])); + } +} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/lib/get_app_state_defaults.js b/src/legacy/core_plugins/kibana/public/dashboard/lib/get_app_state_defaults.js deleted file mode 100644 index 16d97978b651c..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/lib/get_app_state_defaults.js +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { DashboardViewMode } from '../dashboard_view_mode'; -import { FilterUtils } from './filter_utils'; - -export function getAppStateDefaults(savedDashboard, hideWriteControls) { - const appState = { - fullScreenMode: false, - title: savedDashboard.title, - description: savedDashboard.description, - timeRestore: savedDashboard.timeRestore, - panels: savedDashboard.panelsJSON ? JSON.parse(savedDashboard.panelsJSON) : [], - options: savedDashboard.optionsJSON ? JSON.parse(savedDashboard.optionsJSON) : {}, - query: FilterUtils.getQueryFilterForDashboard(savedDashboard), - filters: FilterUtils.getFilterBarsForDashboard(savedDashboard), - viewMode: savedDashboard.id || hideWriteControls ? DashboardViewMode.VIEW : DashboardViewMode.EDIT, - }; - - // For BWC in pre 6.1 versions where uiState was stored at the dashboard level, not at the panel level. - // TODO: introduce a migration for this - if (savedDashboard.uiStateJSON) { - const uiState = JSON.parse(savedDashboard.uiStateJSON); - appState.panels.forEach(panel => { - panel.embeddableConfig = uiState[`P-${panel.panelIndex}`]; - }); - delete savedDashboard.uiStateJSON; - } - - // For BWC of pre 6.4 where search embeddables stored state directly on the panel and not under embeddableConfig. - // TODO: introduce a migration for this - appState.panels.forEach(panel => { - if (panel.columns || panel.sort) { - panel.embeddableConfig = { - ...panel.embeddableConfig, - columns: panel.columns, - sort: panel.sort - }; - delete panel.columns; - delete panel.sort; - } - }); - - - return appState; -} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/lib/get_app_state_defaults.ts b/src/legacy/core_plugins/kibana/public/dashboard/lib/get_app_state_defaults.ts new file mode 100644 index 0000000000000..f8132a07df573 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/lib/get_app_state_defaults.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { DashboardViewMode } from '../dashboard_view_mode'; +import { FilterUtils } from './filter_utils'; +import { SavedObjectDashboard } from '../saved_dashboard/saved_dashboard'; +import { + Pre61SavedDashboardPanel, + Pre64SavedDashboardPanel, + DashboardAppStateParameters, +} from '../types'; + +export function getAppStateDefaults( + savedDashboard: SavedObjectDashboard, + hideWriteControls: boolean +): DashboardAppStateParameters { + const appState = { + fullScreenMode: false, + title: savedDashboard.title, + description: savedDashboard.description || '', + timeRestore: savedDashboard.timeRestore, + panels: savedDashboard.panelsJSON ? JSON.parse(savedDashboard.panelsJSON) : [], + options: savedDashboard.optionsJSON ? JSON.parse(savedDashboard.optionsJSON) : {}, + query: FilterUtils.getQueryFilterForDashboard(savedDashboard), + filters: FilterUtils.getFilterBarsForDashboard(savedDashboard), + viewMode: + savedDashboard.id || hideWriteControls ? DashboardViewMode.VIEW : DashboardViewMode.EDIT, + }; + + // For BWC in pre 6.1 versions where uiState was stored at the dashboard level, not at the panel level. + // TODO: introduce a migration for this + if (savedDashboard.uiStateJSON) { + const uiState = JSON.parse(savedDashboard.uiStateJSON); + appState.panels.forEach((panel: Pre61SavedDashboardPanel) => { + panel.embeddableConfig = uiState[`P-${panel.panelIndex}`]; + }); + delete savedDashboard.uiStateJSON; + } + + // For BWC of pre 6.4 where search embeddables stored state directly on the panel and not under embeddableConfig. + // TODO: introduce a migration for this + appState.panels.forEach((panel: Pre64SavedDashboardPanel) => { + if (panel.columns || panel.sort) { + panel.embeddableConfig = { + ...panel.embeddableConfig, + columns: panel.columns, + sort: panel.sort, + }; + delete panel.columns; + delete panel.sort; + } + }); + + return appState; +} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/lib/index.js b/src/legacy/core_plugins/kibana/public/dashboard/lib/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/dashboard/lib/index.js rename to src/legacy/core_plugins/kibana/public/dashboard/lib/index.ts diff --git a/src/legacy/core_plugins/kibana/public/dashboard/lib/migrate_app_state.js b/src/legacy/core_plugins/kibana/public/dashboard/lib/migrate_app_state.js deleted file mode 100644 index 7f2eff3953f35..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/lib/migrate_app_state.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * Creates a new instance of AppState based of the saved dashboard. - * - * @param appState {AppState} AppState class to instantiate - */ -export function migrateAppState(appState) { - // For BWC in pre 6.1 versions where uiState was stored at the dashboard level, not at the panel level. - if (appState.uiState) { - appState.panels.forEach(panel => { - panel.embeddableConfig = appState.uiState[`P-${panel.panelIndex}`]; - }); - delete appState.uiState; - appState.save(); - } -} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/lib/migrate_app_state.ts b/src/legacy/core_plugins/kibana/public/dashboard/lib/migrate_app_state.ts new file mode 100644 index 0000000000000..1ff2596a546b0 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/lib/migrate_app_state.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedDashboardPanel, DashboardAppState } from '../types'; + +/** + * Creates a new instance of AppState based of the saved dashboard. + * + * @param appState {AppState} AppState class to instantiate + */ +export function migrateAppState(appState: DashboardAppState) { + // For BWC in pre 6.1 versions where uiState was stored at the dashboard level, not at the panel level. + if (appState.uiState) { + appState.panels.forEach((panel: SavedDashboardPanel) => { + panel.embeddableConfig = appState.uiState[`P-${panel.panelIndex}`]; + }); + delete appState.uiState; + appState.save(); + } +} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/lib/save_dashboard.js b/src/legacy/core_plugins/kibana/public/dashboard/lib/save_dashboard.js deleted file mode 100644 index b7a92b9d7fbe0..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/lib/save_dashboard.js +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { updateSavedDashboard } from './update_saved_dashboard'; - -/** - * Saves the dashboard. - * @param toJson {function} A custom toJson function. Used because the previous code used - * the angularized toJson version, and it was unclear whether there was a reason not to use - * JSON.stringify - * @param timeFilter - * @param dashboardStateManager {DashboardStateManager} - * @param {object} [saveOptions={}] - * @property {boolean} [saveOptions.confirmOverwrite=false] - If true, attempts to create the source so it - * can confirm an overwrite if a document with the id already exists. - * @property {boolean} [saveOptions.isTitleDuplicateConfirmed=false] - If true, save allowed with duplicate title - * @property {func} [saveOptions.onTitleDuplicate] - function called if duplicate title exists. - * When not provided, confirm modal will be displayed asking user to confirm or cancel save. - * @returns {Promise} A promise that if resolved, will contain the id of the newly saved - * dashboard. - */ -export function saveDashboard(toJson, timeFilter, dashboardStateManager, saveOptions) { - dashboardStateManager.saveState(); - - const savedDashboard = dashboardStateManager.savedDashboard; - const appState = dashboardStateManager.appState; - - updateSavedDashboard(savedDashboard, appState, timeFilter, toJson); - - return savedDashboard.save(saveOptions) - .then((id) => { - dashboardStateManager.lastSavedDashboardFilters = dashboardStateManager.getFilterState(); - dashboardStateManager.resetState(); - return id; - }); -} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/lib/save_dashboard.ts b/src/legacy/core_plugins/kibana/public/dashboard/lib/save_dashboard.ts new file mode 100644 index 0000000000000..168f320b5ea7e --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/lib/save_dashboard.ts @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SaveOptions } from 'ui/saved_objects/saved_object'; +import { Timefilter } from 'ui/timefilter'; +import { updateSavedDashboard } from './update_saved_dashboard'; +import { DashboardStateManager } from '../dashboard_state_manager'; + +/** + * Saves the dashboard. + * @param toJson A custom toJson function. Used because the previous code used + * the angularized toJson version, and it was unclear whether there was a reason not to use + * JSON.stringify + * @returns A promise that if resolved, will contain the id of the newly saved + * dashboard. + */ +export function saveDashboard( + toJson: (obj: any) => string, + timeFilter: Timefilter, + dashboardStateManager: DashboardStateManager, + saveOptions: SaveOptions +): Promise { + dashboardStateManager.saveState(); + + const savedDashboard = dashboardStateManager.savedDashboard; + const appState = dashboardStateManager.appState; + + updateSavedDashboard(savedDashboard, appState, timeFilter, toJson); + + return savedDashboard.save(saveOptions).then((id: string) => { + dashboardStateManager.lastSavedDashboardFilters = dashboardStateManager.getFilterState(); + dashboardStateManager.resetState(); + return id; + }); +} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/lib/update_saved_dashboard.js b/src/legacy/core_plugins/kibana/public/dashboard/lib/update_saved_dashboard.js deleted file mode 100644 index 6bb8bcc4895b0..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/lib/update_saved_dashboard.js +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import { FilterUtils } from './filter_utils'; - -export function updateSavedDashboard(savedDashboard, appState, timeFilter, toJson) { - savedDashboard.title = appState.title; - savedDashboard.description = appState.description; - savedDashboard.timeRestore = appState.timeRestore; - savedDashboard.panelsJSON = toJson(appState.panels); - savedDashboard.optionsJSON = toJson(appState.options); - - savedDashboard.timeFrom = savedDashboard.timeRestore ? - FilterUtils.convertTimeToUTCString(timeFilter.getTime().from) - : undefined; - savedDashboard.timeTo = savedDashboard.timeRestore ? - FilterUtils.convertTimeToUTCString(timeFilter.getTime().to) - : undefined; - const timeRestoreObj = _.pick(timeFilter.getRefreshInterval(), ['display', 'pause', 'section', 'value']); - savedDashboard.refreshInterval = savedDashboard.timeRestore ? timeRestoreObj : undefined; -} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/lib/update_saved_dashboard.ts b/src/legacy/core_plugins/kibana/public/dashboard/lib/update_saved_dashboard.ts new file mode 100644 index 0000000000000..a43bee881c631 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/lib/update_saved_dashboard.ts @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; +import { AppState } from 'ui/state_management/app_state'; +import { Timefilter } from 'ui/timefilter'; +import { RefreshInterval } from 'ui/timefilter/timefilter'; +import { FilterUtils } from './filter_utils'; +import { SavedObjectDashboard } from '../saved_dashboard/saved_dashboard'; + +export function updateSavedDashboard( + savedDashboard: SavedObjectDashboard, + appState: AppState, + timeFilter: Timefilter, + toJson: (object: T) => string +) { + savedDashboard.title = appState.title; + savedDashboard.description = appState.description; + savedDashboard.timeRestore = appState.timeRestore; + savedDashboard.panelsJSON = toJson(appState.panels); + savedDashboard.optionsJSON = toJson(appState.options); + + savedDashboard.timeFrom = savedDashboard.timeRestore + ? FilterUtils.convertTimeToUTCString(timeFilter.getTime().from) + : undefined; + savedDashboard.timeTo = savedDashboard.timeRestore + ? FilterUtils.convertTimeToUTCString(timeFilter.getTime().to) + : undefined; + const timeRestoreObj: RefreshInterval = _.pick(timeFilter.getRefreshInterval(), [ + 'display', + 'pause', + 'section', + 'value', + ]); + savedDashboard.refreshInterval = savedDashboard.timeRestore ? timeRestoreObj : undefined; +} 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 747b92bb5a5de..107bda0aea08b 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/dashboard/panel/__tests__/panel_state.ts b/src/legacy/core_plugins/kibana/public/dashboard/panel/__tests__/panel_state.ts index 4d910feaa4119..57a3d336c3dec 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/__tests__/panel_state.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/panel/__tests__/panel_state.ts @@ -18,10 +18,15 @@ */ import expect from '@kbn/expect'; -import { PanelState } from '../../selectors'; import { createPanelState } from '../panel_state'; +import { SavedDashboardPanel } from '../../types'; -function createPanelWithDimensions(x: number, y: number, w: number, h: number): PanelState { +function createPanelWithDimensions( + x: number, + y: number, + w: number, + h: number +): SavedDashboardPanel { return { id: 'foo', version: '6.3.0', diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/dashboard_panel.tsx b/src/legacy/core_plugins/kibana/public/dashboard/panel/dashboard_panel.tsx index 0a3cd61a627ad..0d30494b1dab8 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/dashboard_panel.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/panel/dashboard_panel.tsx @@ -30,9 +30,10 @@ import { EmbeddableState, } from 'ui/embeddable'; import { EmbeddableErrorAction } from '../actions'; -import { PanelId, PanelState } from '../selectors'; +import { PanelId } from '../selectors'; import { PanelError } from './panel_error'; import { PanelHeader } from './panel_header'; +import { SavedDashboardPanel } from '../types'; export interface DashboardPanelProps { viewOnlyMode: boolean; @@ -48,7 +49,7 @@ export interface DashboardPanelProps { embeddableError: (errorMessage: EmbeddableErrorAction) => void; embeddableIsInitializing: () => void; initialized: boolean; - panel: PanelState; + panel: SavedDashboardPanel; className?: string; } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/dashboard_panel_container.ts b/src/legacy/core_plugins/kibana/public/dashboard/panel/dashboard_panel_container.ts index f21dbe4b13e21..100d7ba2806b7 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/dashboard_panel_container.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/panel/dashboard_panel_container.ts @@ -47,9 +47,9 @@ import { getPanelType, getViewMode, PanelId, - PanelState, } from '../selectors'; import { DashboardPanel } from './dashboard_panel'; +import { SavedDashboardPanel } from '../types'; export interface DashboardPanelContainerOwnProps { panelId: PanelId; @@ -61,7 +61,7 @@ interface DashboardPanelContainerStateProps { viewOnlyMode: boolean; containerState: ContainerState; initialized: boolean; - panel: PanelState; + panel: SavedDashboardPanel; lastReloadRequestTime?: number; } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_state.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_state.test.ts new file mode 100644 index 0000000000000..97f6883864c6d --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_state.test.ts @@ -0,0 +1,72 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +jest.mock('ui/chrome', () => ({ getKibanaVersion: () => '6.0.0' }), { virtual: true }); +jest.mock('ui/metadata', () => ({ + metadata: { + branch: 'my-metadata-branch', + version: 'my-metadata-version', + }, +})); + +import { DEFAULT_PANEL_HEIGHT, DEFAULT_PANEL_WIDTH } from '../dashboard_constants'; +import { SavedDashboardPanel } from '../types'; +import { createPanelState } from './panel_state'; + +const panels: SavedDashboardPanel[] = []; + +test('createPanelState adds a new panel state in 0,0 position', () => { + const panelState = createPanelState('id', 'type', '1', panels); + expect(panelState.type).toBe('type'); + expect(panelState.gridData.x).toBe(0); + expect(panelState.gridData.y).toBe(0); + expect(panelState.gridData.h).toBe(DEFAULT_PANEL_HEIGHT); + expect(panelState.gridData.w).toBe(DEFAULT_PANEL_WIDTH); + + panels.push(panelState); +}); + +test('createPanelState adds a second new panel state', () => { + const panelState = createPanelState('id2', 'type', '2', panels); + expect(panelState.gridData.x).toBe(DEFAULT_PANEL_WIDTH); + expect(panelState.gridData.y).toBe(0); + expect(panelState.gridData.h).toBe(DEFAULT_PANEL_HEIGHT); + expect(panelState.gridData.w).toBe(DEFAULT_PANEL_WIDTH); + + panels.push(panelState); +}); + +test('createPanelState adds a third new panel state', () => { + const panelState = createPanelState('id3', 'type', '3', panels); + expect(panelState.gridData.x).toBe(0); + expect(panelState.gridData.y).toBe(DEFAULT_PANEL_HEIGHT); + expect(panelState.gridData.h).toBe(DEFAULT_PANEL_HEIGHT); + expect(panelState.gridData.w).toBe(DEFAULT_PANEL_WIDTH); + + panels.push(panelState); +}); + +test('createPanelState adds a new panel state in the top most position', () => { + const panelsWithEmptySpace = panels.filter(panel => panel.gridData.x === 0); + const panelState = createPanelState('id3', 'type', '3', panelsWithEmptySpace); + expect(panelState.gridData.x).toBe(DEFAULT_PANEL_WIDTH); + expect(panelState.gridData.y).toBe(0); + expect(panelState.gridData.h).toBe(DEFAULT_PANEL_HEIGHT); + expect(panelState.gridData.w).toBe(DEFAULT_PANEL_WIDTH); +}); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_state.ts b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_state.ts index 69a0ba24af543..105ae2ebf1fcb 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_state.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_state.ts @@ -23,29 +23,14 @@ import { DEFAULT_PANEL_HEIGHT, DEFAULT_PANEL_WIDTH, } from '../dashboard_constants'; -import { PanelState } from '../selectors'; - -/** - * Represents a panel on a grid. Keeps track of position in the grid and what visualization it - * contains. - * - * @typedef {Object} PanelState - * @property {number} id - Id of the visualization contained in the panel. - * @property {string} version - Version of Kibana this panel was created in. - * @property {string} type - Type of the visualization in the panel. - * @property {number} panelIndex - Unique id to represent this panel in the grid. Note that this is - * NOT the index in the panels array. While it may initially represent that, it is not - * updated with changes in a dashboard, and is simply used as a unique identifier. The name - * remains as panelIndex for backward compatibility reasons - changing it can break reporting. - * @property {Object} gridData - * @property {number} gridData.w - Width of the panel. - * @property {number} gridData.h - Height of the panel. - * @property {number} gridData.x - Column position of the panel. - * @property {number} gridData.y - Row position of the panel. - */ +import { SavedDashboardPanel } from '../types'; // Look for the smallest y and x value where the default panel will fit. -function findTopLeftMostOpenSpace(width: number, height: number, currentPanels: PanelState[]) { +function findTopLeftMostOpenSpace( + width: number, + height: number, + currentPanels: SavedDashboardPanel[] +) { let maxY = -1; currentPanels.forEach(panel => { @@ -65,6 +50,14 @@ function findTopLeftMostOpenSpace(width: number, height: number, currentPanels: currentPanels.forEach(panel => { for (let x = panel.gridData.x; x < panel.gridData.x + panel.gridData.w; x++) { for (let y = panel.gridData.y; y < panel.gridData.y + panel.gridData.h; y++) { + const row = grid[y]; + if (row === undefined) { + throw new Error( + `Attempted to access a row that doesn't exist at ${y} for panel ${JSON.stringify( + panel + )}` + ); + } grid[y][x] = 1; } } @@ -96,22 +89,17 @@ function findTopLeftMostOpenSpace(width: number, height: number, currentPanels: } } } - return { x: 0, y: Infinity }; + return { x: 0, y: maxY }; } /** * Creates and initializes a basic panel state. - * @param {number} id - * @param {string} type - * @param {number} panelIndex - * @param {Array} currentPanels - * @return {PanelState} */ export function createPanelState( id: string, type: string, panelIndex: string, - currentPanels: PanelState[] + currentPanels: SavedDashboardPanel[] ) { const { x, y } = findTopLeftMostOpenSpace( DEFAULT_PANEL_WIDTH, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_utils.ts b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_utils.ts index b74fbe68f00a0..51d0baee2b159 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_utils.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_utils.ts @@ -21,8 +21,7 @@ import { i18n } from '@kbn/i18n'; import _ from 'lodash'; import chrome from 'ui/chrome'; import { DEFAULT_PANEL_HEIGHT, DEFAULT_PANEL_WIDTH } from '../dashboard_constants'; -import { PanelState } from '../selectors'; -import { GridData } from '../types'; +import { GridData, SavedDashboardPanel } from '../types'; const PANEL_HEIGHT_SCALE_FACTOR = 5; const PANEL_HEIGHT_SCALE_FACTOR_WITH_MARGINS = 4; @@ -36,7 +35,7 @@ export interface SemanticVersion { export class PanelUtils { // 6.1 switched from gridster to react grid. React grid uses different variables for tracking layout // eslint-disable-next-line @typescript-eslint/camelcase - public static convertPanelDataPre_6_1(panel: any): PanelState { + public static convertPanelDataPre_6_1(panel: any): SavedDashboardPanel { ['col', 'row'].forEach(key => { if (!_.has(panel, key)) { throw new Error( @@ -126,7 +125,7 @@ export class PanelUtils { }; } - public static initPanelIndexes(panels: PanelState[]): void { + public static initPanelIndexes(panels: SavedDashboardPanel[]): void { // find the largest panelIndex in all the panels let maxIndex = this.getMaxPanelIndex(panels); @@ -138,7 +137,7 @@ export class PanelUtils { }); } - public static getMaxPanelIndex(panels: PanelState[]): number { + public static getMaxPanelIndex(panels: SavedDashboardPanel[]): number { let maxId = panels.reduce((id, panel) => { return Math.max(id, Number(panel.panelIndex || id)); }, 0); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/reducers/panels.ts b/src/legacy/core_plugins/kibana/public/dashboard/reducers/panels.ts index b9db8f6bc22b4..735f636d8af3b 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/reducers/panels.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/reducers/panels.ts @@ -20,7 +20,12 @@ import _ from 'lodash'; import { Reducer } from 'redux'; import { PanelActions, PanelActionTypeKeys, SetPanelTitleActionPayload } from '../actions'; -import { PanelId, PanelState, PanelStateMap } from '../selectors'; +import { PanelId } from '../selectors'; +import { SavedDashboardPanel } from '../types'; + +interface PanelStateMap { + [key: string]: SavedDashboardPanel; +} const deletePanel = (panels: PanelStateMap, panelId: PanelId): PanelStateMap => { const panelsCopy = { ...panels }; @@ -28,7 +33,7 @@ const deletePanel = (panels: PanelStateMap, panelId: PanelId): PanelStateMap => return panelsCopy; }; -const updatePanel = (panels: PanelStateMap, panelState: PanelState): PanelStateMap => ({ +const updatePanel = (panels: PanelStateMap, panelState: SavedDashboardPanel): PanelStateMap => ({ ...panels, [panelState.panelIndex]: panelState, }); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts new file mode 100644 index 0000000000000..651be0a453a7e --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SearchSource } from 'ui/courier'; +import { SavedObject } from 'ui/saved_objects/saved_object'; +import moment from 'moment'; +import { RefreshInterval } from 'ui/timefilter/timefilter'; + +export interface SavedObjectDashboard extends SavedObject { + id?: string; + copyOnSave: boolean; + timeRestore: boolean; + // These optionally being moment objects rather than strings seems more like a bug than by design. It's due to + // some code in udpate_saved_dashboard that should probably get cleaned up. + timeTo: string | moment.Moment | undefined; + timeFrom: string | moment.Moment | undefined; + title: string; + description?: string; + panelsJSON: string; + optionsJSON: string | undefined; + // TODO: write a migration to rid of this, it's only around for bwc. + uiStateJSON?: string; + lastSavedTitle: string; + searchSource: SearchSource; + destroy: () => void; + refreshInterval?: RefreshInterval; +} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.js b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.js index a2719f31593a9..06b2920ac28c6 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.js @@ -18,6 +18,7 @@ */ import angular from 'angular'; +import { i18n } from '@kbn/i18n'; import { uiModules } from 'ui/modules'; import { createDashboardEditUrl } from '../dashboard_constants'; import { createLegacyClass } from 'ui/utils/legacy_class'; @@ -30,7 +31,7 @@ import { const module = uiModules.get('app/dashboard'); // Used only by the savedDashboards service, usually no reason to change this -module.factory('SavedDashboard', function (Private, config, i18n) { +module.factory('SavedDashboard', function (Private) { // SavedDashboard constructor. Usually you'd interact with an instance of this. // ID is option, without it one will be generated on save. const SavedObject = Private(SavedObjectProvider); @@ -49,7 +50,7 @@ module.factory('SavedDashboard', function (Private, config, i18n) { // default values that will get assigned if the doc is new defaults: { - title: i18n('kbn.dashboard.savedDashboard.newDashboardTitle', { defaultMessage: 'New Dashboard' }), + title: i18n.translate('kbn.dashboard.savedDashboard.newDashboardTitle', { defaultMessage: 'New Dashboard' }), hits: 0, description: '', panelsJSON: '[]', diff --git a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboards.js b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboards.js index d20a598ab000e..9b7d036590f1d 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboards.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboards.js @@ -35,7 +35,7 @@ savedObjectManagementRegistry.register({ }); // This is the only thing that gets injected into controllers -module.service('savedDashboards', function (Private, SavedDashboard, kbnIndex, kbnUrl, $http, chrome) { +module.service('savedDashboards', function (Private, SavedDashboard, kbnUrl, chrome) { const savedObjectClient = Private(SavedObjectsClientProvider); return new SavedObjectLoader(SavedDashboard, kbnUrl, chrome, savedObjectClient); }); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/selectors/dashboard.ts b/src/legacy/core_plugins/kibana/public/dashboard/selectors/dashboard.ts index 39ff7e51c689c..6e2a03af6ac0f 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/selectors/dashboard.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/selectors/dashboard.ts @@ -18,15 +18,9 @@ */ import _ from 'lodash'; -import { - ContainerState, - EmbeddableMetadata, - Filters, - Query, - RefreshConfig, - TimeRange, -} from 'ui/embeddable'; +import { ContainerState, EmbeddableMetadata, Query, RefreshConfig, TimeRange } from 'ui/embeddable'; import { EmbeddableCustomization } from 'ui/embeddable/types'; +import { Filter } from '@kbn/es-query'; import { DashboardViewMode } from '../dashboard_view_mode'; import { DashboardMetadata, @@ -34,14 +28,14 @@ import { EmbeddableReduxState, EmbeddablesMap, PanelId, - PanelState, - PanelStateMap, } from './types'; +import { SavedDashboardPanel, SavedDashboardPanelMap, StagedFilter } from '../types'; -export const getPanels = (dashboard: DashboardState): Readonly => dashboard.panels; +export const getPanels = (dashboard: DashboardState): Readonly => + dashboard.panels; -export const getPanel = (dashboard: DashboardState, panelId: PanelId): PanelState => - getPanels(dashboard)[panelId] as PanelState; +export const getPanel = (dashboard: DashboardState, panelId: PanelId): SavedDashboardPanel => + getPanels(dashboard)[panelId] as SavedDashboardPanel; export const getPanelType = (dashboard: DashboardState, panelId: PanelId): string => getPanel(dashboard, panelId).type; @@ -118,7 +112,7 @@ export const getTimeRange = (dashboard: DashboardState): TimeRange => dashboard. export const getRefreshConfig = (dashboard: DashboardState): RefreshConfig => dashboard.view.refreshConfig; -export const getFilters = (dashboard: DashboardState): Filters => dashboard.view.filters; +export const getFilters = (dashboard: DashboardState): Filter[] => dashboard.view.filters; export const getQuery = (dashboard: DashboardState): Query => dashboard.view.query; @@ -150,5 +144,5 @@ export const getContainerState = (dashboard: DashboardState, panelId: PanelId): /** * @return an array of filters any embeddables wish dashboard to apply */ -export const getStagedFilters = (dashboard: DashboardState): Filters => +export const getStagedFilters = (dashboard: DashboardState): StagedFilter[] => _.compact(_.map(dashboard.embeddables, 'stagedFilter')); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/selectors/types.ts b/src/legacy/core_plugins/kibana/public/dashboard/selectors/types.ts index 165f06d9037ae..d814ca52b85fe 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/selectors/types.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/selectors/types.ts @@ -17,9 +17,10 @@ * under the License. */ -import { EmbeddableMetadata, Filters, Query, RefreshConfig, TimeRange } from 'ui/embeddable'; +import { EmbeddableMetadata, Query, RefreshConfig, TimeRange } from 'ui/embeddable'; +import { Filter } from '@kbn/es-query'; import { DashboardViewMode } from '../dashboard_view_mode'; -import { GridData } from '../types'; +import { SavedDashboardPanelMap } from '../types'; export type DashboardViewMode = DashboardViewMode; export interface ViewState { @@ -32,22 +33,12 @@ export interface ViewState { readonly hidePanelTitles: boolean; readonly useMargins: boolean; readonly query: Query; - readonly filters: Filters; + readonly filters: Filter[]; } export type PanelId = string; export type SavedObjectId = string; -export interface PanelState { - readonly id: SavedObjectId; - readonly version: string; - readonly type: string; - panelIndex: PanelId; - readonly embeddableConfig: any; - readonly gridData: GridData; - readonly title?: string; -} - export interface EmbeddableReduxState { readonly metadata?: EmbeddableMetadata; readonly error?: string | object; @@ -59,23 +50,6 @@ export interface EmbeddableReduxState { readonly lastReloadRequestTime: number; } -export interface Pre61PanelState { - size_x: number; - size_y: number; - row: number; - col: number; - panelIndex: any; // earlier versions allowed this to be number or string - id: string; - type: string; - // Embeddableconfig didn't actually exist on older panel states but `migrate_app_state.js` handles - // stuffing it on. - embeddableConfig: any; -} - -export interface PanelStateMap { - [panelId: string]: PanelState | Pre61PanelState; -} - export interface EmbeddablesMap { readonly [panelId: string]: EmbeddableReduxState; } @@ -87,7 +61,7 @@ export interface DashboardMetadata { export interface DashboardState { readonly view: ViewState; - readonly panels: PanelStateMap; + readonly panels: SavedDashboardPanelMap; readonly embeddables: EmbeddablesMap; readonly metadata: DashboardMetadata; } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/store/panel_actions_store.ts b/src/legacy/core_plugins/kibana/public/dashboard/store/panel_actions_store.ts index 449125d0ecfa4..69a9a93b1828a 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/store/panel_actions_store.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/store/panel_actions_store.ts @@ -28,7 +28,9 @@ class PanelActionsStore { */ public initializeFromRegistry(panelActionsRegistry: ContextMenuAction[]) { panelActionsRegistry.forEach(panelAction => { - this.actions.push(panelAction); + if (!this.actions.includes(panelAction)) { + this.actions.push(panelAction); + } }); } } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/top_nav_ids.js b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/top_nav_ids.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/dashboard/top_nav/top_nav_ids.js rename to src/legacy/core_plugins/kibana/public/dashboard/top_nav/top_nav_ids.ts diff --git a/src/legacy/core_plugins/kibana/public/dashboard/types.ts b/src/legacy/core_plugins/kibana/public/dashboard/types.ts index eb76d73af7a58..40131d990e1e4 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/types.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/types.ts @@ -17,6 +17,11 @@ * under the License. */ +import { Query } from 'ui/embeddable'; +import { AppState } from 'ui/state_management/app_state'; +import { Filter } from '@kbn/es-query'; +import { DashboardViewMode } from './dashboard_view_mode'; + export interface GridData { w: number; h: number; @@ -24,3 +29,84 @@ export interface GridData { y: number; i: string; } + +export interface SavedDashboardPanel { + // TODO: Make id optional when embeddable API V2 is merged. At that point, it's okay to store panels + // that aren't backed by saved object ids. + readonly id: string; + + readonly version: string; + readonly type: string; + panelIndex: string; + embeddableConfig: any; + readonly gridData: GridData; + readonly title?: string; +} + +export interface Pre61SavedDashboardPanel { + readonly size_x: number; + readonly size_y: number; + readonly row: number; + readonly col: number; + readonly panelIndex: number | string; // earlier versions allowed this to be number or string + readonly id: string; + readonly type: string; + embeddableConfig: any; +} + +export interface Pre64SavedDashboardPanel { + columns?: string; + sort?: string; + readonly id?: string; + readonly version: string; + readonly type: string; + readonly panelIndex: string; + readonly gridData: GridData; + readonly title?: string; + embeddableConfig: any; +} + +export interface DashboardAppStateDefaults { + panels: SavedDashboardPanel[]; + fullScreenMode: boolean; + title: string; + description?: string; + timeRestore: boolean; + options: { + useMargins: boolean; + hidePanelTitles: boolean; + }; + query: Query; + filters: Filter[]; + viewMode: DashboardViewMode; +} + +export interface DashboardAppStateParameters { + panels: SavedDashboardPanel[]; + fullScreenMode: boolean; + title: string; + description: string; + timeRestore: boolean; + options: { + hidePanelTitles: boolean; + useMargins: boolean; + }; + query: Query | string; + filters: Filter[]; + viewMode: DashboardViewMode; +} + +// This could probably be improved if we flesh out AppState more... though AppState will be going away +// so maybe not worth too much time atm. +export type DashboardAppState = DashboardAppStateParameters & AppState; + +export interface SavedDashboardPanelMap { + [key: string]: SavedDashboardPanel; +} + +export interface StagedFilter { + field: string; + value: string; + operator: string; + index: string; +} diff --git a/src/legacy/core_plugins/kibana/public/dev_tools/hacks/__tests__/hide_empty_tools.js b/src/legacy/core_plugins/kibana/public/dev_tools/hacks/__tests__/hide_empty_tools.js index 03c86767067bf..25c7b945b9dfb 100644 --- a/src/legacy/core_plugins/kibana/public/dev_tools/hacks/__tests__/hide_empty_tools.js +++ b/src/legacy/core_plugins/kibana/public/dev_tools/hacks/__tests__/hide_empty_tools.js @@ -21,7 +21,7 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; import { hideEmptyDevTools } from '../hide_empty_tools'; -import { getNewPlatform } from 'ui/new_platform'; +import { npStart } from 'ui/new_platform'; describe('hide dev tools', function () { let updateNavLink; @@ -39,7 +39,7 @@ describe('hide dev tools', function () { } beforeEach(function () { - const coreNavLinks = getNewPlatform().start.core.chrome.navLinks; + const coreNavLinks = npStart.core.chrome.navLinks; updateNavLink = sinon.spy(coreNavLinks, 'update'); }); diff --git a/src/legacy/core_plugins/kibana/public/dev_tools/hacks/hide_empty_tools.js b/src/legacy/core_plugins/kibana/public/dev_tools/hacks/hide_empty_tools.js index c4b07f95f26e2..4dc55194562e5 100644 --- a/src/legacy/core_plugins/kibana/public/dev_tools/hacks/hide_empty_tools.js +++ b/src/legacy/core_plugins/kibana/public/dev_tools/hacks/hide_empty_tools.js @@ -19,12 +19,12 @@ import { uiModules } from 'ui/modules'; import { DevToolsRegistryProvider } from 'ui/registry/dev_tools'; -import { getNewPlatform } from 'ui/new_platform'; +import { npStart } from 'ui/new_platform'; export function hideEmptyDevTools(Private) { const hasTools = !!Private(DevToolsRegistryProvider).length; if (!hasTools) { - getNewPlatform().start.core.chrome.navLinks.update('kibana:dev_tools', { + npStart.core.chrome.navLinks.update('kibana:dev_tools', { hidden: true }); } diff --git a/src/legacy/core_plugins/kibana/public/dev_tools/index.js b/src/legacy/core_plugins/kibana/public/dev_tools/index.js index be81d1ec84285..e36e75f6837ab 100644 --- a/src/legacy/core_plugins/kibana/public/dev_tools/index.js +++ b/src/legacy/core_plugins/kibana/public/dev_tools/index.js @@ -18,6 +18,7 @@ */ import uiRoutes from 'ui/routes'; +import { i18n } from '@kbn/i18n'; import { DevToolsRegistryProvider } from 'ui/registry/dev_tools'; import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue'; import 'ui/directives/kbn_href'; @@ -34,24 +35,24 @@ uiRoutes }); uiRoutes.defaults(/^\/dev_tools(\/|$)/, { - badge: (i18n, uiCapabilities) => { + badge: uiCapabilities => { if (uiCapabilities.dev_tools.save) { return undefined; } return { - text: i18n('kbn.devTools.badge.readOnly.text', { + text: i18n.translate('kbn.devTools.badge.readOnly.text', { defaultMessage: 'Read only', }), - tooltip: i18n('kbn.devTools.badge.readOnly.tooltip', { + tooltip: i18n.translate('kbn.devTools.badge.readOnly.tooltip', { defaultMessage: 'Unable to save', }), iconType: 'glasses' }; }, - k7Breadcrumbs: (i18n) => [ + k7Breadcrumbs: () => [ { - text: i18n('kbn.devTools.k7BreadcrumbsDevToolsLabel', { + text: i18n.translate('kbn.devTools.k7BreadcrumbsDevToolsLabel', { defaultMessage: 'Dev Tools' }), href: '#/dev_tools' @@ -59,13 +60,13 @@ uiRoutes.defaults(/^\/dev_tools(\/|$)/, { ] }); -FeatureCatalogueRegistryProvider.register(i18n => { +FeatureCatalogueRegistryProvider.register(() => { return { id: 'console', - title: i18n('kbn.devTools.consoleTitle', { + title: i18n.translate('kbn.devTools.consoleTitle', { defaultMessage: 'Console' }), - description: i18n('kbn.devTools.consoleDescription', { + description: i18n.translate('kbn.devTools.consoleDescription', { defaultMessage: 'Skip cURL and use this JSON interface to work with your data directly.' }), icon: 'consoleApp', diff --git a/src/legacy/core_plugins/kibana/public/discover/components/fetch_error/fetch_error.js b/src/legacy/core_plugins/kibana/public/discover/components/fetch_error/fetch_error.js index 4d188030b7312..670e9446c6e4f 100644 --- a/src/legacy/core_plugins/kibana/public/discover/components/fetch_error/fetch_error.js +++ b/src/legacy/core_plugins/kibana/public/discover/components/fetch_error/fetch_error.js @@ -22,7 +22,7 @@ import React, { Fragment } from 'react'; import { uiModules } from 'ui/modules'; import { wrapInI18nContext } from 'ui/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { getNewPlatform } from 'ui/new_platform'; +import { npStart } from 'ui/new_platform'; import { EuiFlexGroup, @@ -40,7 +40,7 @@ const DiscoverFetchError = ({ fetchError }) => { let body; if (fetchError.lang === 'painless') { - const managementUrl = getNewPlatform().start.core.chrome.navLinks.get('kibana:management').url; + const managementUrl = npStart.core.chrome.navLinks.get('kibana:management').url; const url = `${managementUrl}/kibana/index_patterns`; body = ( diff --git a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field.js b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field.js index 622133d681861..f7469d0142d57 100644 --- a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field.js +++ b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field.js @@ -18,6 +18,7 @@ */ import $ from 'jquery'; +import { i18n } from '@kbn/i18n'; import html from './discover_field.html'; import _ from 'lodash'; import 'ui/directives/css_truncate'; @@ -28,7 +29,7 @@ import { capabilities } from 'ui/capabilities'; import { uiModules } from 'ui/modules'; const app = uiModules.get('apps/discover'); -app.directive('discoverField', function ($compile, i18n) { +app.directive('discoverField', function ($compile) { return { restrict: 'E', template: html, @@ -50,10 +51,10 @@ app.directive('discoverField', function ($compile, i18n) { } $scope.addRemoveButtonLabel = $scope.field.display - ? i18n('kbn.discover.fieldChooser.discoverField.removeButtonLabel', { + ? i18n.translate('kbn.discover.fieldChooser.discoverField.removeButtonLabel', { defaultMessage: 'remove', }) - : i18n('kbn.discover.fieldChooser.discoverField.addButtonLabel', { + : i18n.translate('kbn.discover.fieldChooser.discoverField.addButtonLabel', { defaultMessage: 'add', }); }; @@ -62,7 +63,7 @@ app.directive('discoverField', function ($compile, i18n) { let warnings = []; if (field.scripted) { - warnings.push(i18n('kbn.discover.fieldChooser.discoverField.scriptedFieldsTakeLongExecuteDescription', { + warnings.push(i18n.translate('kbn.discover.fieldChooser.discoverField.scriptedFieldsTakeLongExecuteDescription', { defaultMessage: 'Scripted fields can take a long time to execute.', })); } @@ -106,11 +107,11 @@ app.directive('discoverField', function ($compile, i18n) { detailScope = $scope.$new(); detailScope.warnings = getWarnings(field); detailScope.getBucketAriaLabel = (bucket) => { - return i18n('kbn.discover.fieldChooser.discoverField.bucketAriaLabel', { + return i18n.translate('kbn.discover.fieldChooser.discoverField.bucketAriaLabel', { defaultMessage: 'Value: {value}', values: { value: bucket.display === '' - ? i18n('kbn.discover.fieldChooser.discoverField.emptyStringText', { + ? i18n.translate('kbn.discover.fieldChooser.discoverField.emptyStringText', { defaultMessage: 'Empty string', }) : bucket.display, diff --git a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/field_chooser.js b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/field_chooser.js index f29e5b4af8ad2..15a49cb9d9624 100644 --- a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/field_chooser.js +++ b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/field_chooser.js @@ -18,6 +18,7 @@ */ import 'ui/directives/css_truncate'; +import { i18n } from '@kbn/i18n'; import 'ui/directives/field_name'; import './discover_field'; import 'ui/angular_ui_select'; @@ -30,7 +31,7 @@ import { uiModules } from 'ui/modules'; import fieldChooserTemplate from './field_chooser.html'; const app = uiModules.get('apps/discover'); -app.directive('discFieldChooser', function ($location, globalState, config, $route, i18n) { +app.directive('discFieldChooser', function ($location, config, $route) { return { restrict: 'E', scope: { @@ -48,10 +49,10 @@ app.directive('discFieldChooser', function ($location, globalState, config, $rou link: function ($scope) { $scope.$parent.$watch('showFilter', () =>{ $scope.toggleFieldFilterButtonAriaLabel = $scope.$parent.showFilter - ? i18n('kbn.discover.fieldChooser.toggleFieldFilterButtonHideAriaLabel', { + ? i18n.translate('kbn.discover.fieldChooser.toggleFieldFilterButtonHideAriaLabel', { defaultMessage: 'Hide field settings', }) - : i18n('kbn.discover.fieldChooser.toggleFieldFilterButtonShowAriaLabel', { + : i18n.translate('kbn.discover.fieldChooser.toggleFieldFilterButtonShowAriaLabel', { defaultMessage: 'Show field settings', }); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js index a688e8d7e3646..977fb8091c6a9 100644 --- a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js @@ -18,6 +18,7 @@ */ import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; import React from 'react'; import angular from 'angular'; import moment from 'moment'; @@ -39,9 +40,9 @@ import { timefilter } from 'ui/timefilter'; import { hasSearchStategyForIndexPattern, isDefaultTypeIndexPattern } from 'ui/courier'; import { toastNotifications } from 'ui/notify'; import { VisProvider } from 'ui/vis'; -import { VislibSeriesResponseHandlerProvider } from 'ui/vis/response_handlers/vislib'; +import { vislibSeriesResponseHandlerProvider } from 'ui/vis/response_handlers/vislib'; import { DocTitleProvider } from 'ui/doc_title'; -import { FilterBarQueryFilterProvider } from 'ui/filter_bar/query_filter'; +import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter'; import { intervalOptions } from 'ui/agg_types/buckets/_interval_options'; import { stateMonitorFactory } from 'ui/state_management/state_monitor_factory'; import uiRoutes from 'ui/routes'; @@ -95,16 +96,16 @@ uiRoutes ? getSavedSearchBreadcrumbs : getRootBreadcrumbs ), - badge: (i18n, uiCapabilities) => { + badge: uiCapabilities => { if (uiCapabilities.discover.save) { return undefined; } return { - text: i18n('kbn.discover.badge.readOnly.text', { + text: i18n.translate('kbn.discover.badge.readOnly.text', { defaultMessage: 'Read only', }), - tooltip: i18n('kbn.discover.badge.readOnly.tooltip', { + tooltip: i18n.translate('kbn.discover.badge.readOnly.tooltip', { defaultMessage: 'Unable to save searches', }), iconType: 'glasses' @@ -115,7 +116,7 @@ uiRoutes template: indexTemplate, reloadOnSearch: false, resolve: { - ip: function (Promise, indexPatterns, config, $location, Private) { + ip: function (Promise, indexPatterns, config, Private) { const State = Private(StateProvider); const savedObjectsClient = Private(SavedObjectsClientProvider); @@ -184,32 +185,30 @@ function discoverController( $timeout, $window, AppState, - Notifier, Private, Promise, config, courier, kbnUrl, localStorage, - i18n, - uiCapabilities, + uiCapabilities ) { const visualizeLoader = Private(VisualizeLoaderProvider); let visualizeHandler; const Vis = Private(VisProvider); const docTitle = Private(DocTitleProvider); const queryFilter = Private(FilterBarQueryFilterProvider); - const responseHandler = Private(VislibSeriesResponseHandlerProvider).handler; + const responseHandler = vislibSeriesResponseHandlerProvider().handler; const filterManager = Private(FilterManagerProvider); - const notify = new Notifier({ - location: 'Discover' - }); const getUnhashableStates = Private(getUnhashableStatesProvider); const shareContextMenuExtensions = Private(ShareContextMenuExtensionsRegistryProvider); const inspectorAdapters = { requests: new RequestAdapter() }; + let filterUpdateSubscription; + let filterFetchSubscription; + timefilter.disableTimeRangeSelector(); timefilter.disableAutoRefreshSelector(); @@ -226,7 +225,11 @@ function discoverController( // the saved savedSearch const savedSearch = $route.current.locals.savedSearch; - $scope.$on('$destroy', savedSearch.destroy); + $scope.$on('$destroy', () => { + savedSearch.destroy(); + if (filterFetchSubscription) filterFetchSubscription.unsubscribe(); + if (filterUpdateSubscription) filterUpdateSubscription.unsubscribe(); + }); const $appStatus = $scope.appStatus = this.appStatus = { dirty: !savedSearch.id @@ -235,10 +238,10 @@ function discoverController( const getTopNavLinks = () => { const newSearch = { key: 'new', - label: i18n('kbn.discover.localMenu.localMenu.newSearchTitle', { + label: i18n.translate('kbn.discover.localMenu.localMenu.newSearchTitle', { defaultMessage: 'New', }), - description: i18n('kbn.discover.localMenu.newSearchDescription', { + description: i18n.translate('kbn.discover.localMenu.newSearchDescription', { defaultMessage: 'New Search', }), run: function () { kbnUrl.change('/discover'); }, @@ -247,10 +250,10 @@ function discoverController( const saveSearch = { key: 'save', - label: i18n('kbn.discover.localMenu.saveTitle', { + label: i18n.translate('kbn.discover.localMenu.saveTitle', { defaultMessage: 'Save', }), - description: i18n('kbn.discover.localMenu.saveSearchDescription', { + description: i18n.translate('kbn.discover.localMenu.saveSearchDescription', { defaultMessage: 'Save Search', }), testId: 'discoverSaveButton', @@ -287,10 +290,10 @@ function discoverController( const openSearch = { key: 'open', - label: i18n('kbn.discover.localMenu.openTitle', { + label: i18n.translate('kbn.discover.localMenu.openTitle', { defaultMessage: 'Open', }), - description: i18n('kbn.discover.localMenu.openSavedSearchDescription', { + description: i18n.translate('kbn.discover.localMenu.openSavedSearchDescription', { defaultMessage: 'Open Saved Search', }), testId: 'discoverOpenButton', @@ -305,14 +308,14 @@ function discoverController( const shareSearch = { key: 'share', - label: i18n('kbn.discover.localMenu.shareTitle', { + label: i18n.translate('kbn.discover.localMenu.shareTitle', { defaultMessage: 'Share', }), - description: i18n('kbn.discover.localMenu.shareSearchDescription', { + description: i18n.translate('kbn.discover.localMenu.shareSearchDescription', { defaultMessage: 'Share Search', }), testId: 'shareTopNavButton', - run: async (menuItem, navController, anchorElement) => { + run: async (menuItem, navController, anchorElement) => { // eslint-disable-line no-unused-vars const sharingData = await this.getSharingData(); showShareContextMenu({ anchorElement, @@ -333,10 +336,10 @@ function discoverController( const inspectSearch = { key: 'inspect', - label: i18n('kbn.discover.localMenu.inspectTitle', { + label: i18n.translate('kbn.discover.localMenu.inspectTitle', { defaultMessage: 'Inspect', }), - description: i18n('kbn.discover.localMenu.openInspectorForSearchDescription', { + description: i18n.translate('kbn.discover.localMenu.openInspectorForSearchDescription', { defaultMessage: 'Open Inspector for search', }), testId: 'openInspectorButton', @@ -349,7 +352,7 @@ function discoverController( return [ newSearch, - ...uiCapabilities.discover.save ? [saveSearch] : [], + ...(uiCapabilities.discover.save ? [saveSearch] : []), openSearch, shareSearch, inspectSearch, @@ -383,7 +386,7 @@ function discoverController( const pageTitleSuffix = savedSearch.id && savedSearch.title ? `: ${savedSearch.title}` : ''; docTitle.change(`Discover${pageTitleSuffix}`); - const discoverBreadcrumbsTitle = i18n('kbn.discover.discoverBreadcrumbTitle', { + const discoverBreadcrumbsTitle = i18n.translate('kbn.discover.discoverBreadcrumbTitle', { defaultMessage: 'Discover', }); @@ -501,22 +504,20 @@ function discoverController( $state.sort = getSort.array($state.sort, $scope.indexPattern); $scope.getBucketIntervalToolTipText = () => { - return ( - i18n('kbn.discover.bucketIntervalTooltip', { - // eslint-disable-next-line max-len - defaultMessage: 'This interval creates {bucketsDescription} to show in the selected time range, so it has been scaled to {bucketIntervalDescription}', - values: { - bucketsDescription: $scope.bucketInterval.scale > 1 - ? i18n('kbn.discover.bucketIntervalTooltip.tooLargeBucketsText', { - defaultMessage: 'buckets that are too large', - }) - : i18n('kbn.discover.bucketIntervalTooltip.tooManyBucketsText', { - defaultMessage: 'too many buckets', - }), - bucketIntervalDescription: $scope.bucketInterval.description, - }, - }) - ); + return i18n.translate('kbn.discover.bucketIntervalTooltip', { + // eslint-disable-next-line max-len + defaultMessage: 'This interval creates {bucketsDescription} to show in the selected time range, so it has been scaled to {bucketIntervalDescription}', + values: { + bucketsDescription: $scope.bucketInterval.scale > 1 + ? i18n.translate('kbn.discover.bucketIntervalTooltip.tooLargeBucketsText', { + defaultMessage: 'buckets that are too large', + }) + : i18n.translate('kbn.discover.bucketIntervalTooltip.tooManyBucketsText', { + defaultMessage: 'too many buckets', + }), + bucketIntervalDescription: $scope.bucketInterval.description, + }, + }); }; $scope.$watchCollection('state.columns', function () { @@ -561,21 +562,23 @@ function discoverController( }); // update data source when filters update - $scope.$listen(queryFilter, 'update', function () { - $scope.filters = queryFilter.getFilters(); - return $scope.updateDataSource().then(function () { - $state.save(); - }); - }); + filterUpdateSubscription = queryFilter.getUpdates$().subscribe( + () => { + $scope.filters = queryFilter.getFilters(); + $scope.updateDataSource().then(function () { + $state.save(); + }); + } + ); + + // fetch data when filters fire fetch event + filterFetchSubscription = queryFilter.getFetches$().subscribe($scope.fetch); // update data source when hitting forward/back and the query changes $scope.$listen($state, 'fetch_with_changes', function (diff) { if (diff.indexOf('query') >= 0) $scope.fetch(); }); - // fetch data when filters fire fetch event - $scope.$listen(queryFilter, 'fetch', $scope.fetch); - $scope.$watch('opts.timefield', function (timefield) { $scope.enableTimeRangeSelector = !!timefield; }); @@ -662,7 +665,7 @@ function discoverController( stateMonitor.setInitialState($state.toJSON()); if (id) { toastNotifications.addSuccess({ - title: i18n('kbn.discover.notifications.savedSearchTitle', { + title: i18n.translate('kbn.discover.notifications.savedSearchTitle', { defaultMessage: `Search '{savedSearchTitle}' was saved`, values: { savedSearchTitle: savedSearch.title, @@ -683,7 +686,7 @@ function discoverController( return { id }; } catch(saveError) { toastNotifications.addDanger({ - title: i18n('kbn.discover.notifications.notSavedSearchTitle', { + title: i18n.translate('kbn.discover.notifications.notSavedSearchTitle', { defaultMessage: `Search '{savedSearchTitle}' was not saved.`, values: { savedSearchTitle: savedSearch.title, @@ -711,7 +714,13 @@ function discoverController( logInspectorRequest(); return courier.fetch(); }) - .catch(notify.error); + .catch((error) => { + toastNotifications.addError(error, { + title: i18n.translate('kbn.discover.discoverError', { + defaultMessage: 'Discover error', + }), + }); + }); }; $scope.updateQueryAndFetch = function ({ query, dateRange }) { @@ -764,10 +773,10 @@ function discoverController( function logInspectorRequest() { inspectorAdapters.requests.reset(); - const title = i18n('kbn.discover.inspectorRequestDataTitle', { + const title = i18n.translate('kbn.discover.inspectorRequestDataTitle', { defaultMessage: 'Data', }); - const description = i18n('kbn.discover.inspectorRequestDescription', { + const description = i18n.translate('kbn.discover.inspectorRequestDescription', { defaultMessage: 'This request queries Elasticsearch to fetch the data for the search.', }); inspectorRequest = inspectorAdapters.requests.start(title, { description }); @@ -792,7 +801,11 @@ function discoverController( if (fetchError) { $scope.fetchError = fetchError; } else { - notify.error(error); + toastNotifications.addError(error, { + title: i18n.translate('kbn.discover.errorLoadingData', { + defaultMessage: 'Error loading data', + }), + }); } // Restart. This enables auto-refresh functionality. @@ -958,7 +971,7 @@ function discoverController( } if (stateVal && !stateValFound) { - const warningTitle = i18n('kbn.discover.valueIsNotConfiguredIndexPatternIDWarningTitle', { + const warningTitle = i18n.translate('kbn.discover.valueIsNotConfiguredIndexPatternIDWarningTitle', { defaultMessage: '{stateVal} is not a configured index pattern ID', values: { stateVal: `"${stateVal}"`, @@ -968,7 +981,7 @@ function discoverController( if (ownIndexPattern) { toastNotifications.addWarning({ title: warningTitle, - text: i18n('kbn.discover.showingSavedIndexPatternWarningDescription', { + text: i18n.translate('kbn.discover.showingSavedIndexPatternWarningDescription', { defaultMessage: 'Showing the saved index pattern: "{ownIndexPatternTitle}" ({ownIndexPatternId})', values: { ownIndexPatternTitle: ownIndexPattern.title, @@ -981,7 +994,7 @@ function discoverController( toastNotifications.addWarning({ title: warningTitle, - text: i18n('kbn.discover.showingDefaultIndexPatternWarningDescription', { + text: i18n.translate('kbn.discover.showingDefaultIndexPatternWarningDescription', { defaultMessage: 'Showing the default index pattern: "{loadedIndexPatternTitle}" ({loadedIndexPatternId})', values: { loadedIndexPatternTitle: loadedIndexPattern.title, 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 ebc27316d4675..9f400e54899af 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/discover/doc_table/components/table_header.js b/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_header.js index 6bd80b0196d25..60d440b1f957d 100644 --- a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_header.js +++ b/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_header.js @@ -18,13 +18,14 @@ */ import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; import { shortenDottedString } from '../../../../common/utils/shorten_dotted_string'; import headerHtml from './table_header.html'; import { uiModules } from 'ui/modules'; const module = uiModules.get('app/discover'); -module.directive('kbnTableHeader', function (i18n) { +module.directive('kbnTableHeader', function () { return { restrict: 'A', scope: { @@ -55,7 +56,7 @@ module.directive('kbnTableHeader', function (i18n) { $scope.tooltip = function (column) { if (!$scope.isSortableColumn(column)) return ''; const name = $scope.isShortDots ? shortenDottedString(column) : column; - return i18n('kbn.docTable.tableHeader.sortByColumnTooltip', { + return i18n.translate('kbn.docTable.tableHeader.sortByColumnTooltip', { defaultMessage: 'Sort by {columnName}', values: { columnName: name }, }); @@ -132,12 +133,12 @@ module.directive('kbnTableHeader', function (i18n) { const [currentColumnName, currentDirection = 'asc'] = $scope.sortOrder; if(name === currentColumnName && currentDirection === 'asc') { - return i18n('kbn.docTable.tableHeader.sortByColumnDescendingAriaLabel', { + return i18n.translate('kbn.docTable.tableHeader.sortByColumnDescendingAriaLabel', { defaultMessage: 'Sort {columnName} descending', values: { columnName: name }, }); } - return i18n('kbn.docTable.tableHeader.sortByColumnAscendingAriaLabel', { + return i18n.translate('kbn.docTable.tableHeader.sortByColumnAscendingAriaLabel', { defaultMessage: 'Sort {columnName} ascending', values: { columnName: name }, }); diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_row.js b/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_row.js index de09c12aa14d3..6380d783b0a6f 100644 --- a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_row.js +++ b/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_row.js @@ -27,6 +27,8 @@ import detailsHtml from './table_row/details.html'; import { uiModules } from 'ui/modules'; import { disableFilter } from '@kbn/es-query'; import { dispatchRenderComplete } from 'ui/render_complete'; +import cellTemplateHtml from '../components/table_row/cell.html'; +import truncateByHeightTemplateHtml from '../components/table_row/truncate_by_height.html'; const module = uiModules.get('app/discover'); @@ -44,8 +46,8 @@ const MIN_LINE_LENGTH = 20; * ``` */ module.directive('kbnTableRow', function ($compile, $httpParamSerializer, kbnUrl, config) { - const cellTemplate = _.template(noWhiteSpace(require('../components/table_row/cell.html'))); - const truncateByHeightTemplate = _.template(noWhiteSpace(require('../components/table_row/truncate_by_height.html'))); + const cellTemplate = _.template(noWhiteSpace(cellTemplateHtml)); + const truncateByHeightTemplate = _.template(noWhiteSpace(truncateByHeightTemplateHtml)); return { restrict: 'A', diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_row/details.html b/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_row/details.html index 01cd1ecf769f0..8bc67f5b99696 100644 --- a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_row/details.html +++ b/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_row/details.html @@ -21,7 +21,7 @@ class="euiLink" data-test-subj="docTableRowAction" ng-href="{{ getContextAppHref() }}" - ng-if="indexPattern.isTimeBased()" + ng-if="indexPattern.isTimeBased() && !indexPattern.isTimeNanosBased()" i18n-id="kbn.docTable.tableRow.viewSurroundingDocumentsLinkText" i18n-default-message="View surrounding documents" > diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/doc_table.js b/src/legacy/core_plugins/kibana/public/discover/doc_table/doc_table.js index 4449a4f56b22e..17d4cb62e41ee 100644 --- a/src/legacy/core_plugins/kibana/public/discover/doc_table/doc_table.js +++ b/src/legacy/core_plugins/kibana/public/discover/doc_table/doc_table.js @@ -18,6 +18,7 @@ */ import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; import html from './doc_table.html'; import { getSort } from './lib/get_sort'; import './infinite_scroll'; @@ -28,11 +29,12 @@ import { uiModules } from 'ui/modules'; import 'ui/pager_control'; import 'ui/pager'; import { getRequestInspectorStats, getResponseInspectorStats } from 'ui/courier/utils/courier_inspector_utils'; +import { toastNotifications } from 'ui/notify'; import { getLimitedSearchResultsMessage } from './doc_table_strings'; uiModules.get('app/discover') - .directive('docTable', function (config, Notifier, getAppState, pagerFactory, $filter, courier, i18n) { + .directive('docTable', function (config, getAppState, pagerFactory, $filter, courier) { return { restrict: 'E', template: html, @@ -53,8 +55,6 @@ uiModules.get('app/discover') inspectorAdapters: '=?', }, link: function ($scope, $el) { - const notify = new Notifier(); - $scope.$watch('minimumVisibleRows', (minimumVisibleRows) => { $scope.limit = Math.max(minimumVisibleRows || 50, $scope.limit || 50); }); @@ -137,10 +137,10 @@ uiModules.get('app/discover') let inspectorRequest = undefined; if (_.has($scope, 'inspectorAdapters.requests')) { $scope.inspectorAdapters.requests.reset(); - const title = i18n('kbn.docTable.inspectorRequestDataTitle', { + const title = i18n.translate('kbn.docTable.inspectorRequestDataTitle', { defaultMessage: 'Data', }); - const description = i18n('kbn.docTable.inspectorRequestDescription', { + const description = i18n.translate('kbn.docTable.inspectorRequestDescription', { defaultMessage: 'This request queries Elasticsearch to fetch the data for the search.', }); inspectorRequest = $scope.inspectorAdapters.requests.start(title, { description }); @@ -160,7 +160,11 @@ uiModules.get('app/discover') }) .then(onResults) .catch(error => { - notify.error(error); + toastNotifications.addError(error, { + title: i18n.translate('kbn.docTable.errorTitle', { + defaultMessage: 'Error fetching data' + }), + }); startSearching(); }); } diff --git a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts index c41092682283d..fe5b9994c574e 100644 --- a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts +++ b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts @@ -110,6 +110,10 @@ export class SearchEmbeddable extends Embeddable { return this.inspectorAdaptors; } + public getPanelTitle() { + return this.panelTitle; + } + public onContainerStateChanged(containerState: ContainerState) { this.customization = containerState.embeddableCustomization || {}; this.filters = containerState.filters; diff --git a/src/legacy/core_plugins/kibana/public/discover/index.html b/src/legacy/core_plugins/kibana/public/discover/index.html index f60d02689f51c..5b4b2559875dd 100644 --- a/src/legacy/core_plugins/kibana/public/discover/index.html +++ b/src/legacy/core_plugins/kibana/public/discover/index.html @@ -5,18 +5,21 @@
-

+

-   + > + + {{::'kbn.discover.reloadSavedSearchButton' | i18n: {defaultMessage: 'Reload'} }} +

diff --git a/src/legacy/core_plugins/kibana/public/discover/index.js b/src/legacy/core_plugins/kibana/public/discover/index.js index ed2c84ff8c2e5..def832107322d 100644 --- a/src/legacy/core_plugins/kibana/public/discover/index.js +++ b/src/legacy/core_plugins/kibana/public/discover/index.js @@ -18,6 +18,7 @@ */ import './saved_searches/saved_searches'; +import { i18n } from '@kbn/i18n'; import './directives'; import 'ui/collapsible_sidebar'; import './components/field_chooser/field_chooser'; @@ -25,13 +26,13 @@ import './controllers/discover'; import './doc_table/components/table_row'; import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue'; -FeatureCatalogueRegistryProvider.register(i18n => { +FeatureCatalogueRegistryProvider.register(() => { return { id: 'discover', - title: i18n('kbn.discover.discoverTitle', { + title: i18n.translate('kbn.discover.discoverTitle', { defaultMessage: 'Discover', }), - description: i18n('kbn.discover.discoverDescription', { + description: i18n.translate('kbn.discover.discoverDescription', { defaultMessage: 'Interactively explore your data by querying and filtering raw documents.', }), icon: 'discoverApp', diff --git a/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.js b/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.js index ca4e04749ef41..281c2e08d6ebe 100644 --- a/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.js +++ b/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.js @@ -18,6 +18,7 @@ */ import 'ui/notify'; +import { i18n } from '@kbn/i18n'; import { uiModules } from 'ui/modules'; import { createLegacyClass } from 'ui/utils/legacy_class'; import { SavedObjectProvider } from 'ui/saved_objects/saved_object'; @@ -27,7 +28,7 @@ const module = uiModules.get('discover/saved_searches', [ 'kibana/courier' ]); -module.factory('SavedSearch', function (Private, i18n) { +module.factory('SavedSearch', function (Private) { const SavedObject = Private(SavedObjectProvider); createLegacyClass(SavedSearch).inherits(SavedObject); function SavedSearch(id) { @@ -38,7 +39,7 @@ module.factory('SavedSearch', function (Private, i18n) { id: id, defaults: { - title: i18n('kbn.discover.savedSearch.newSavedSearchTitle', { + title: i18n.translate('kbn.discover.savedSearch.newSavedSearchTitle', { defaultMessage: 'New Saved Search', }), description: '', diff --git a/src/legacy/core_plugins/kibana/public/doc/__tests__/doc.js b/src/legacy/core_plugins/kibana/public/doc/__tests__/doc.js index 69ca21b2cf869..fff8b800337eb 100644 --- a/src/legacy/core_plugins/kibana/public/doc/__tests__/doc.js +++ b/src/legacy/core_plugins/kibana/public/doc/__tests__/doc.js @@ -46,7 +46,7 @@ const init = function (index, type, id) { }; }); - $provide.service('es', function (Private, $q) { + $provide.service('es', function ($q) { this.search = function (config) { const deferred = $q.defer(); diff --git a/src/legacy/core_plugins/kibana/public/doc_viewer/doc_viewer.js b/src/legacy/core_plugins/kibana/public/doc_viewer/doc_viewer.js index 82aff3d324602..cfc45e4af5251 100644 --- a/src/legacy/core_plugins/kibana/public/doc_viewer/doc_viewer.js +++ b/src/legacy/core_plugins/kibana/public/doc_viewer/doc_viewer.js @@ -24,7 +24,7 @@ import { DocViewsRegistryProvider } from 'ui/registry/doc_views'; import 'ui/render_directive'; uiModules.get('apps/discover') - .directive('docViewer', function (config, Private) { + .directive('docViewer', function (Private) { const docViews = Private(DocViewsRegistryProvider); return { restrict: 'E', diff --git a/src/legacy/core_plugins/kibana/public/field_formats/__tests__/_conformance.js b/src/legacy/core_plugins/kibana/public/field_formats/__tests__/_conformance.js index c62c90dbe4c14..cf0fde3d9a74e 100644 --- a/src/legacy/core_plugins/kibana/public/field_formats/__tests__/_conformance.js +++ b/src/legacy/core_plugins/kibana/public/field_formats/__tests__/_conformance.js @@ -29,6 +29,7 @@ const config = chrome.getUiSettingsClient(); const formatIds = [ 'bytes', 'date', + 'date_nanos', 'duration', 'ip', 'number', diff --git a/src/legacy/core_plugins/kibana/public/field_formats/__tests__/_date.js b/src/legacy/core_plugins/kibana/public/field_formats/__tests__/_date.js index 33b3a4d4c73cf..517fd24a9ddf2 100644 --- a/src/legacy/core_plugins/kibana/public/field_formats/__tests__/_date.js +++ b/src/legacy/core_plugins/kibana/public/field_formats/__tests__/_date.js @@ -28,7 +28,7 @@ describe('Date Format', function () { let off; beforeEach(ngMock.module('kibana')); - beforeEach(ngMock.inject(function (Private, config, $rootScope) { + beforeEach(ngMock.inject(function (config, $rootScope) { $scope = $rootScope; settings = config; diff --git a/src/legacy/core_plugins/kibana/public/field_formats/register.js b/src/legacy/core_plugins/kibana/public/field_formats/register.js index 545f25e50e90f..6855714354db8 100644 --- a/src/legacy/core_plugins/kibana/public/field_formats/register.js +++ b/src/legacy/core_plugins/kibana/public/field_formats/register.js @@ -21,6 +21,7 @@ import { fieldFormats } from 'ui/registry/field_formats'; import { createUrlFormat } from '../../common/field_formats/types/url'; import { createBytesFormat } from '../../common/field_formats/types/bytes'; import { createDateFormat } from '../../common/field_formats/types/date'; +import { createDateNanosFormat } from '../../common/field_formats/types/date_nanos'; import { createRelativeDateFormat } from '../../common/field_formats/types/relative_date'; import { createDurationFormat } from '../../common/field_formats/types/duration'; import { createIpFormat } from '../../common/field_formats/types/ip'; @@ -36,6 +37,7 @@ import { createStaticLookupFormat } from '../../common/field_formats/types/stati fieldFormats.register(createUrlFormat); fieldFormats.register(createBytesFormat); fieldFormats.register(createDateFormat); +fieldFormats.register(createDateNanosFormat); fieldFormats.register(createRelativeDateFormat); fieldFormats.register(createDurationFormat); fieldFormats.register(createIpFormat); diff --git a/src/legacy/core_plugins/kibana/public/home/components/home.test.js b/src/legacy/core_plugins/kibana/public/home/components/home.test.js index 5de9c883d37b2..c10aa3f0b1e32 100644 --- a/src/legacy/core_plugins/kibana/public/home/components/home.test.js +++ b/src/legacy/core_plugins/kibana/public/home/components/home.test.js @@ -32,6 +32,15 @@ jest.mock( { virtual: true } ); +jest.mock( + 'ui/capabilities', + () => ({ + catalogue: {}, + management: {}, + navLinks: {} + }) +); + describe('home', () => { let defaultProps; 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 05d968c3d4e3f..82d2698b42e9c 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/home/index.js b/src/legacy/core_plugins/kibana/public/home/index.js index 0b886f7c67855..b8e9bd3bddf84 100644 --- a/src/legacy/core_plugins/kibana/public/home/index.js +++ b/src/legacy/core_plugins/kibana/public/home/index.js @@ -39,7 +39,7 @@ const homeTitle = i18n.translate('kbn.home.breadcrumbs.homeTitle', { defaultMess function getRoute() { return { template, - controller($scope, config, indexPatterns, Private) { + controller($scope, Private) { $scope.directories = Private(FeatureCatalogueRegistryProvider).inTitleOrder; $scope.recentlyAccessed = recentlyAccessed.get().map(item => { item.link = chrome.addBasePath(item.link); diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/cisco_logs/screenshot.png b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/cisco_logs/screenshot.png new file mode 100644 index 0000000000000..6108b1a99ecfd Binary files /dev/null and b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/cisco_logs/screenshot.png differ diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/envoyproxy_logs/screenshot.png b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/envoyproxy_logs/screenshot.png new file mode 100644 index 0000000000000..87f589b4e3c66 Binary files /dev/null and b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/envoyproxy_logs/screenshot.png differ diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/iptables_logs/screenshot.png b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/iptables_logs/screenshot.png new file mode 100644 index 0000000000000..e01ba5275c762 Binary files /dev/null and b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/iptables_logs/screenshot.png differ diff --git a/src/legacy/core_plugins/kibana/public/management/index.js b/src/legacy/core_plugins/kibana/public/management/index.js index f188907ab0108..e4819f5d34113 100644 --- a/src/legacy/core_plugins/kibana/public/management/index.js +++ b/src/legacy/core_plugins/kibana/public/management/index.js @@ -18,6 +18,7 @@ */ import React from 'react'; +import { i18n } from '@kbn/i18n'; import { render, unmountComponentAtNode } from 'react-dom'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -126,7 +127,7 @@ export const destroyReact = id => { uiModules .get('apps/management') - .directive('kbnManagementApp', function (Private, $location) { + .directive('kbnManagementApp', function ($location) { return { restrict: 'E', template: appTemplate, @@ -171,13 +172,13 @@ uiModules }; }); -FeatureCatalogueRegistryProvider.register(i18n => { +FeatureCatalogueRegistryProvider.register(() => { return { id: 'management', - title: i18n('kbn.management.managementLabel', { + title: i18n.translate('kbn.management.managementLabel', { defaultMessage: 'Management', }), - description: i18n('kbn.management.managementDescription', { + description: i18n.translate('kbn.management.managementDescription', { defaultMessage: 'Your center console for managing the Elastic Stack.', }), icon: 'managementApp', diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_time_field/step_time_field.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_time_field/step_time_field.js index 0d1d4515d8dca..d500924376df3 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_time_field/step_time_field.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_time_field/step_time_field.js @@ -127,12 +127,9 @@ export class StepTimeFieldComponent extends Component { } catch (error) { if (!this.mounted) return; this.setState({ - error: error instanceof Error ? error.message : String(error) + error: error instanceof Error ? error.message : String(error), + isCreating: false, }); - } finally { - if (this.mounted) { - this.setState({ isCreating: false }); - } } } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.js index 3b1457046a5eb..5bf791d4a662c 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.js @@ -36,6 +36,7 @@ import { ensureMinimumTime, getIndices, } from './lib'; +import { i18n } from '@kbn/i18n'; export class CreateIndexPatternWizard extends Component { static propTypes = { @@ -130,6 +131,16 @@ export class CreateIndexPatternWizard extends Component { }); const createdId = await emptyPattern.create(); + if (!createdId) { + const confirmMessage = i18n.translate('kbn.management.indexPattern.titleExistsLabel', { values: { title: this.title }, + defaultMessage: 'An index pattern with the title \'{title}\' already exists.' }); + try { + await services.confirmModalPromise(confirmMessage, { confirmButtonText: 'Go to existing pattern' }); + return services.changeUrl(`/management/kibana/index_patterns/${indexPatternId}`); + } catch (err) { + return false; + } + } if (!services.config.get('defaultIndex')) { await services.config.set('defaultIndex', createdId); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/index.js index 3a34a1a8af1ef..b500f5c79e98b 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/index.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/index.js @@ -44,6 +44,7 @@ uiRoutes.when('/management/kibana/index_pattern', { $http: $injector.get('$http'), savedObjectsClient: Private(SavedObjectsClientProvider), indexPatternCreationType, + confirmModalPromise: $injector.get('confirmModalPromise'), changeUrl: url => { $scope.$evalAsync(() => kbnUrl.changePath(url)); }, 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 8461271d9c148..ca04ac8fcfaab 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 07d5cc73537b4..9de71f808d00a 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'; @@ -86,16 +85,16 @@ uiRoutes .defaults(/management\/kibana\/(index_patterns|index_pattern)/, { resolve: indexPatternsResolutions, requireUICapability: 'management.kibana.index_patterns', - badge: (i18n, uiCapabilities) => { + badge: uiCapabilities => { if (uiCapabilities.indexPatterns.save) { return undefined; } return { - text: i18n('kbn.management.indexPatterns.badge.readOnly.text', { + text: i18n.translate('kbn.management.indexPatterns.badge.readOnly.text', { defaultMessage: 'Read only', }), - tooltip: i18n('kbn.management.indexPatterns.badge.readOnly.tooltip', { + tooltip: i18n.translate('kbn.management.indexPatterns.badge.readOnly.tooltip', { defaultMessage: 'Unable to save index patterns', }), iconType: 'glasses' @@ -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); - - 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; - }) || []; + $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, + 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 79ff3d8c624d0..2e4134bf9b6b4 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 })} /> )} - + - +

{ $injector.get(obj.service)); - const uiCapabilites = getNewPlatform().start.core.application.capabilities; + const uiCapabilites = npStart.core.application.capabilities; $scope.$$postDigest(() => { const node = document.getElementById(REACT_OBJECTS_TABLE_DOM_ELEMENT_ID); @@ -55,6 +56,7 @@ function updateObjectsTable($scope, $injector) { {}; $http.post = jest.fn().mockImplementation(() => ([])); const defaultProps = { goInspectObject: () => {}, + confirmModalPromise: jest.fn(), savedObjectsClient: { find: jest.fn(), bulkGet: jest.fn(), @@ -133,26 +134,12 @@ const defaultProps = { services: [], uiCapabilities: { savedObjectsManagement: { - 'index-pattern': { - read: true - }, - visualization: { - read: true - }, - dashboard: { - read: true - }, - search: { - read: true - } + read: true, + edit: false, + delete: false, } }, - canDeleteSavedObjectTypes: [ - 'index-pattern', - 'visualization', - 'dashboard', - 'search' - ] + canDelete: true, }; beforeEach(() => { @@ -271,41 +258,6 @@ describe('ObjectsTable', () => { expect(addDangerMock).toHaveBeenCalled(); }); - it('should filter find operation based on the uiCapabilities', async () => { - const uiCapabilities = { - savedObjectsManagement: { - 'index-pattern': { - read: false, - }, - visualization: { - read: false, - }, - dashboard: { - read: false, - }, - search: { - read: true, - } - } - }; - const customizedProps = { ...defaultProps, uiCapabilities }; - const component = shallowWithIntl( - - ); - - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - expect(findObjects).toHaveBeenCalledWith(expect.objectContaining({ - type: ['search'] - })); - }); - describe('export', () => { it('should export selected objects', async () => { const mockSelectedSavedObjects = [ @@ -451,42 +403,6 @@ describe('ObjectsTable', () => { expect(getRelationships).toHaveBeenCalledWith('search', '1', savedObjectTypes, defaultProps.$http, defaultProps.basePath); }); - it('should fetch relationships filtered based on the uiCapabilities', async () => { - const { getRelationships } = require('../../../lib/get_relationships'); - - const uiCapabilities = { - savedObjectsManagement: { - 'index-pattern': { - read: false, - }, - visualization: { - read: false, - }, - dashboard: { - read: false, - }, - search: { - read: true, - } - } - }; - const customizedProps = { ...defaultProps, uiCapabilities }; - const component = shallowWithIntl( - - ); - - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - await component.instance().getRelationships('search', '1'); - const savedObjectTypes = ['search']; - expect(getRelationships).toHaveBeenCalledWith('search', '1', savedObjectTypes, defaultProps.$http, defaultProps.basePath); - }); - it('should show the flyout', async () => { const component = shallowWithIntl( [{ id: '1' }, { id: '2' }]), }, @@ -433,6 +434,7 @@ describe('Flyout', () => { conflictedSavedObjectsLinkedToSavedSearches: mockConflictedSavedObjectsLinkedToSavedSearches, conflictedSearchDocs: mockConflictedSearchDocs, importedObjectCount: 2, + confirmModalPromise: () => {}, })); }); @@ -453,7 +455,8 @@ describe('Flyout', () => { mockData.slice(0, 2).map((doc) => ({ ...doc, _migrationVersion: {} })), true, defaultProps.services, - defaultProps.indexPatterns + defaultProps.indexPatterns, + defaultProps.confirmModalPromise, ); expect(component.state()).toMatchObject({ diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js index 80dda4e062637..dc4eaa9360ecd 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js @@ -69,6 +69,7 @@ class FlyoutUI extends Component { services: PropTypes.array.isRequired, newIndexPatternUrl: PropTypes.string.isRequired, indexPatterns: PropTypes.object.isRequired, + confirmModalPromise: PropTypes.func.isRequired, }; constructor(props) { @@ -213,7 +214,7 @@ class FlyoutUI extends Component { } legacyImport = async () => { - const { services, indexPatterns, intl } = this.props; + const { services, indexPatterns, intl, confirmModalPromise } = this.props; const { file, isOverwriteAllChecked } = this.state; this.setState({ status: 'loading', error: undefined }); @@ -268,7 +269,8 @@ class FlyoutUI extends Component { contents, isOverwriteAllChecked, services, - indexPatterns + indexPatterns, + confirmModalPromise ); const byId = groupBy(conflictedIndexPatterns, ({ obj }) => diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/__snapshots__/table.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/__snapshots__/table.test.js.snap index bc2eed7284056..7ec3b7c55afae 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/__snapshots__/table.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/__snapshots__/table.test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Table restricts which saved objects can be deleted based on type 1`] = ` +exports[`Table prevents saved objects from being deleted 1`] = ` {}, isSearching: false, onShowRelationships: () => {}, - canDeleteSavedObjectTypes: ['visualization'] + canDelete: true }; describe('Table', () => { @@ -127,9 +127,9 @@ describe('Table', () => { expect(component.state().isSearchTextValid).toBe(true); }); - it(`restricts which saved objects can be deleted based on type`, () => { + it(`prevents saved objects from being deleted`, () => { const selectedSavedObjects = [{ type: 'visualization' }, { type: 'search' }, { type: 'index-pattern' }]; - const customizedProps = { ...defaultProps, selectedSavedObjects, canDeleteSavedObjectTypes: ['visualization'] }; + const customizedProps = { ...defaultProps, selectedSavedObjects, canDelete: false }; const component = shallowWithIntl( type) - .filter(type => !this.props.canDeleteSavedObjectTypes.includes(type)); - const button = ( 0 + !this.props.canDelete } title={ - unableToDeleteSavedObjectTypes.length > 0 ? `Unable to delete ${unableToDeleteSavedObjectTypes.join(', ')}` : undefined + this.props.canDelete + ? undefined + : i18n.translate( + 'kbn.management.objects.objectsTable.table.deleteButtonTitle', + { defaultMessage: 'Unable to delete saved objects' } + ) } data-test-subj="savedObjectsManagementDelete" > diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/objects_table.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/objects_table.js index 9ac57deba951e..9c1f1a84e1bb6 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/objects_table.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/objects_table.js @@ -73,6 +73,7 @@ class ObjectsTableUI extends Component { basePath: PropTypes.string.isRequired, perPageConfig: PropTypes.number, newIndexPatternUrl: PropTypes.string.isRequired, + confirmModalPromise: PropTypes.func.isRequired, services: PropTypes.array.isRequired, uiCapabilities: PropTypes.object.isRequired, goInspectObject: PropTypes.func.isRequired, @@ -81,9 +82,7 @@ class ObjectsTableUI extends Component { constructor(props) { super(props); - this.savedObjectTypes = POSSIBLE_TYPES.filter(type => { - return this.props.uiCapabilities.savedObjectsManagement[type].read; - }); + this.savedObjectTypes = POSSIBLE_TYPES; this.state = { totalCount: 0, @@ -418,6 +417,7 @@ class ObjectsTableUI extends Component { indexPatterns={this.props.indexPatterns} newIndexPatternUrl={this.props.newIndexPatternUrl} savedObjectTypes={this.props.savedObjectTypes} + confirmModalPromise={this.props.confirmModalPromise} /> ); } @@ -675,10 +675,6 @@ class ObjectsTableUI extends Component { view: `${type} (${savedObjectCounts[type] || 0})`, })); - const canDeleteSavedObjectTypes = POSSIBLE_TYPES.filter(type => { - return this.props.uiCapabilities.savedObjectsManagement[type].delete; - }); - return ( { + badge: uiCapabilities => { if (uiCapabilities.advancedSettings.save) { return undefined; } return { - text: i18n('kbn.management.advancedSettings.badge.readOnly.text', { + text: i18n.translate('kbn.management.advancedSettings.badge.readOnly.text', { defaultMessage: 'Read only', }), - tooltip: i18n('kbn.management.advancedSettings.badge.readOnly.tooltip', { + tooltip: i18n.translate('kbn.management.advancedSettings.badge.readOnly.tooltip', { defaultMessage: 'Unable to save advanced settings', }), iconType: 'glasses' @@ -106,13 +106,13 @@ management.getSection('kibana').register('settings', { url: '#/management/kibana/settings' }); -FeatureCatalogueRegistryProvider.register(i18n => { +FeatureCatalogueRegistryProvider.register(() => { return { id: 'advanced_settings', - title: i18n('kbn.management.settings.advancedSettingsLabel', { + title: i18n.translate('kbn.management.settings.advancedSettingsLabel', { defaultMessage: 'Advanced Settings', }), - description: i18n('kbn.management.settings.advancedSettingsDescription', { + description: i18n.translate('kbn.management.settings.advancedSettingsDescription', { defaultMessage: 'Directly edit settings that control behavior in Kibana.', }), icon: 'advancedSettingsApp', diff --git a/src/legacy/core_plugins/kibana/public/selectors/dashboard_selectors.ts b/src/legacy/core_plugins/kibana/public/selectors/dashboard_selectors.ts index d4e1d436c6cab..388483c5c1c03 100644 --- a/src/legacy/core_plugins/kibana/public/selectors/dashboard_selectors.ts +++ b/src/legacy/core_plugins/kibana/public/selectors/dashboard_selectors.ts @@ -17,11 +17,13 @@ * under the License. */ -import { Filters, Query, TimeRange } from 'ui/embeddable'; +import { Query, TimeRange } from 'ui/embeddable'; +import { Filter } from '@kbn/es-query'; import { DashboardViewMode } from '../dashboard/dashboard_view_mode'; import * as DashboardSelectors from '../dashboard/selectors'; import { PanelId } from '../dashboard/selectors/types'; import { CoreKibanaState } from './types'; +import { StagedFilter } from '../dashboard/types'; export const getDashboard = (state: CoreKibanaState): DashboardSelectors.DashboardState => state.dashboard; @@ -46,7 +48,7 @@ export const getEmbeddableStagedFilter = (state: CoreKibanaState, panelId: Panel export const getEmbeddableMetadata = (state: CoreKibanaState, panelId: PanelId) => DashboardSelectors.getEmbeddableMetadata(getDashboard(state), panelId); -export const getStagedFilters = (state: CoreKibanaState): Filters => +export const getStagedFilters = (state: CoreKibanaState): StagedFilter[] => DashboardSelectors.getStagedFilters(getDashboard(state)); export const getViewMode = (state: CoreKibanaState): DashboardViewMode => DashboardSelectors.getViewMode(getDashboard(state)); @@ -60,7 +62,7 @@ export const getHidePanelTitles = (state: CoreKibanaState): boolean => DashboardSelectors.getHidePanelTitles(getDashboard(state)); export const getTimeRange = (state: CoreKibanaState): TimeRange => DashboardSelectors.getTimeRange(getDashboard(state)); -export const getFilters = (state: CoreKibanaState): Filters => +export const getFilters = (state: CoreKibanaState): Filter[] => DashboardSelectors.getFilters(getDashboard(state)); export const getQuery = (state: CoreKibanaState): Query => DashboardSelectors.getQuery(getDashboard(state)); diff --git a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js index fe5df3a8de54d..e1ec84aee8754 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js @@ -18,6 +18,7 @@ */ import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; import '../saved_visualizations/saved_visualizations'; import './visualization_editor'; import 'ui/vis/editors/default/sidebar'; @@ -25,8 +26,6 @@ import 'ui/visualize'; import 'ui/collapsible_sidebar'; import { capabilities } from 'ui/capabilities'; -import 'ui/apply_filters'; -import 'ui/listen'; import chrome from 'ui/chrome'; import React from 'react'; import angular from 'angular'; @@ -34,7 +33,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { toastNotifications } from 'ui/notify'; import { VisTypesRegistryProvider } from 'ui/registry/vis_types'; import { DocTitleProvider } from 'ui/doc_title'; -import { FilterBarQueryFilterProvider } from 'ui/filter_bar/query_filter'; +import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter'; import { stateMonitorFactory } from 'ui/state_management/state_monitor_factory'; import { migrateAppState } from './lib'; import uiRoutes from 'ui/routes'; @@ -53,24 +52,22 @@ import { getUnhashableStatesProvider } from 'ui/state_management/state_hashing'; import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; import { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal'; import { getEditBreadcrumbs, getCreateBreadcrumbs } from '../breadcrumbs'; -import { getNewPlatform } from 'ui/new_platform'; +import { npStart } from 'ui/new_platform'; -import { data } from 'plugins/data'; -data.search.loadLegacyDirectives(); uiRoutes .when(VisualizeConstants.CREATE_PATH, { template: editorTemplate, k7Breadcrumbs: getCreateBreadcrumbs, resolve: { - savedVis: function (savedVisualizations, redirectWhenMissing, $route, Private, i18n) { + savedVis: function (savedVisualizations, redirectWhenMissing, $route, Private) { const visTypes = Private(VisTypesRegistryProvider); const visType = _.find(visTypes, { name: $route.current.params.type }); const shouldHaveIndex = visType.requiresSearch && visType.options.showIndexSelection; const hasIndex = $route.current.params.indexPattern || $route.current.params.savedSearchId; if (shouldHaveIndex && !hasIndex) { throw new Error( - i18n('kbn.visualize.createVisualization.noIndexPatternOrSavedSearchIdErrorMessage', { + i18n.translate('kbn.visualize.createVisualization.noIndexPatternOrSavedSearchIdErrorMessage', { defaultMessage: 'You must provide either an indexPattern or a savedSearchId', }) ); @@ -133,8 +130,7 @@ function VisEditor( Promise, config, kbnBaseUrl, - localStorage, - i18n + localStorage ) { const docTitle = Private(DocTitleProvider); const queryFilter = Private(FilterBarQueryFilterProvider); @@ -154,8 +150,8 @@ function VisEditor( }; $scope.topNavMenu = [...(capabilities.get().visualize.save ? [{ - key: i18n('kbn.topNavMenu.saveVisualizationButtonLabel', { defaultMessage: 'save' }), - description: i18n('kbn.visualize.topNavMenu.saveVisualizationButtonAriaLabel', { + key: i18n.translate('kbn.topNavMenu.saveVisualizationButtonLabel', { defaultMessage: 'save' }), + description: i18n.translate('kbn.visualize.topNavMenu.saveVisualizationButtonAriaLabel', { defaultMessage: 'Save Visualization', }), testId: 'visualizeSaveButton', @@ -164,7 +160,7 @@ function VisEditor( }, tooltip() { if (vis.dirty) { - return i18n('kbn.visualize.topNavMenu.saveVisualizationDisabledButtonTooltip', { + return i18n.translate('kbn.visualize.topNavMenu.saveVisualizationDisabledButtonTooltip', { defaultMessage: 'Apply or Discard your changes before saving' }); } @@ -207,8 +203,8 @@ function VisEditor( showSaveModal(saveModal); } }] : []), { - key: i18n('kbn.topNavMenu.shareVisualizationButtonLabel', { defaultMessage: 'share' }), - description: i18n('kbn.visualize.topNavMenu.shareVisualizationButtonAriaLabel', { + key: i18n.translate('kbn.topNavMenu.shareVisualizationButtonLabel', { defaultMessage: 'share' }), + description: i18n.translate('kbn.visualize.topNavMenu.shareVisualizationButtonAriaLabel', { defaultMessage: 'Share Visualization', }), testId: 'shareTopNavButton', @@ -230,8 +226,8 @@ function VisEditor( }); } }, { - key: i18n('kbn.topNavMenu.openInspectorButtonLabel', { defaultMessage: 'inspect' }), - description: i18n('kbn.visualize.topNavMenu.openInspectorButtonAriaLabel', { + key: i18n.translate('kbn.topNavMenu.openInspectorButtonLabel', { defaultMessage: 'inspect' }), + description: i18n.translate('kbn.visualize.topNavMenu.openInspectorButtonAriaLabel', { defaultMessage: 'Open Inspector for visualization', }), testId: 'openInspectorButton', @@ -247,14 +243,14 @@ function VisEditor( }, tooltip() { if (!vis.hasInspector || !vis.hasInspector()) { - return i18n('kbn.visualize.topNavMenu.openInspectorDisabledButtonTooltip', { + return i18n.translate('kbn.visualize.topNavMenu.openInspectorDisabledButtonTooltip', { defaultMessage: `This visualization doesn't support any inspectors.`, }); } } }, { - key: i18n('kbn.topNavMenu.refreshButtonLabel', { defaultMessage: 'refresh' }), - description: i18n('kbn.visualize.topNavMenu.refreshButtonAriaLabel', { + key: i18n.translate('kbn.topNavMenu.refreshButtonLabel', { defaultMessage: 'refresh' }), + description: i18n.translate('kbn.visualize.topNavMenu.refreshButtonAriaLabel', { defaultMessage: 'Refresh', }), run: function () { @@ -420,10 +416,12 @@ function VisEditor( $scope.$listenAndDigestAsync(timefilter, 'refreshIntervalUpdate', updateRefreshInterval); // update the searchSource when filters update - $scope.$listen(queryFilter, 'update', function () { - $scope.filters = queryFilter.getFilters(); - $scope.fetch(); - }); + const filterUpdateSubscription = queryFilter.getUpdates$().subscribe( + () => { + $scope.filters = queryFilter.getFilters(); + $scope.fetch(); + }, + ); // update the searchSource when query updates $scope.fetch = function () { @@ -440,6 +438,7 @@ function VisEditor( } savedVis.destroy(); stateMonitor.destroy(); + filterUpdateSubscription.unsubscribe(); }); if (!$scope.chrome.getVisible()) { @@ -484,7 +483,7 @@ function VisEditor( if (id) { toastNotifications.addSuccess({ - title: i18n('kbn.visualize.topNavMenu.saveVisualization.successNotificationText', { + title: i18n.translate('kbn.visualize.topNavMenu.saveVisualization.successNotificationText', { defaultMessage: `Saved '{visTitle}'`, values: { visTitle: savedVis.title, @@ -506,7 +505,7 @@ function VisEditor( // url, not the unsaved one. chrome.trackSubUrlForApp('kibana:visualize', savedVisualizationParsedUrl); - const lastDashboardAbsoluteUrl = getNewPlatform().start.core.chrome.navLinks.get('kibana:dashboard').url; + const lastDashboardAbsoluteUrl = npStart.core.chrome.navLinks.get('kibana:dashboard').url; const dashboardParsedUrl = absoluteToParsedUrl(lastDashboardAbsoluteUrl, chrome.getBasePath()); dashboardParsedUrl.addQueryParameter(DashboardConstants.NEW_VISUALIZATION_ID_PARAM, savedVis.id); kbnUrl.change(dashboardParsedUrl.appPath); @@ -523,7 +522,7 @@ function VisEditor( // eslint-disable-next-line console.error(error); toastNotifications.addDanger({ - title: i18n('kbn.visualize.topNavMenu.saveVisualization.failureNotificationText', { + title: i18n.translate('kbn.visualize.topNavMenu.saveVisualization.failureNotificationText', { defaultMessage: `Error on saving '{visTitle}'`, values: { visTitle: savedVis.title, @@ -553,7 +552,7 @@ function VisEditor( searchSource.setParent(searchSourceGrandparent); toastNotifications.addSuccess( - i18n('kbn.visualize.linkedToSearch.unlinkSuccessNotificationText', { + i18n.translate('kbn.visualize.linkedToSearch.unlinkSuccessNotificationText', { defaultMessage: `Unlinked from saved search '{searchTitle}'`, values: { searchTitle: savedVis.savedSearch.title @@ -566,12 +565,10 @@ function VisEditor( $scope.getAdditionalMessage = () => { - return ( - '' + - i18n('kbn.visualize.experimentalVisInfoText', { defaultMessage: 'This visualization is marked as experimental.' }) + - ' ' + - vis.type.feedbackMessage - ); + return '' + + i18n.translate('kbn.visualize.experimentalVisInfoText', { defaultMessage: 'This visualization is marked as experimental.' }) + + ' ' + + vis.type.feedbackMessage; }; init(); diff --git a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts index d284bda9405ab..7529f1e92678d 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts @@ -180,7 +180,7 @@ export class VisualizeEmbeddable extends Embeddable { timeRange: containerState.timeRange, query: containerState.query, filters: containerState.filters, - cssClass: `panel-content panel-content--fullWidth`, + cssClass: `embPanel__content embPanel__content--fullWidth`, dataAttrs, }; diff --git a/src/legacy/core_plugins/kibana/public/visualize/index.js b/src/legacy/core_plugins/kibana/public/visualize/index.js index fda4301490dec..26b6af9899b76 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/index.js +++ b/src/legacy/core_plugins/kibana/public/visualize/index.js @@ -18,9 +18,9 @@ */ import './editor/editor'; +import { i18n } from '@kbn/i18n'; import './saved_visualizations/_saved_vis'; import './saved_visualizations/saved_visualizations'; -import 'ui/filter_bar'; import uiRoutes from 'ui/routes'; import 'ui/capabilities/route_setup'; import visualizeListingTemplate from './listing/visualize_listing.html'; @@ -29,20 +29,24 @@ import { VisualizeConstants } from './visualize_constants'; import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue'; import { getLandingBreadcrumbs, getWizardStep1Breadcrumbs } from './breadcrumbs'; +import { data } from 'plugins/data'; +data.search.loadLegacyDirectives(); +data.filter.loadLegacyDirectives(); + uiRoutes .defaults(/visualize/, { requireDefaultIndex: true, requireUICapability: 'visualize.show', - badge: (i18n, uiCapabilities) => { + badge: uiCapabilities => { if (uiCapabilities.visualize.save) { return undefined; } return { - text: i18n('kbn.visualize.badge.readOnly.text', { + text: i18n.translate('kbn.visualize.badge.readOnly.text', { defaultMessage: 'Read only', }), - tooltip: i18n('kbn.visualize.badge.readOnly.tooltip', { + tooltip: i18n.translate('kbn.visualize.badge.readOnly.tooltip', { defaultMessage: 'Unable to save visualizations', }), iconType: 'glasses' @@ -68,11 +72,11 @@ uiRoutes }, }); -FeatureCatalogueRegistryProvider.register(i18n => { +FeatureCatalogueRegistryProvider.register(() => { return { id: 'visualize', title: 'Visualize', - description: i18n( + description: i18n.translate( 'kbn.visualize.visualizeDescription', { defaultMessage: 'Create visualizations and aggregate data stores in your Elasticsearch indices.', diff --git a/src/legacy/core_plugins/kibana/public/visualize/listing/visualize_listing.html b/src/legacy/core_plugins/kibana/public/visualize/listing/visualize_listing.html index f877504b38658..edb7cccbd46a2 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/listing/visualize_listing.html +++ b/src/legacy/core_plugins/kibana/public/visualize/listing/visualize_listing.html @@ -1,41 +1,13 @@
-
-
-
-
- - . -
-
-
-
- reactDirective(wrapInI1 app.directive('newVisModal', reactDirective => reactDirective(wrapInI18nContext(NewVisModal))); export function VisualizeListingController($injector, createNewVis) { - const Notifier = $injector.get('Notifier'); const Private = $injector.get('Private'); const config = $injector.get('config'); const kbnUrl = $injector.get('kbnUrl'); @@ -77,15 +77,12 @@ export function VisualizeListingController($injector, createNewVis) { // TODO: Extract this into an external service. const services = Private(SavedObjectRegistryProvider).byLoaderPropertiesName; const visualizationService = services.visualizations; - const notify = new Notifier({ location: 'Visualize' }); this.fetchItems = (filter) => { const isLabsEnabled = config.get('visualize:enableLabs'); return visualizationService.find(filter, config.get('savedObjects:listingLimit')) .then(result => { this.totalItems = result.total; - this.showLimitError = result.total > config.get('savedObjects:listingLimit'); - this.listingLimit = config.get('savedObjects:listingLimit'); return { total: result.total, @@ -96,7 +93,13 @@ export function VisualizeListingController($injector, createNewVis) { this.deleteSelectedItems = function deleteSelectedItems(selectedIds) { return visualizationService.delete(selectedIds) - .catch(error => notify.error(error)); + .catch(error => { + toastNotifications.addError(error, { + title: i18n.translate('kbn.visualize.visualizeListingDeleteErrorTitle', { + defaultMessage: 'Error deleting visualization', + }), + }); + }); }; chrome.breadcrumbs.set([{ @@ -104,4 +107,6 @@ export function VisualizeListingController($injector, createNewVis) { defaultMessage: 'Visualize', }) }]); + + this.listingLimit = config.get('savedObjects:listingLimit'); } diff --git a/src/legacy/core_plugins/kibana/public/visualize/listing/visualize_listing_table.js b/src/legacy/core_plugins/kibana/public/visualize/listing/visualize_listing_table.js index dfa7f0048bb05..6c59c820e0897 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/listing/visualize_listing_table.js +++ b/src/legacy/core_plugins/kibana/public/visualize/listing/visualize_listing_table.js @@ -49,7 +49,7 @@ class VisualizeListingTableUi extends Component { deleteItems={capabilities.get().visualize.delete ? this.props.deleteItems : null} editItem={capabilities.get().visualize.save ? this.props.editItem : null} tableColumns={this.getTableColumns()} - listingLimit={100} + listingLimit={this.props.listingLimit} initialFilter={''} noItemsFragment={this.getNoItemsMessage()} entityName={ @@ -222,6 +222,7 @@ VisualizeListingTableUi.propTypes = { createItem: PropTypes.func.isRequired, getViewUrl: PropTypes.func.isRequired, editItem: PropTypes.func.isRequired, + listingLimit: PropTypes.number.isRequired, }; export const VisualizeListingTable = injectI18n(VisualizeListingTableUi); diff --git a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js index 0c0eb9d892c15..fd13e458caeaa 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js +++ b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js @@ -26,6 +26,7 @@ */ import { VisProvider } from 'ui/vis'; +import { i18n } from '@kbn/i18n'; import { uiModules } from 'ui/modules'; import { updateOldState } from 'ui/vis/vis_update_state'; import { VisualizeConstants } from '../visualize_constants'; @@ -38,7 +39,7 @@ import { uiModules .get('app/visualize') - .factory('SavedVis', function (config, $injector, Promise, savedSearches, Private, i18n) { + .factory('SavedVis', function (Promise, savedSearches, Private) { const Vis = Private(VisProvider); const SavedObject = Private(SavedObjectProvider); createLegacyClass(SavedVis).inherits(SavedObject); @@ -57,7 +58,7 @@ uiModules id: opts.id, indexPattern: opts.indexPattern, defaults: { - title: i18n('kbn.visualize.defaultVisualizationTitle', { + title: i18n.translate('kbn.visualize.defaultVisualizationTitle', { defaultMessage: 'New Visualization', }), visState: (function () { diff --git a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js index f306fb3f423b7..dee8cd7fda9ab 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js +++ b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js @@ -32,7 +32,7 @@ savedObjectManagementRegistry.register({ title: 'visualizations' }); -app.service('savedVisualizations', function (Promise, kbnIndex, SavedVis, Private, kbnUrl, $http, chrome) { +app.service('savedVisualizations', function (SavedVis, Private, kbnUrl, chrome) { const visTypes = Private(VisTypesRegistryProvider); const savedObjectClient = Private(SavedObjectsClientProvider); diff --git a/src/legacy/core_plugins/kibana/public/visualize/wizard/__snapshots__/new_vis_modal.test.tsx.snap b/src/legacy/core_plugins/kibana/public/visualize/wizard/__snapshots__/new_vis_modal.test.tsx.snap index 12ef4a16924ba..37168803bb69d 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/wizard/__snapshots__/new_vis_modal.test.tsx.snap +++ b/src/legacy/core_plugins/kibana/public/visualize/wizard/__snapshots__/new_vis_modal.test.tsx.snap @@ -174,17 +174,13 @@ exports[`NewVisModal filter for visualization types should render as expected 1` > + />
+ />
@@ -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` >
@@ -1224,27 +1155,23 @@ exports[`NewVisModal filter for visualization types should render as expected 1` size="l" type="empty" > - + @@ -1322,27 +1249,23 @@ exports[`NewVisModal filter for visualization types should render as expected 1` size="l" type="empty" > - + @@ -1618,17 +1541,13 @@ exports[`NewVisModal 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`] = ` >
@@ -2647,27 +2501,23 @@ exports[`NewVisModal should render as expected 1`] = ` size="l" type="empty" > - + @@ -2745,27 +2595,23 @@ exports[`NewVisModal should render as expected 1`] = ` size="l" type="empty" > - + diff --git a/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/make_kql_usage_collector.js b/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/make_kql_usage_collector.js index 534cc266d83a3..19fb64b7ecc74 100644 --- a/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/make_kql_usage_collector.js +++ b/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/make_kql_usage_collector.js @@ -25,6 +25,7 @@ export function makeKQLUsageCollector(server) { const kqlUsageCollector = server.usage.collectorSet.makeUsageCollector({ type: 'kql', fetch, + isReady: () => true, }); server.usage.collectorSet.register(kqlUsageCollector); diff --git a/src/legacy/core_plugins/kibana/server/tutorials/apm/envs/elastic_cloud.js b/src/legacy/core_plugins/kibana/server/tutorials/apm/envs/elastic_cloud.js index ba56aed215ef2..f1425dd642f42 100644 --- a/src/legacy/core_plugins/kibana/server/tutorials/apm/envs/elastic_cloud.js +++ b/src/legacy/core_plugins/kibana/server/tutorials/apm/envs/elastic_cloud.js @@ -29,6 +29,7 @@ import { createJsAgentInstructions, createGoAgentInstructions, createJavaAgentInstructions, + createDotNetAgentInstructions, } from '../instructions/apm_agent_instructions'; function getIfExists(config, key) { @@ -117,6 +118,10 @@ function getApmAgentInstructionSet(config) { id: INSTRUCTION_VARIANT.JAVA, instructions: createJavaAgentInstructions(apmServerUrl, secretToken), }, + { + id: INSTRUCTION_VARIANT.DOTNET, + instructions: createDotNetAgentInstructions(apmServerUrl, secretToken), + }, ], }; } diff --git a/src/legacy/core_plugins/kibana/server/tutorials/apm/envs/on_prem.js b/src/legacy/core_plugins/kibana/server/tutorials/apm/envs/on_prem.js index 1537a027e9296..9fd61770092b1 100644 --- a/src/legacy/core_plugins/kibana/server/tutorials/apm/envs/on_prem.js +++ b/src/legacy/core_plugins/kibana/server/tutorials/apm/envs/on_prem.js @@ -37,6 +37,7 @@ import { createJsAgentInstructions, createGoAgentInstructions, createJavaAgentInstructions, + createDotNetAgentInstructions, } from '../instructions/apm_agent_instructions'; export function onPremInstructions(config) { @@ -146,6 +147,10 @@ export function onPremInstructions(config) { id: INSTRUCTION_VARIANT.GO, instructions: createGoAgentInstructions(), }, + { + id: INSTRUCTION_VARIANT.DOTNET, + instructions: createDotNetAgentInstructions(), + }, ], statusCheck: { title: i18n.translate('kbn.server.tutorials.apm.apmAgents.statusCheck.title', { diff --git a/src/legacy/core_plugins/kibana/server/tutorials/apm/index_pattern.json b/src/legacy/core_plugins/kibana/server/tutorials/apm/index_pattern.json index ef1f6223e3144..214d5de458fa5 100644 --- a/src/legacy/core_plugins/kibana/server/tutorials/apm/index_pattern.json +++ b/src/legacy/core_plugins/kibana/server/tutorials/apm/index_pattern.json @@ -1,7 +1,7 @@ { "attributes": { - "fieldFormatMap": "{\"client.bytes\":{\"id\":\"bytes\"},\"destination.bytes\":{\"id\":\"bytes\"},\"event.duration\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"nanoseconds\",\"outputFormat\":\"asMilliseconds\",\"outputPrecision\":1}},\"http.request.body.bytes\":{\"id\":\"bytes\"},\"http.request.bytes\":{\"id\":\"bytes\"},\"http.response.body.bytes\":{\"id\":\"bytes\"},\"http.response.bytes\":{\"id\":\"bytes\"},\"network.bytes\":{\"id\":\"bytes\"},\"server.bytes\":{\"id\":\"bytes\"},\"source.bytes\":{\"id\":\"bytes\"},\"system.cpu.total.norm.pct\":{\"id\":\"percent\"},\"system.memory.actual.free\":{\"id\":\"bytes\"},\"system.memory.total\":{\"id\":\"bytes\"},\"system.process.cpu.total.norm.pct\":{\"id\":\"percent\"},\"system.process.memory.rss.bytes\":{\"id\":\"bytes\"},\"system.process.memory.size\":{\"id\":\"bytes\"},\"view spans\":{\"id\":\"url\",\"params\":{\"labelTemplate\":\"View Spans\"}}}", - "fields": "[{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"@timestamp\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.account.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.availability_zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.machine.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.region\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.tag\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.runtime\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"ecs.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.dataset\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.end\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.kind\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.outcome\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score_norm\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.severity\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.timezone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.ctime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.device\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.gid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.group\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.inode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mtime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.owner\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.target_path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.method\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.referrer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.status_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.application\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.community_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.direction\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.forwarded_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.iana_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.transport\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.vendor\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.args\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.executable\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.ppid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.id\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.title\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.working_directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.state\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.fragment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.password\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.query\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.scheme\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.username\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.device.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"fields\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.project.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"docker.container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.containerized\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.build\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.namespace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.annotations\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.replicaset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.deployment.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.statefulset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.image\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.event\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"timestamp.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.request.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.finished\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.response.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.environment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.sampled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"parent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.listening\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version_major\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"experimental\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.culprit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.grouping_key\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.handled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.logger_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.param_message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.total\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.actual.free\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.rss.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.bundle_filepath\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"view spans\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.subtype\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.start.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.sync\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.result\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks.*.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.span_count.dropped\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_id\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_index\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_score\",\"scripted\":false,\"searchable\":false,\"type\":\"number\"}]", + "fieldFormatMap": "{\"client.bytes\":{\"id\":\"bytes\"},\"client.port\":{\"id\":\"string\"},\"destination.bytes\":{\"id\":\"bytes\"},\"destination.port\":{\"id\":\"string\"},\"event.duration\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"nanoseconds\",\"outputFormat\":\"asMilliseconds\",\"outputPrecision\":1}},\"event.severity\":{\"id\":\"string\"},\"http.request.body.bytes\":{\"id\":\"bytes\"},\"http.request.bytes\":{\"id\":\"bytes\"},\"http.response.body.bytes\":{\"id\":\"bytes\"},\"http.response.bytes\":{\"id\":\"bytes\"},\"http.response.status_code\":{\"id\":\"string\"},\"network.bytes\":{\"id\":\"bytes\"},\"process.pid\":{\"id\":\"string\"},\"process.ppid\":{\"id\":\"string\"},\"process.thread.id\":{\"id\":\"string\"},\"server.bytes\":{\"id\":\"bytes\"},\"server.port\":{\"id\":\"string\"},\"source.bytes\":{\"id\":\"bytes\"},\"source.port\":{\"id\":\"string\"},\"system.cpu.total.norm.pct\":{\"id\":\"percent\"},\"system.memory.actual.free\":{\"id\":\"bytes\"},\"system.memory.total\":{\"id\":\"bytes\"},\"system.process.cpu.total.norm.pct\":{\"id\":\"percent\"},\"system.process.memory.rss.bytes\":{\"id\":\"bytes\"},\"system.process.memory.size\":{\"id\":\"bytes\"},\"url.port\":{\"id\":\"string\"},\"view spans\":{\"id\":\"url\",\"params\":{\"labelTemplate\":\"View Spans\"}}}", + "fields": "[{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"@timestamp\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.account.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.availability_zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.machine.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.region\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.tag\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.runtime\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"ecs.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.dataset\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.end\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.kind\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.outcome\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score_norm\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.severity\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.timezone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.ctime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.device\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.gid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.group\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.inode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mtime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.owner\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.target_path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.method\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.referrer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.status_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.application\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.community_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.direction\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.forwarded_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.iana_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.transport\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.vendor\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.args\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.executable\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.ppid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.id\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.title\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.working_directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.state\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.fragment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.password\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.query\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.scheme\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.username\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.device.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"fields\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"timeseries.instance\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.project.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.image.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"docker.container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.containerized\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.build\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.codename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.namespace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.annotations\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.replicaset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.deployment.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.statefulset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.image\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.event\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"timestamp.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.request.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.finished\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.response.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.environment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.sampled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"parent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.listening\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version_major\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"experimental\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.culprit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.grouping_key\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.handled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.logger_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.param_message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.total\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.actual.free\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.rss.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.bundle_filepath\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"view spans\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.subtype\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.start.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.sync\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.result\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks.*.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.span_count.dropped\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_id\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_index\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_score\",\"scripted\":false,\"searchable\":false,\"type\":\"number\"}]", "sourceFilters": "[{\"value\":\"sourcemap.sourcemap\"}]", "timeFieldName": "@timestamp" }, diff --git a/src/legacy/core_plugins/kibana/server/tutorials/apm/instructions/apm_agent_instructions.js b/src/legacy/core_plugins/kibana/server/tutorials/apm/instructions/apm_agent_instructions.js index c2138efb50aff..c2186b12a7b07 100644 --- a/src/legacy/core_plugins/kibana/server/tutorials/apm/instructions/apm_agent_instructions.js +++ b/src/legacy/core_plugins/kibana/server/tutorials/apm/instructions/apm_agent_instructions.js @@ -381,7 +381,7 @@ file name, or the `ELASTIC_APM_SERVICE_NAME` environment variable.', })} # ${i18n.translate('kbn.server.tutorials.apm.goClient.configure.commands.usedExecutableNameComment', { defaultMessage: - 'If ELASTIC_APM_SERVICE_NAME is not specified, the executable name will be used.', + 'If ELASTIC_APM_SERVICE_NAME is not specified, the executable name will be used.', })} export ELASTIC_APM_SERVICE_NAME= @@ -465,3 +465,68 @@ usage.', }), }, ]; + +export const createDotNetAgentInstructions = (apmServerUrl = '', secretToken = '') => [ + { + title: i18n.translate('kbn.server.tutorials.apm.dotNetClient.download.title', { + defaultMessage: 'Download the APM agent', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.dotNetClient.download.textPre', { + defaultMessage: '**Warning: The .NET agent is currently in Beta and not meant for production use.** \n\n \ + Add the the agent package(s) from [NuGet]({allNuGetPacakgesLink}) to your .NET application. There are multiple \ + NuGet packages available for different use cases. \n\n For an ASP.NET Core application with Entity Framework \ + Core download the [Elastic.Apm.All]({allApmPackageLink}) package. This package will automatically add every agent component to \ + your application. \n\n In case you would like to to minimize the dependencies, you can use the \ + [Elastic.Apm.AspNetCore]({aspNetCorePackageLink}) package for just \ + ASP.NET Core monitoring or the [Elastic.Apm.EfCore]({efCorePackageLink}) package for just Entity Framework Core monitoring. \n\n \ + In case you only want to use the public Agent API for manual instrumentation use the [Elastic.Apm]({elasticApmPackageLink}) package.', + values: { + allNuGetPacakgesLink: 'https://www.nuget.org/packages?q=Elastic.apm', + allApmPackageLink: 'https://www.nuget.org/packages/Elastic.Apm.All', + aspNetCorePackageLink: 'https://www.nuget.org/packages/Elastic.Apm.AspNetCore', + efCorePackageLink: 'https://www.nuget.org/packages/Elastic.Apm.EntityFrameworkCore', + elasticApmPackageLink: 'https://www.nuget.org/packages/Elastic.Apm', + }, + }), + }, + { + title: i18n.translate('kbn.server.tutorials.apm.dotNetClient.configureApplication.title', { + defaultMessage: 'Add the agent to the application', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.dotNetClient.configureApplication.textPre', { + defaultMessage: 'In case of ASP.NET Core, call the `UseElasticApm` method in the `Configure` method within the `Startup.cs` file.' + }), + commands: `public class Startup +{curlyOpen} + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + {curlyOpen} + app.UseElasticApm(Configuration); + //…rest of the method + {curlyClose} + //…rest of the class +{curlyClose}`.split('\n'), + textPost: i18n.translate('kbn.server.tutorials.apm.dotNetClient.configureApplication.textPost', { + defaultMessage: 'Passing an `IConfiguration` instance is optional and by doing so, the agent will read config settings through this \ + `IConfiguration` instance (e.g. from the `appsettings.json` file).', + }), + }, + { + title: i18n.translate('kbn.server.tutorials.apm.dotNetClient.configureAgent.title', { + defaultMessage: 'Sample appsettings.json file:', + }), + commands: `{curlyOpen} + "ElasticApm": {curlyOpen} + "LogLevel": "Error", + "SecretToken": "${secretToken}", + "ServerUrls": "${apmServerUrl || 'http://localhost:8200'}", //Set custom APM Server URL (default: http://localhost:8200) + "ServiceName" : "MyApp", //allowed characters: a-z, A-Z, 0-9, -, _, and space. Default is the entry assembly of the application + {curlyClose} +{curlyClose}`.split('\n'), + textPost: i18n.translate('kbn.server.tutorials.apm.dotNetClient.configureAgent.textPost', { + defaultMessage: 'In case you don’t pass an `IConfiguration` instance to the agent (e.g. in case of non ASP.NET Core applications) \ + you can also configure the agent through environment variables. \n \ + See [the documentation]({documentationLink}) for advanced usage.', + values: { documentationLink: '{config.docs.base_url}guide/en/apm/agent/dotnet/current/configuration.html' }, + }), + }, +]; diff --git a/src/legacy/core_plugins/kibana/server/tutorials/cisco_logs/index.js b/src/legacy/core_plugins/kibana/server/tutorials/cisco_logs/index.js new file mode 100644 index 0000000000000..0179f20c74e3a --- /dev/null +++ b/src/legacy/core_plugins/kibana/server/tutorials/cisco_logs/index.js @@ -0,0 +1,66 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; +import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/filebeat_instructions'; + +export function ciscoLogsSpecProvider(server, context) { + const moduleName = 'cisco'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS']; + return { + id: 'ciscoLogs', + name: i18n.translate('kbn.server.tutorials.ciscoLogs.nameTitle', { + defaultMessage: 'Cisco', + }), + category: TUTORIAL_CATEGORY.SECURITY, + shortDescription: i18n.translate('kbn.server.tutorials.ciscoLogs.shortDescription', { + defaultMessage: 'Collect and parse logs received from Cisco ASA firewalls.', + }), + longDescription: i18n.translate('kbn.server.tutorials.ciscoLogs.longDescription', { + defaultMessage: 'This is a module for Cisco network device’s logs. Currently \ +supports the "asa" fileset for Cisco ASA firewall logs received over syslog or read from a file. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-cisco.html', + }, + }), + //euiIconType: 'logoCisco', + artifacts: { + dashboards: [], + application: { + path: '/app/siem', + label: i18n.translate( + 'kbn.server.tutorials.ciscoLogs.artifacts.dashboards.linkLabel', + { + defaultMessage: 'SIEM App', + } + ), + }, + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-cisco.html' + } + }, + completionTimeMinutes: 10, + previewImagePath: '/plugins/kibana/home/tutorial_resources/cisco_logs/screenshot.png', + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms) + }; +} diff --git a/src/legacy/core_plugins/kibana/server/tutorials/envoyproxy_logs/index.js b/src/legacy/core_plugins/kibana/server/tutorials/envoyproxy_logs/index.js new file mode 100644 index 0000000000000..c0df1806fb52c --- /dev/null +++ b/src/legacy/core_plugins/kibana/server/tutorials/envoyproxy_logs/index.js @@ -0,0 +1,66 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; +import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/filebeat_instructions'; + +export function envoyproxyLogsSpecProvider(server, context) { + const moduleName = 'envoyproxy'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS']; + return { + id: 'envoyproxyLogs', + name: i18n.translate('kbn.server.tutorials.envoyproxyLogs.nameTitle', { + defaultMessage: 'Envoyproxy', + }), + category: TUTORIAL_CATEGORY.SECURITY, + shortDescription: i18n.translate('kbn.server.tutorials.envoyproxyLogs.shortDescription', { + defaultMessage: 'Collect and parse logs received from the Envoy proxy.', + }), + longDescription: i18n.translate('kbn.server.tutorials.envoyproxyLogs.longDescription', { + defaultMessage: 'This is a filebeat module for [Envoy proxy access log](https://www.envoyproxy.io/docs/envoy/v1.10.0/configuration/access_log). \ +It supports both standalone deployment and Envoy proxy deployment in Kubernetes. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-envoyproxy.html', + }, + }), + //euiIconType: 'logoCisco', + artifacts: { + dashboards: [], + application: { + path: '/app/siem', + label: i18n.translate( + 'kbn.server.tutorials.envoyproxyLogs.artifacts.dashboards.linkLabel', + { + defaultMessage: 'SIEM App', + } + ), + }, + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-envoyproxy.html' + } + }, + completionTimeMinutes: 10, + previewImagePath: '/plugins/kibana/home/tutorial_resources/envoyproxy_logs/screenshot.png', + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms) + }; +} diff --git a/src/legacy/core_plugins/kibana/server/tutorials/iptables_logs/index.js b/src/legacy/core_plugins/kibana/server/tutorials/iptables_logs/index.js new file mode 100644 index 0000000000000..992ed2f31f77f --- /dev/null +++ b/src/legacy/core_plugins/kibana/server/tutorials/iptables_logs/index.js @@ -0,0 +1,68 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; +import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/filebeat_instructions'; + +export function iptablesLogsSpecProvider(server, context) { + const moduleName = 'iptables'; + const platforms = ['DEB', 'RPM']; + return { + id: 'iptablesLogs', + name: i18n.translate('kbn.server.tutorials.iptablesLogs.nameTitle', { + defaultMessage: 'Iptables / Ubiquiti', + }), + category: TUTORIAL_CATEGORY.SECURITY, + shortDescription: i18n.translate('kbn.server.tutorials.iptablesLogs.shortDescription', { + defaultMessage: 'Collect and parse iptables and ip6tables logs or from Ubiqiti firewalls.', + }), + longDescription: i18n.translate('kbn.server.tutorials.iptablesLogs.longDescription', { + defaultMessage: 'This is a module for iptables and ip6tables logs. It parses logs \ +received over the network via syslog or from a file. Also, it understands the \ +prefix added by some Ubiquiti firewalls, which includes the rule set name, rule \ +number and the action performed on the traffic (allow/deny).. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-iptables.html', + }, + }), + //euiIconType: 'logoUbiquiti', + artifacts: { + dashboards: [], + application: { + path: '/app/siem', + label: i18n.translate( + 'kbn.server.tutorials.iptablesLogs.artifacts.dashboards.linkLabel', + { + defaultMessage: 'SIEM App', + } + ), + }, + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-iptables.html' + } + }, + completionTimeMinutes: 10, + previewImagePath: '/plugins/kibana/home/tutorial_resources/iptables_logs/screenshot.png', + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms) + }; +} diff --git a/src/legacy/core_plugins/kibana/server/tutorials/register.js b/src/legacy/core_plugins/kibana/server/tutorials/register.js index afae92b204290..4aae4e33d70ca 100644 --- a/src/legacy/core_plugins/kibana/server/tutorials/register.js +++ b/src/legacy/core_plugins/kibana/server/tutorials/register.js @@ -72,6 +72,9 @@ import { zeekLogsSpecProvider } from './zeek_logs'; import { corednsMetricsSpecProvider } from './coredns_metrics'; import { corednsLogsSpecProvider } from './coredns_logs'; import { auditbeatSpecProvider } from './auditbeat'; +import { iptablesLogsSpecProvider } from './iptables_logs'; +import { ciscoLogsSpecProvider } from './cisco_logs'; +import { envoyproxyLogsSpecProvider } from './envoyproxy_logs'; export function registerTutorials(server) { server.registerTutorial(systemLogsSpecProvider); @@ -129,4 +132,7 @@ export function registerTutorials(server) { server.registerTutorial(corednsMetricsSpecProvider); server.registerTutorial(corednsLogsSpecProvider); server.registerTutorial(auditbeatSpecProvider); + server.registerTutorial(iptablesLogsSpecProvider); + server.registerTutorial(ciscoLogsSpecProvider); + server.registerTutorial(envoyproxyLogsSpecProvider); } diff --git a/src/legacy/core_plugins/kibana/ui_setting_defaults.js b/src/legacy/core_plugins/kibana/ui_setting_defaults.js index bb7ccd57f2510..3967572f73ff7 100644 --- a/src/legacy/core_plugins/kibana/ui_setting_defaults.js +++ b/src/legacy/core_plugins/kibana/ui_setting_defaults.js @@ -208,6 +208,23 @@ export function getUiSettingDefaults() { type: 'select', options: weekdays }, + 'dateNanosFormat': { + name: i18n.translate('kbn.advancedSettings.dateNanosFormatTitle', { + defaultMessage: 'Date with nanoseconds format', + }), + value: 'MMM D, YYYY @ HH:mm:ss.SSSSSSSSS', + description: i18n.translate('kbn.advancedSettings.dateNanosFormatText', { + defaultMessage: 'Used for the {dateNanosLink} datatype of Elasticsearch', + values: { + dateNanosLink: + '' + + i18n.translate('kbn.advancedSettings.dateNanosLinkTitle', { + defaultMessage: 'date_nanos', + }) + + '', + }, + }), + }, 'defaultIndex': { name: i18n.translate('kbn.advancedSettings.defaultIndexTitle', { defaultMessage: 'Default index', @@ -608,6 +625,7 @@ export function getUiSettingDefaults() { `{ "ip": { "id": "ip", "params": {} }, "date": { "id": "date", "params": {} }, + "date_nanos": { "id": "date_nanos", "params": {}, "es": true }, "number": { "id": "number", "params": {} }, "boolean": { "id": "boolean", "params": {} }, "_source": { "id": "_source", "params": {} }, diff --git a/src/legacy/core_plugins/markdown_vis/public/markdown_vis.js b/src/legacy/core_plugins/markdown_vis/public/markdown_vis.js index 96eca5cb86c06..7842ad0bf523e 100644 --- a/src/legacy/core_plugins/markdown_vis/public/markdown_vis.js +++ b/src/legacy/core_plugins/markdown_vis/public/markdown_vis.js @@ -18,6 +18,7 @@ */ import { MarkdownVisWrapper } from './markdown_vis_controller'; +import { i18n } from '@kbn/i18n'; import { VisFactoryProvider } from 'ui/vis/vis_factory'; import markdownVisParamsTemplate from './markdown_vis_params.html'; import { VisTypesRegistryProvider } from 'ui/registry/vis_types'; @@ -29,7 +30,7 @@ import { DefaultEditorSize } from 'ui/vis/editor_size'; // register the provider with the visTypes registry so that other know it exists VisTypesRegistryProvider.register(MarkdownVisProvider); -function MarkdownVisProvider(Private, i18n) { +function MarkdownVisProvider(Private) { const VisFactory = Private(VisFactoryProvider); // return the visType object, which kibana will use to display and configure new @@ -39,7 +40,7 @@ function MarkdownVisProvider(Private, i18n) { title: 'Markdown', isAccessible: true, icon: 'visText', - description: i18n('markdownVis.markdownDescription', { defaultMessage: 'Create a document using markdown syntax' }), + description: i18n.translate('markdownVis.markdownDescription', { defaultMessage: 'Create a document using markdown syntax' }), visConfig: { component: MarkdownVisWrapper, defaults: { diff --git a/src/legacy/core_plugins/metric_vis/public/components/metric_vis_value.test.js b/src/legacy/core_plugins/metric_vis/public/components/metric_vis_value.test.js index 6919a09ba9660..fe1eec9851871 100644 --- a/src/legacy/core_plugins/metric_vis/public/components/metric_vis_value.test.js +++ b/src/legacy/core_plugins/metric_vis/public/components/metric_vis_value.test.js @@ -43,4 +43,41 @@ describe('MetricVisValue', () => { expect(component.find('EuiKeyboardAccessible').exists()).toBe(false); }); + it('should add -isfilterable class if onFilter is provided', () => { + const onFilter = jest.fn(); + const component = shallow( + + ); + component.simulate('click'); + expect(component.find('.mtrVis__container-isfilterable')).toHaveLength(1); + }); + + it('should not add -isfilterable class if onFilter is not provided', () => { + const component = shallow( + + ); + component.simulate('click'); + expect(component.find('.mtrVis__container-isfilterable')).toHaveLength(0); + }); + + it('should call onFilter callback if provided', () => { + const onFilter = jest.fn(); + const component = shallow( + + ); + component.find('.mtrVis__container-isfilterable').simulate('click'); + expect(onFilter).toHaveBeenCalledWith({ label: 'Foo', value: 'foo' }); + }); }); diff --git a/src/legacy/core_plugins/metric_vis/public/metric_vis.js b/src/legacy/core_plugins/metric_vis/public/metric_vis.js index 712413e330baa..1cea731339a62 100644 --- a/src/legacy/core_plugins/metric_vis/public/metric_vis.js +++ b/src/legacy/core_plugins/metric_vis/public/metric_vis.js @@ -18,6 +18,7 @@ */ import './metric_vis_params'; +import { i18n } from '@kbn/i18n'; import { VisFactoryProvider } from 'ui/vis/vis_factory'; import { Schemas } from 'ui/vis/editors/default/schemas'; import { VisTypesRegistryProvider } from 'ui/registry/vis_types'; @@ -30,16 +31,16 @@ import { MetricVisComponent } from './metric_vis_controller'; // register the provider with the visTypes registry VisTypesRegistryProvider.register(MetricVisProvider); -function MetricVisProvider(Private, i18n) { +function MetricVisProvider(Private) { const VisFactory = Private(VisFactoryProvider); // return the visType object, which kibana will use to display and configure new // Vis object of this type. return VisFactory.createReactVisualization({ name: 'metric', - title: i18n('metricVis.metricTitle', { defaultMessage: 'Metric' }), + title: i18n.translate('metricVis.metricTitle', { defaultMessage: 'Metric' }), icon: 'visMetric', - description: i18n('metricVis.metricDescription', { defaultMessage: 'Display a calculation as a single number' }), + description: i18n.translate('metricVis.metricDescription', { defaultMessage: 'Display a calculation as a single number' }), visConfig: { component: MetricVisComponent, defaults: { @@ -73,15 +74,15 @@ function MetricVisProvider(Private, i18n) { metricColorMode: [ { id: 'None', - label: i18n('metricVis.colorModes.noneOptionLabel', { defaultMessage: 'None' }) + label: i18n.translate('metricVis.colorModes.noneOptionLabel', { defaultMessage: 'None' }) }, { id: 'Labels', - label: i18n('metricVis.colorModes.labelsOptionLabel', { defaultMessage: 'Labels' }) + label: i18n.translate('metricVis.colorModes.labelsOptionLabel', { defaultMessage: 'Labels' }) }, { id: 'Background', - label: i18n('metricVis.colorModes.backgroundOptionLabel', { defaultMessage: 'Background' }) + label: i18n.translate('metricVis.colorModes.backgroundOptionLabel', { defaultMessage: 'Background' }) } ], colorSchemas: Object.values(vislibColorMaps).map(value => ({ id: value.id, label: value.label })), @@ -91,7 +92,7 @@ function MetricVisProvider(Private, i18n) { { group: 'metrics', name: 'metric', - title: i18n('metricVis.schemas.metricTitle', { defaultMessage: 'Metric' }), + title: i18n.translate('metricVis.schemas.metricTitle', { defaultMessage: 'Metric' }), min: 1, aggFilter: [ '!std_dev', '!geo_centroid', @@ -102,7 +103,7 @@ function MetricVisProvider(Private, i18n) { }, { group: 'buckets', name: 'group', - title: i18n('metricVis.schemas.splitGroupTitle', { defaultMessage: 'Split Group' }), + title: i18n.translate('metricVis.schemas.splitGroupTitle', { defaultMessage: 'Split Group' }), min: 0, max: 1, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] diff --git a/src/legacy/core_plugins/metric_vis/public/metric_vis_controller.js b/src/legacy/core_plugins/metric_vis/public/metric_vis_controller.js index 6ed4b1b490e17..e8c646646ec4b 100644 --- a/src/legacy/core_plugins/metric_vis/public/metric_vis_controller.js +++ b/src/legacy/core_plugins/metric_vis/public/metric_vis_controller.js @@ -161,7 +161,7 @@ export class MetricVisComponent extends Component { key={index} metric={metric} fontSize={this.props.visParams.metric.style.fontSize} - onFilter={metric.filterKey && metric.bucketAgg ? this._filterBucket : null} + onFilter={this.props.visParams.dimensions.bucket ? this._filterBucket : null} showLabel={this.props.visParams.metric.labels.show} /> ); diff --git a/src/legacy/core_plugins/metric_vis/public/metric_vis_params.js b/src/legacy/core_plugins/metric_vis/public/metric_vis_params.js index 20ad8954f9617..b913d4afaebba 100644 --- a/src/legacy/core_plugins/metric_vis/public/metric_vis_params.js +++ b/src/legacy/core_plugins/metric_vis/public/metric_vis_params.js @@ -18,12 +18,13 @@ */ import { uiModules } from 'ui/modules'; +import { i18n } from '@kbn/i18n'; import 'ui/directives/inequality'; import metricVisParamsTemplate from './metric_vis_params.html'; import _ from 'lodash'; const module = uiModules.get('kibana'); -module.directive('metricVisParams', function (i18n) { +module.directive('metricVisParams', function () { return { restrict: 'E', template: metricVisParamsTemplate, @@ -81,7 +82,8 @@ module.directive('metricVisParams', function (i18n) { $scope.customColors = true; }); - $scope.editorState.requiredDescription = i18n('metricVis.params.ranges.warning.requiredDescription', { defaultMessage: 'Required:' }); + $scope.editorState.requiredDescription = i18n.translate( + 'metricVis.params.ranges.warning.requiredDescription', { defaultMessage: 'Required:' }); } }; }); diff --git a/src/legacy/core_plugins/metrics/common/__tests__/calculate_label.js b/src/legacy/core_plugins/metrics/common/__tests__/calculate_label.js index ad2d0b0bfaa47..6983f77e4a8be 100644 --- a/src/legacy/core_plugins/metrics/common/__tests__/calculate_label.js +++ b/src/legacy/core_plugins/metrics/common/__tests__/calculate_label.js @@ -18,7 +18,7 @@ */ import { expect } from 'chai'; -import calculateLabel from '../calculate_label'; +import { calculateLabel } from '../calculate_label'; describe('calculateLabel(metric, metrics)', () => { it('returns "Unknown" for empty metric', () => { diff --git a/src/legacy/core_plugins/metrics/common/__tests__/get_last_value.js b/src/legacy/core_plugins/metrics/common/__tests__/get_last_value.js deleted file mode 100644 index 0dc06c7e6c8c5..0000000000000 --- a/src/legacy/core_plugins/metrics/common/__tests__/get_last_value.js +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { expect } from 'chai'; -import getLastValue from '../get_last_value'; - -describe('getLastValue(data)', () => { - - it('returns data if data is not array', () => { - expect(getLastValue('foo')).to.equal('foo'); - }); - - it('returns the last value', () => { - const data = [[1, 1]]; - expect(getLastValue(data)).to.equal(1); - }); - - it('returns the second to last value if the last value is null (default)', () => { - const data = [[1, 4], [2, null]]; - expect(getLastValue(data)).to.equal(4); - }); - - it('returns 0 if second to last is not defined (default)', () => { - const data = [[1, null], [2, null]]; - expect(getLastValue(data)).to.equal(0); - }); - - it('returns the N to last value if the last N-1 values are null (default)', () => { - const data = [[1, 4], [2, null], [3, null]]; - expect(getLastValue(data, 3)).to.equal(4); - }); -}); - diff --git a/src/legacy/core_plugins/metrics/common/agg_lookup.js b/src/legacy/core_plugins/metrics/common/agg_lookup.js index 2de9a1aee206c..6713899c0f82e 100644 --- a/src/legacy/core_plugins/metrics/common/agg_lookup.js +++ b/src/legacy/core_plugins/metrics/common/agg_lookup.js @@ -20,7 +20,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -const lookup = { +export const lookup = { count: i18n.translate('tsvb.aggLookup.countLabel', { defaultMessage: 'Count' }), calculation: i18n.translate('tsvb.aggLookup.calculationLabel', { defaultMessage: 'Calculation' }), std_deviation: i18n.translate('tsvb.aggLookup.deviationLabel', { defaultMessage: 'Std. Deviation' }), @@ -111,5 +111,3 @@ export function createOptions(type = '_all', siblings = []) { }) .value(); } - -export default lookup; diff --git a/src/legacy/core_plugins/metrics/common/basic_aggs.js b/src/legacy/core_plugins/metrics/common/basic_aggs.js index e640021795616..b330eea559843 100644 --- a/src/legacy/core_plugins/metrics/common/basic_aggs.js +++ b/src/legacy/core_plugins/metrics/common/basic_aggs.js @@ -17,7 +17,7 @@ * under the License. */ -export default [ +export const basicAggs = [ 'count', 'avg', 'max', diff --git a/src/legacy/core_plugins/metrics/common/calculate_label.js b/src/legacy/core_plugins/metrics/common/calculate_label.js index 5707e6c53a1cd..231fc4897fe27 100644 --- a/src/legacy/core_plugins/metrics/common/calculate_label.js +++ b/src/legacy/core_plugins/metrics/common/calculate_label.js @@ -18,7 +18,7 @@ */ import { includes, startsWith } from 'lodash'; -import lookup from './agg_lookup'; +import { lookup } from './agg_lookup'; import { i18n } from '@kbn/i18n'; const paths = [ @@ -35,7 +35,8 @@ const paths = [ 'serial_diff', 'positive_only', ]; -export default function calculateLabel(metric, metrics) { + +export function calculateLabel(metric, metrics) { if (!metric) return i18n.translate('tsvb.calculateLabel.unknownLabel', { defaultMessage: 'Unknown' }); if (metric.alias) return metric.alias; diff --git a/src/legacy/core_plugins/metrics/common/extract_index_patterns.js b/src/legacy/core_plugins/metrics/common/extract_index_patterns.js new file mode 100644 index 0000000000000..5c4b9709886b7 --- /dev/null +++ b/src/legacy/core_plugins/metrics/common/extract_index_patterns.js @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { uniq } from 'lodash'; + +export function extractIndexPatterns(panel, excludedFields = {}) { + const patterns = []; + + if (!excludedFields[panel.index_pattern]) { + patterns.push(panel.index_pattern); + } + + panel.series.forEach(series => { + const indexPattern = series.series_index_pattern; + if (indexPattern && series.override_index_pattern && !excludedFields[indexPattern]) { + patterns.push(indexPattern); + } + }); + + if (panel.annotations) { + panel.annotations.forEach(item => { + const indexPattern = item.index_pattern; + if (indexPattern && !excludedFields[indexPattern]) { + patterns.push(indexPattern); + } + }); + } + + if (patterns.length === 0) { + patterns.push(''); + } + + return uniq(patterns).sort(); +} diff --git a/src/legacy/core_plugins/metrics/common/extract_index_patterns.test.js b/src/legacy/core_plugins/metrics/common/extract_index_patterns.test.js new file mode 100644 index 0000000000000..967ecffc018da --- /dev/null +++ b/src/legacy/core_plugins/metrics/common/extract_index_patterns.test.js @@ -0,0 +1,67 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { extractIndexPatterns } from './extract_index_patterns'; + +describe('extractIndexPatterns(vis)', () => { + let visParams; + let visFields; + + beforeEach(() => { + visFields = { + '*': [], + }; + visParams = { + index_pattern: '*', + series: [ + { + override_index_pattern: 1, + series_index_pattern: 'example-1-*', + }, + { + override_index_pattern: 1, + series_index_pattern: 'example-2-*', + }, + ], + annotations: [ + { index_pattern: 'notes-*' }, + { index_pattern: 'example-1-*' }, + ], + }; + }); + + test('should return index patterns', () => { + visFields = {}; + + expect(extractIndexPatterns(visParams, visFields)).toEqual([ + '*', + 'example-1-*', + 'example-2-*', + 'notes-*', + ]); + }); + + test('should return index patterns that do not exist in visFields', () => { + expect(extractIndexPatterns(visParams, visFields)).toEqual([ + 'example-1-*', + 'example-2-*', + 'notes-*', + ]); + }); +}); diff --git a/src/legacy/core_plugins/metrics/common/get_last_value.js b/src/legacy/core_plugins/metrics/common/get_last_value.js index 202eac8d16c75..c763d6bbf71b1 100644 --- a/src/legacy/core_plugins/metrics/common/get_last_value.js +++ b/src/legacy/core_plugins/metrics/common/get_last_value.js @@ -17,20 +17,15 @@ * under the License. */ -import { isArray, findLast } from 'lodash'; +import { isArray, last } from 'lodash'; const DEFAULT_VALUE = 0; +const extractValue = data => data && data[1] || null; -export default (data, defaultValue = DEFAULT_VALUE) => { +export const getLastValue = (data, defaultValue = DEFAULT_VALUE) => { if (!isArray(data)) { - return data; + return data || defaultValue; } - const extractValue = data => data && data[1] || null; - - // If the last value is zero or null because of a partial bucket or - // some kind of timeshift weirdness we will show the second to last. - const lastValid = findLast(data, item => extractValue(item)); - - return extractValue(lastValid) || defaultValue; + return extractValue(last(data)) || defaultValue; }; diff --git a/src/legacy/core_plugins/metrics/common/get_last_value.test.js b/src/legacy/core_plugins/metrics/common/get_last_value.test.js new file mode 100644 index 0000000000000..d8426365ca483 --- /dev/null +++ b/src/legacy/core_plugins/metrics/common/get_last_value.test.js @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getLastValue } from './get_last_value'; + +describe('getLastValue(data)', () => { + test('should returns data if data is not array', () => { + expect(getLastValue('foo')).toBe('foo'); + }); + + test('should returns the last value', () => { + expect(getLastValue([[1, 2]])).toBe(2); + }); + + test('should returns the default value ', () => { + expect(getLastValue()).toBe(0); + }); + + test('should returns 0 if second to last is not defined (default)', () => { + expect(getLastValue([[1, null], [2, null]])).toBe(0); + }); + + test('should allows to override the default value', () => { + expect(getLastValue(null, '-')).toBe('-'); + }); +}); + diff --git a/src/legacy/core_plugins/metrics/common/interval_regexp.js b/src/legacy/core_plugins/metrics/common/interval_regexp.js index 392d2efd4aa94..948935b28b2d7 100644 --- a/src/legacy/core_plugins/metrics/common/interval_regexp.js +++ b/src/legacy/core_plugins/metrics/common/interval_regexp.js @@ -18,6 +18,7 @@ */ import dateMath from '@elastic/datemath'; + export const GTE_INTERVAL_RE = new RegExp(`^>=([\\d\\.]+\\s*(${dateMath.units.join('|')}))$`); export const INTERVAL_STRING_RE = new RegExp(`^([\\d\\.]+)\\s*(${dateMath.units.join('|')})$`); diff --git a/src/legacy/core_plugins/metrics/common/set_is_reversed.js b/src/legacy/core_plugins/metrics/common/set_is_reversed.js index 2f24ceb9f9ec9..5024d0f48b391 100644 --- a/src/legacy/core_plugins/metrics/common/set_is_reversed.js +++ b/src/legacy/core_plugins/metrics/common/set_is_reversed.js @@ -19,6 +19,7 @@ import color from 'color'; import chrome from '../../../ui/public/chrome'; + const IS_DARK_THEME = chrome.getUiSettingsClient().get('theme:darkMode'); /** diff --git a/src/legacy/core_plugins/metrics/index.js b/src/legacy/core_plugins/metrics/index.js index 076d98931e328..490cb0753d36c 100644 --- a/src/legacy/core_plugins/metrics/index.js +++ b/src/legacy/core_plugins/metrics/index.js @@ -19,8 +19,8 @@ import { resolve } from 'path'; -import fieldsRoutes from './server/routes/fields'; -import visDataRoutes from './server/routes/vis'; +import { fieldsRoutes } from './server/routes/fields'; +import { visDataRoutes } from './server/routes/vis'; import { SearchStrategiesRegister } from './server/lib/search_strategies/search_strategies_register'; export default function (kibana) { diff --git a/src/legacy/core_plugins/metrics/public/components/add_delete_buttons.js b/src/legacy/core_plugins/metrics/public/components/add_delete_buttons.js index df10c7a8430ca..381aae41d9313 100644 --- a/src/legacy/core_plugins/metrics/public/components/add_delete_buttons.js +++ b/src/legacy/core_plugins/metrics/public/components/add_delete_buttons.js @@ -23,7 +23,7 @@ import { EuiToolTip, EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/e import { i18n } from '@kbn/i18n'; import { isBoolean } from 'lodash'; -function AddDeleteButtons(props) { +export function AddDeleteButtons(props) { const { testSubj } = props; const createDelete = () => { if (props.disableDelete) { @@ -146,5 +146,3 @@ AddDeleteButtons.propTypes = { onDelete: PropTypes.func, responsive: PropTypes.bool, }; - -export default AddDeleteButtons; diff --git a/src/legacy/core_plugins/metrics/public/components/add_delete_buttons.test.js b/src/legacy/core_plugins/metrics/public/components/add_delete_buttons.test.js index 71492c5ee1f84..73d20f7f28ffd 100644 --- a/src/legacy/core_plugins/metrics/public/components/add_delete_buttons.test.js +++ b/src/legacy/core_plugins/metrics/public/components/add_delete_buttons.test.js @@ -21,7 +21,7 @@ import React from 'react'; import { expect } from 'chai'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import sinon from 'sinon'; -import AddDeleteButtons from './add_delete_buttons'; +import { AddDeleteButtons } from './add_delete_buttons'; describe('AddDeleteButtons', () => { it('calls onAdd={handleAdd}', () => { diff --git a/src/legacy/core_plugins/metrics/public/components/aggs/agg.js b/src/legacy/core_plugins/metrics/public/components/aggs/agg.js index 4702b89038302..3c21f717ebe6d 100644 --- a/src/legacy/core_plugins/metrics/public/components/aggs/agg.js +++ b/src/legacy/core_plugins/metrics/public/components/aggs/agg.js @@ -19,13 +19,13 @@ import PropTypes from 'prop-types'; import React from 'react'; -import aggToComponent from '../lib/agg_to_component'; +import { aggToComponent } from '../lib/agg_to_component'; import { UnsupportedAgg } from './unsupported_agg'; import { TemporaryUnsupportedAgg } from './temporary_unsupported_agg'; import { isMetricEnabled } from '../../lib/check_ui_restrictions'; -function Agg(props) { +export function Agg(props) { const { model, uiRestrictions } = props; let Component = aggToComponent[model.type]; @@ -76,5 +76,3 @@ Agg.propTypes = { uiRestrictions: PropTypes.object, dragHandleProps: PropTypes.object, }; - -export default Agg; diff --git a/src/legacy/core_plugins/metrics/public/components/aggs/agg_row.js b/src/legacy/core_plugins/metrics/public/components/aggs/agg_row.js index d218c3f7249e9..b84fc925ac19e 100644 --- a/src/legacy/core_plugins/metrics/public/components/aggs/agg_row.js +++ b/src/legacy/core_plugins/metrics/public/components/aggs/agg_row.js @@ -20,7 +20,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { last } from 'lodash'; -import AddDeleteButtons from '../add_delete_buttons'; +import { AddDeleteButtons } from '../add_delete_buttons'; import { EuiIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { injectI18n } from '@kbn/i18n/react'; import { SeriesDragHandler } from '../series_drag_handler'; @@ -72,5 +72,4 @@ AggRowUi.propTypes = { dragHandleProps: PropTypes.object, }; -const AggRow = injectI18n(AggRowUi); -export default AggRow; +export const AggRow = injectI18n(AggRowUi); diff --git a/src/legacy/core_plugins/metrics/public/components/aggs/agg_select.js b/src/legacy/core_plugins/metrics/public/components/aggs/agg_select.js index 66e61e8f7f99e..0ea2f2e7bb66e 100644 --- a/src/legacy/core_plugins/metrics/public/components/aggs/agg_select.js +++ b/src/legacy/core_plugins/metrics/public/components/aggs/agg_select.js @@ -241,5 +241,4 @@ AggSelectUi.propTypes = { uiRestrictions: PropTypes.object, }; -const AggSelect = injectI18n(AggSelectUi); -export default AggSelect; +export const AggSelect = injectI18n(AggSelectUi); diff --git a/src/legacy/core_plugins/metrics/public/components/aggs/aggs.js b/src/legacy/core_plugins/metrics/public/components/aggs/aggs.js index 1418803ebb193..990a3cc2cb41f 100644 --- a/src/legacy/core_plugins/metrics/public/components/aggs/aggs.js +++ b/src/legacy/core_plugins/metrics/public/components/aggs/aggs.js @@ -22,9 +22,9 @@ import PropTypes from 'prop-types'; import { EuiDraggable, EuiDroppable } from '@elastic/eui'; -import Agg from './agg'; -import newMetricAggFn from '../lib/new_metric_agg_fn'; -import seriesChangeHandler from '../lib/series_change_handler'; +import { Agg } from './agg'; +import { newMetricAggFn } from '../lib/new_metric_agg_fn'; +import { seriesChangeHandler } from '../lib/series_change_handler'; import { handleAdd, handleDelete } from '../lib/collection_actions'; const DROPPABLE_ID = 'aggs_dnd'; diff --git a/src/legacy/core_plugins/metrics/public/components/aggs/calculation.js b/src/legacy/core_plugins/metrics/public/components/aggs/calculation.js index b086bbd0df1c7..e34886469fac4 100644 --- a/src/legacy/core_plugins/metrics/public/components/aggs/calculation.js +++ b/src/legacy/core_plugins/metrics/public/components/aggs/calculation.js @@ -21,13 +21,13 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import _ from 'lodash'; import uuid from 'uuid'; -import AggRow from './agg_row'; -import AggSelect from './agg_select'; +import { AggRow } from './agg_row'; +import { AggSelect } from './agg_select'; -import createChangeHandler from '../lib/create_change_handler'; -import createSelectHandler from '../lib/create_select_handler'; -import createTextHandler from '../lib/create_text_handler'; -import Vars from './vars'; +import { createChangeHandler } from '../lib/create_change_handler'; +import { createSelectHandler } from '../lib/create_select_handler'; +import { createTextHandler } from '../lib/create_text_handler'; +import { CalculationVars } from './vars'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -40,7 +40,7 @@ import { EuiCode, } from '@elastic/eui'; -class CalculationAgg extends Component { +export class CalculationAgg extends Component { componentWillMount() { if (!this.props.model.variables) { @@ -95,7 +95,7 @@ class CalculationAgg extends Component { defaultMessage="Variables" /> - - { + const part = { query_string: filter }; + collectionActions.handleChange(this.props, { + ...model, + ...part + }); + }; renderRow(row) { - const defaults = { fields: '', template: '', index_pattern: '*', query_string: '' }; + const defaults = { + fields: '', + template: '', + index_pattern: '*', + query_string: { query: '', language: getDefaultQueryLanguage() } + }; const model = { ...defaults, ...row }; const handleChange = (part) => { const fn = collectionActions.handleChange.bind(null, this.props); @@ -154,10 +169,16 @@ class AnnotationsEditor extends Component { />)} fullWidth > - this.handleQueryChange(model, query)} + appName={'VisEditor'} + indexPatterns={[model.index_pattern]} + store={localStorage} + showDatePicker={false} /> @@ -250,7 +271,6 @@ class AnnotationsEditor extends Component { - @@ -312,7 +332,6 @@ class AnnotationsEditor extends Component { ); } - } AnnotationsEditor.defaultProps = { @@ -325,5 +344,3 @@ AnnotationsEditor.propTypes = { name: PropTypes.string, onChange: PropTypes.func }; - -export default AnnotationsEditor; diff --git a/src/legacy/core_plugins/metrics/public/components/color_picker.js b/src/legacy/core_plugins/metrics/public/components/color_picker.js index 2bedccb32cdee..1f38859063972 100644 --- a/src/legacy/core_plugins/metrics/public/components/color_picker.js +++ b/src/legacy/core_plugins/metrics/public/components/color_picker.js @@ -23,10 +23,10 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { EuiIconTip, } from '@elastic/eui'; -import Picker from './custom_color_picker'; +import { CustomColorPicker } from './custom_color_picker'; import { injectI18n } from '@kbn/i18n/react'; -class ColorPicker extends Component { +class ColorPickerUI extends Component { constructor(props) { super(props); @@ -109,7 +109,7 @@ class ColorPicker extends Component { ); } return ( -
+
{ swatch } { clear } { @@ -120,7 +120,7 @@ class ColorPicker extends Component { className="tvbColorPicker__cover" onClick={this.handleClose} /> - @@ -133,11 +133,11 @@ class ColorPicker extends Component { } -ColorPicker.propTypes = { +ColorPickerUI.propTypes = { name: PropTypes.string.isRequired, value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), disableTrash: PropTypes.bool, onChange: PropTypes.func }; -export default injectI18n(ColorPicker); +export const ColorPicker = injectI18n(ColorPickerUI); diff --git a/src/legacy/core_plugins/metrics/public/components/color_rules.js b/src/legacy/core_plugins/metrics/public/components/color_rules.js index e5ed6e2e93990..4c88bb41997ae 100644 --- a/src/legacy/core_plugins/metrics/public/components/color_rules.js +++ b/src/legacy/core_plugins/metrics/public/components/color_rules.js @@ -20,9 +20,9 @@ import PropTypes from 'prop-types'; import React, { Component, Fragment } from 'react'; import _ from 'lodash'; -import AddDeleteButtons from './add_delete_buttons'; -import * as collectionActions from './lib/collection_actions'; -import ColorPicker from './color_picker'; +import { AddDeleteButtons } from './add_delete_buttons'; +import { collectionActions } from './lib/collection_actions'; +import { ColorPicker } from './color_picker'; import { htmlIdGenerator, EuiComboBox, @@ -34,7 +34,7 @@ import { import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -class ColorRules extends Component { +class ColorRulesUI extends Component { constructor(props) { super(props); @@ -181,7 +181,7 @@ class ColorRules extends Component { } -ColorRules.defaultProps = { +ColorRulesUI.defaultProps = { name: 'color_rules', primaryName: i18n.translate('tsvb.colorRules.defaultPrimaryNameLabel', { defaultMessage: 'background' }), primaryVarName: 'background_color', @@ -190,7 +190,7 @@ ColorRules.defaultProps = { hideSecondary: false }; -ColorRules.propTypes = { +ColorRulesUI.propTypes = { name: PropTypes.string, model: PropTypes.object, onChange: PropTypes.func, @@ -201,4 +201,4 @@ ColorRules.propTypes = { hideSecondary: PropTypes.bool }; -export default injectI18n(ColorRules); +export const ColorRules = injectI18n(ColorRulesUI); diff --git a/src/legacy/core_plugins/metrics/public/components/custom_color_picker.js b/src/legacy/core_plugins/metrics/public/components/custom_color_picker.js index 9a5775876e864..d0c1f9667b271 100644 --- a/src/legacy/core_plugins/metrics/public/components/custom_color_picker.js +++ b/src/legacy/core_plugins/metrics/public/components/custom_color_picker.js @@ -28,7 +28,7 @@ import CompactColor from 'react-color/lib/components/compact/CompactColor'; import color from 'react-color/lib/helpers/color'; import shallowCompare from 'react-addons-shallow-compare'; -export class CustomColorPicker extends Component { +class CustomColorPickerUI extends Component { constructor(props) { super(props); this.handleChange = this.handleChange.bind(this); @@ -82,7 +82,7 @@ export class CustomColorPicker extends Component { }); return ( -
+
{ this.props.onChange([{ value: this.custom && this.custom.value || '' }]); - } + }; - handleChange(selectedOptions) { + handleChange = selectedOptions => { if (selectedOptions.length < 1) { return; } @@ -61,12 +63,12 @@ class DataFormatPicker extends Component { } else if (selectedOptions[0].value === 'duration') { const { from, to, decimals } = this.state; this.props.onChange([{ - value: `${from},${to},${decimals}` + value: `${from},${to},${decimals}`, }]); } else { this.props.onChange(selectedOptions); } - } + }; handleDurationChange(name) { return (selectedOptions) => { @@ -82,11 +84,11 @@ class DataFormatPicker extends Component { } this.setState({ - [name]: newValue + [name]: newValue, }, () => { const { from, to, decimals } = this.state; this.props.onChange([{ - value: `${from},${to},${decimals}` + value: `${from},${to},${decimals}`, }]); }); }; @@ -99,16 +101,31 @@ class DataFormatPicker extends Component { if (!_.includes(['bytes', 'number', 'percent'], value)) { defaultValue = 'custom'; } - if (durationFormatTest.test(value)) { + if (isDuration(value)) { defaultValue = 'duration'; } const { intl } = this.props; const options = [ - { label: intl.formatMessage({ id: 'tsvb.dataFormatPicker.bytesLabel', defaultMessage: 'Bytes' }), value: 'bytes' }, - { label: intl.formatMessage({ id: 'tsvb.dataFormatPicker.numberLabel', defaultMessage: 'Number' }), value: 'number' }, - { label: intl.formatMessage({ id: 'tsvb.dataFormatPicker.percentLabel', defaultMessage: 'Percent' }), value: 'percent' }, - { label: intl.formatMessage({ id: 'tsvb.dataFormatPicker.durationLabel', defaultMessage: 'Duration' }), value: 'duration' }, - { label: intl.formatMessage({ id: 'tsvb.dataFormatPicker.customLabel', defaultMessage: 'Custom' }), value: 'custom' } + { + label: intl.formatMessage({ id: 'tsvb.dataFormatPicker.bytesLabel', defaultMessage: 'Bytes' }), + value: 'bytes', + }, + { + label: intl.formatMessage({ id: 'tsvb.dataFormatPicker.numberLabel', defaultMessage: 'Number' }), + value: 'number', + }, + { + label: intl.formatMessage({ id: 'tsvb.dataFormatPicker.percentLabel', defaultMessage: 'Percent' }), + value: 'percent', + }, + { + label: intl.formatMessage({ id: 'tsvb.dataFormatPicker.durationLabel', defaultMessage: 'Duration' }), + value: 'duration', + }, + { + label: intl.formatMessage({ id: 'tsvb.dataFormatPicker.customLabel', defaultMessage: 'Custom' }), + value: 'custom', + }, ]; const selectedOption = options.find(option => { return defaultValue === option.value; @@ -118,7 +135,7 @@ class DataFormatPicker extends Component { if (defaultValue === 'duration') { const [from, to, decimals] = value.split(','); const selectedFrom = durationInputOptions.find(option => from === option.value); - const selectedTo = durationOutputOptions.find(option => to === option.value); + const selectedTo = durationOutputOptions.find(option => to === option.value); return ( @@ -167,21 +184,26 @@ class DataFormatPicker extends Component { /> - - )} - > - this.decimals = el} - onChange={this.handleDurationChange('decimals')} - /> - - + + {selectedTo && selectedTo.value !== 'humanize' && ( + + )} + > + this.decimals = el} + placeholder={DEFAULT_OUTPUT_PRECISION} + onChange={this.handleDurationChange('decimals')} + /> + + ) + } + ); } @@ -198,7 +220,9 @@ class DataFormatPicker extends Component { Numeral.js) }} + values={{ + numeralJsLink: (Numeral.js), + }} /> } @@ -231,14 +255,14 @@ class DataFormatPicker extends Component { } } -DataFormatPicker.defaultProps = { - label: i18n.translate('tsvb.defaultDataFormatterLabel', { defaultMessage: 'Data Formatter' }) +DataFormatPickerUI.defaultProps = { + label: i18n.translate('tsvb.defaultDataFormatterLabel', { defaultMessage: 'Data Formatter' }), }; -DataFormatPicker.propTypes = { +DataFormatPickerUI.propTypes = { value: PropTypes.string, label: PropTypes.string, - onChange: PropTypes.func + onChange: PropTypes.func, }; -export default injectI18n(DataFormatPicker); +export const DataFormatPicker = injectI18n(DataFormatPickerUI); diff --git a/src/legacy/core_plugins/metrics/public/components/error.js b/src/legacy/core_plugins/metrics/public/components/error.js index bc3272605e579..c8b3b7b94d777 100644 --- a/src/legacy/core_plugins/metrics/public/components/error.js +++ b/src/legacy/core_plugins/metrics/public/components/error.js @@ -25,7 +25,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; const guidPattern = /\[[[a-f\d-\\]{36}\]/g; -function ErrorComponent(props) { +export function ErrorComponent(props) { const { error } = props; let additionalInfo; const type = _.get(error, 'error.caused_by.type') || _.get(error, 'error.type'); @@ -79,5 +79,3 @@ function ErrorComponent(props) { ErrorComponent.propTypes = { error: PropTypes.object }; - -export default ErrorComponent; diff --git a/src/legacy/core_plugins/metrics/public/components/icon_select.js b/src/legacy/core_plugins/metrics/public/components/icon_select.js index 46d9c7f207fcd..e2891381dfce2 100644 --- a/src/legacy/core_plugins/metrics/public/components/icon_select.js +++ b/src/legacy/core_plugins/metrics/public/components/icon_select.js @@ -35,7 +35,7 @@ function renderOption(option) { ); } -function IconSelect(props) { +export function IconSelect(props) { const selectedIcon = props.icons.find(option => { return props.value === option.value; }); @@ -84,5 +84,3 @@ IconSelect.propTypes = { onChange: PropTypes.func, value: PropTypes.string.isRequired }; - -export default IconSelect; diff --git a/src/legacy/core_plugins/metrics/public/components/index_pattern.js b/src/legacy/core_plugins/metrics/public/components/index_pattern.js index 260dc93778f87..231f5e40e2b89 100644 --- a/src/legacy/core_plugins/metrics/public/components/index_pattern.js +++ b/src/legacy/core_plugins/metrics/public/components/index_pattern.js @@ -19,10 +19,10 @@ import PropTypes from 'prop-types'; import React from 'react'; -import FieldSelect from './aggs/field_select'; -import createSelectHandler from './lib/create_select_handler'; -import createTextHandler from './lib/create_text_handler'; -import YesNo from './yes_no'; +import { FieldSelect } from './aggs/field_select'; +import { createSelectHandler } from './lib/create_select_handler'; +import { createTextHandler } from './lib/create_text_handler'; +import { YesNo } from './yes_no'; import { htmlIdGenerator, EuiFieldText, @@ -54,8 +54,9 @@ export const IndexPattern = props => { }; const htmlId = htmlIdGenerator(); - const model = { ...defaults, ...props.model }; + const isDefaultIndexPatternUsed = model.default_index_pattern && !model[indexPatternName]; + return (
@@ -66,10 +67,10 @@ export const IndexPattern = props => { id="tsvb.indexPatternLabel" defaultMessage="Index pattern" />)} - helpText={(model.default_index_pattern && !model[indexPatternName] && )} + />} fullWidth > { onChange={handleSelectChange(timeFieldName)} indexPattern={model[indexPatternName]} fields={fields} + placeholder={isDefaultIndexPatternUsed ? model.default_timefield : undefined} fullWidth /> diff --git a/src/legacy/core_plugins/metrics/public/components/lib/__tests__/calculate_siblings.js b/src/legacy/core_plugins/metrics/public/components/lib/__tests__/calculate_siblings.js index 109fd8b5bad19..d9a0aa09384c9 100644 --- a/src/legacy/core_plugins/metrics/public/components/lib/__tests__/calculate_siblings.js +++ b/src/legacy/core_plugins/metrics/public/components/lib/__tests__/calculate_siblings.js @@ -17,7 +17,7 @@ * under the License. */ -import calculateSiblings from '../calculate_siblings'; +import { calculateSiblings } from '../calculate_siblings'; import { expect } from 'chai'; describe('calculateSiblings(metrics, metric)', () => { diff --git a/src/legacy/core_plugins/metrics/public/components/lib/__tests__/create_number_handler.js b/src/legacy/core_plugins/metrics/public/components/lib/__tests__/create_number_handler.js index 57ec767fd9f91..60cbdf95cc923 100644 --- a/src/legacy/core_plugins/metrics/public/components/lib/__tests__/create_number_handler.js +++ b/src/legacy/core_plugins/metrics/public/components/lib/__tests__/create_number_handler.js @@ -19,7 +19,7 @@ import sinon from 'sinon'; import { expect } from 'chai'; -import createNumberHandler from '../create_number_handler'; +import { createNumberHandler } from '../create_number_handler'; describe('createNumberHandler()', () => { diff --git a/src/legacy/core_plugins/metrics/public/components/lib/__tests__/create_select_handler.js b/src/legacy/core_plugins/metrics/public/components/lib/__tests__/create_select_handler.js index 6e0c86df76c23..14eb9813c0844 100644 --- a/src/legacy/core_plugins/metrics/public/components/lib/__tests__/create_select_handler.js +++ b/src/legacy/core_plugins/metrics/public/components/lib/__tests__/create_select_handler.js @@ -19,7 +19,7 @@ import sinon from 'sinon'; import { expect } from 'chai'; -import createSelectHandler from '../create_select_handler'; +import { createSelectHandler } from '../create_select_handler'; describe('createSelectHandler()', () => { let handleChange; diff --git a/src/legacy/core_plugins/metrics/public/components/lib/__tests__/create_text_handler.js b/src/legacy/core_plugins/metrics/public/components/lib/__tests__/create_text_handler.js index 58a0d4c401d97..dcde7470b1ccb 100644 --- a/src/legacy/core_plugins/metrics/public/components/lib/__tests__/create_text_handler.js +++ b/src/legacy/core_plugins/metrics/public/components/lib/__tests__/create_text_handler.js @@ -19,7 +19,7 @@ import sinon from 'sinon'; import { expect } from 'chai'; -import createTextHandler from '../create_text_handler'; +import { createTextHandler } from '../create_text_handler'; describe('createTextHandler()', () => { diff --git a/src/legacy/core_plugins/metrics/public/components/lib/__tests__/re_id_series.js b/src/legacy/core_plugins/metrics/public/components/lib/__tests__/re_id_series.js index 89be84088eb43..ebe3b820c2031 100644 --- a/src/legacy/core_plugins/metrics/public/components/lib/__tests__/re_id_series.js +++ b/src/legacy/core_plugins/metrics/public/components/lib/__tests__/re_id_series.js @@ -19,7 +19,7 @@ import uuid from 'uuid'; import { expect } from 'chai'; -import reIdSeries from '../re_id_series'; +import { reIdSeries } from '../re_id_series'; describe('reIdSeries()', () => { diff --git a/src/legacy/core_plugins/metrics/public/components/lib/__tests__/replace_vars.js b/src/legacy/core_plugins/metrics/public/components/lib/__tests__/replace_vars.js index f176c3d148e92..0ac7427f6facc 100644 --- a/src/legacy/core_plugins/metrics/public/components/lib/__tests__/replace_vars.js +++ b/src/legacy/core_plugins/metrics/public/components/lib/__tests__/replace_vars.js @@ -18,7 +18,7 @@ */ import { expect } from 'chai'; -import replaceVars from '../replace_vars'; +import { replaceVars } from '../replace_vars'; describe('replaceVars(str, args, vars)', () => { it('replaces vars with values', () => { diff --git a/src/legacy/core_plugins/metrics/public/components/lib/__tests__/tick_formatter.js b/src/legacy/core_plugins/metrics/public/components/lib/__tests__/tick_formatter.js index d36320625b667..71132a2e2969d 100644 --- a/src/legacy/core_plugins/metrics/public/components/lib/__tests__/tick_formatter.js +++ b/src/legacy/core_plugins/metrics/public/components/lib/__tests__/tick_formatter.js @@ -18,7 +18,7 @@ */ import { expect } from 'chai'; -import tickFormatter from '../tick_formatter'; +import { tickFormatter } from '../tick_formatter'; describe('tickFormatter(format, template)', () => { diff --git a/src/legacy/core_plugins/metrics/public/components/lib/agg_to_component.js b/src/legacy/core_plugins/metrics/public/components/lib/agg_to_component.js index d5192ace0669b..ca40d60f20848 100644 --- a/src/legacy/core_plugins/metrics/public/components/lib/agg_to_component.js +++ b/src/legacy/core_plugins/metrics/public/components/lib/agg_to_component.js @@ -19,29 +19,30 @@ import { MovingAverageAgg } from '../aggs/moving_average'; import { DerivativeAgg } from '../aggs/derivative'; -import Calculation from '../aggs/calculation'; -import StdAgg from '../aggs/std_agg'; -import Percentile from '../aggs/percentile'; -import CumulativeSum from '../aggs/cumulative_sum'; +import { CalculationAgg } from '../aggs/calculation'; +import { StandardAgg } from '../aggs/std_agg'; +import { PercentileAgg } from '../aggs/percentile'; +import { CumulativeSumAgg } from '../aggs/cumulative_sum'; import { StandardDeviationAgg } from '../aggs/std_deviation'; import { StandardSiblingAgg } from '../aggs/std_sibling'; -import SeriesAgg from '../aggs/series_agg'; +import { SeriesAgg } from '../aggs/series_agg'; import { SerialDiffAgg } from '../aggs/serial_diff'; import { PositiveOnlyAgg } from '../aggs/positive_only'; import { FilterRatioAgg } from '../aggs/filter_ratio'; import { PercentileRankAgg } from '../aggs/percentile_rank'; import { Static } from '../aggs/static'; -import MathAgg from '../aggs/math'; +import { MathAgg } from '../aggs/math'; import { TopHitAgg } from '../aggs/top_hit'; -export default { - count: StdAgg, - avg: StdAgg, - max: StdAgg, - min: StdAgg, - sum: StdAgg, + +export const aggToComponent = { + count: StandardAgg, + avg: StandardAgg, + max: StandardAgg, + min: StandardAgg, + sum: StandardAgg, std_deviation: StandardDeviationAgg, - sum_of_squares: StdAgg, - variance: StdAgg, + sum_of_squares: StandardAgg, + variance: StandardAgg, avg_bucket: StandardSiblingAgg, max_bucket: StandardSiblingAgg, min_bucket: StandardSiblingAgg, @@ -49,12 +50,12 @@ export default { variance_bucket: StandardSiblingAgg, sum_of_squares_bucket: StandardSiblingAgg, std_deviation_bucket: StandardSiblingAgg, - percentile: Percentile, + percentile: PercentileAgg, percentile_rank: PercentileRankAgg, - cardinality: StdAgg, - value_count: StdAgg, - calculation: Calculation, - cumulative_sum: CumulativeSum, + cardinality: StandardAgg, + value_count: StandardAgg, + calculation: CalculationAgg, + cumulative_sum: CumulativeSumAgg, moving_average: MovingAverageAgg, derivative: DerivativeAgg, series_agg: SeriesAgg, diff --git a/src/legacy/core_plugins/metrics/public/components/lib/calculate_siblings.js b/src/legacy/core_plugins/metrics/public/components/lib/calculate_siblings.js index 9e60545e8d045..3f1ef03e3d68b 100644 --- a/src/legacy/core_plugins/metrics/public/components/lib/calculate_siblings.js +++ b/src/legacy/core_plugins/metrics/public/components/lib/calculate_siblings.js @@ -29,7 +29,7 @@ function getAncestors(siblings, item) { return ancestors; } -export default (siblings, model) => { +export const calculateSiblings = (siblings, model) => { const ancestors = getAncestors(siblings, model); return siblings.filter(row => !_.includes(ancestors, row.id)); }; diff --git a/src/legacy/core_plugins/metrics/public/components/lib/collection_actions.js b/src/legacy/core_plugins/metrics/public/components/lib/collection_actions.js index 632f1d483763c..79cbe98b3d3db 100644 --- a/src/legacy/core_plugins/metrics/public/components/lib/collection_actions.js +++ b/src/legacy/core_plugins/metrics/public/components/lib/collection_actions.js @@ -56,4 +56,4 @@ export function handleAdd(props, fn = newFn) { } } -export default { handleAdd, handleDelete, handleChange }; +export const collectionActions = { handleAdd, handleDelete, handleChange }; diff --git a/src/legacy/core_plugins/metrics/public/components/lib/convert_series_to_vars.js b/src/legacy/core_plugins/metrics/public/components/lib/convert_series_to_vars.js index 2300197cc4c24..e3e07dc8a7265 100644 --- a/src/legacy/core_plugins/metrics/public/components/lib/convert_series_to_vars.js +++ b/src/legacy/core_plugins/metrics/public/components/lib/convert_series_to_vars.js @@ -18,10 +18,11 @@ */ import _ from 'lodash'; -import getLastValue from '../../../common/get_last_value'; -import tickFormatter from './tick_formatter'; +import { getLastValue } from '../../../common/get_last_value'; +import { tickFormatter } from './tick_formatter'; import moment from 'moment'; -export default (series, model, dateFormat = 'lll', getConfig = null) => { + +export const convertSeriesToVars = (series, model, dateFormat = 'lll', getConfig = null) => { const variables = {}; model.series.forEach(seriesModel => { series diff --git a/src/legacy/core_plugins/metrics/public/components/lib/create_change_handler.js b/src/legacy/core_plugins/metrics/public/components/lib/create_change_handler.js index 2997085e8d5b9..f7d4454e0e528 100644 --- a/src/legacy/core_plugins/metrics/public/components/lib/create_change_handler.js +++ b/src/legacy/core_plugins/metrics/public/components/lib/create_change_handler.js @@ -18,7 +18,8 @@ */ import _ from 'lodash'; -export default (handleChange, model) => part => { + +export const createChangeHandler = (handleChange, model) => part => { const doc = _.assign({}, model, part); handleChange(doc); }; diff --git a/src/legacy/core_plugins/metrics/public/components/lib/create_number_handler.js b/src/legacy/core_plugins/metrics/public/components/lib/create_number_handler.js index e023f5d014948..e21a9ffce9c6a 100644 --- a/src/legacy/core_plugins/metrics/public/components/lib/create_number_handler.js +++ b/src/legacy/core_plugins/metrics/public/components/lib/create_number_handler.js @@ -19,7 +19,8 @@ import _ from 'lodash'; import { detectIE } from './detect_ie'; -export default (handleChange) => { + +export const createNumberHandler = (handleChange) => { return (name, defaultValue) => (e) => { if (!detectIE() || e.keyCode === 13) e.preventDefault(); diff --git a/src/legacy/core_plugins/metrics/public/components/lib/create_select_handler.js b/src/legacy/core_plugins/metrics/public/components/lib/create_select_handler.js index 944adccba870d..fc21691882e36 100644 --- a/src/legacy/core_plugins/metrics/public/components/lib/create_select_handler.js +++ b/src/legacy/core_plugins/metrics/public/components/lib/create_select_handler.js @@ -18,7 +18,8 @@ */ import _ from 'lodash'; -export default (handleChange) => { + +export const createSelectHandler = (handleChange) => { return (name) => (selectedOptions) => { if (_.isFunction(handleChange)) { return handleChange({ diff --git a/src/legacy/core_plugins/metrics/public/components/lib/create_text_handler.js b/src/legacy/core_plugins/metrics/public/components/lib/create_text_handler.js index ca2711da9fd81..67715ebd38160 100644 --- a/src/legacy/core_plugins/metrics/public/components/lib/create_text_handler.js +++ b/src/legacy/core_plugins/metrics/public/components/lib/create_text_handler.js @@ -20,7 +20,7 @@ import _ from 'lodash'; import { detectIE } from './detect_ie'; -export default (handleChange) => { +export const createTextHandler = (handleChange) => { return (name, defaultValue) => (e) => { // IE preventDefault breaks input, but we still need top prevent enter from being pressed if (!detectIE() || e.keyCode === 13) e.preventDefault(); diff --git a/src/legacy/core_plugins/metrics/public/components/lib/durations.js b/src/legacy/core_plugins/metrics/public/components/lib/durations.js index dc16a32f2406c..37e619401fb01 100644 --- a/src/legacy/core_plugins/metrics/public/components/lib/durations.js +++ b/src/legacy/core_plugins/metrics/public/components/lib/durations.js @@ -19,53 +19,94 @@ import { i18n } from '@kbn/i18n'; -export const durationOutputOptions = [ +const durationBaseOptions = [ { - label: i18n.translate('tsvb.durationOptions.millisecondsLabel', { defaultMessage: 'milliseconds' }), - value: 'ms' + label: i18n.translate('tsvb.durationOptions.millisecondsLabel', { defaultMessage: 'Milliseconds' }), + value: 'ms', }, { - label: i18n.translate('tsvb.durationOptions.secondsLabel', { defaultMessage: 'seconds' }), - value: 's' + label: i18n.translate('tsvb.durationOptions.secondsLabel', { defaultMessage: 'Seconds' }), + value: 's', }, { - label: i18n.translate('tsvb.durationOptions.minutesLabel', { defaultMessage: 'minutes' }), - value: 'm' + label: i18n.translate('tsvb.durationOptions.minutesLabel', { defaultMessage: 'Minutes' }), + value: 'm', }, { - label: i18n.translate('tsvb.durationOptions.hoursLabel', { defaultMessage: 'hours' }), - value: 'h' + label: i18n.translate('tsvb.durationOptions.hoursLabel', { defaultMessage: 'Hours' }), + value: 'h', }, { - label: i18n.translate('tsvb.durationOptions.daysLabel', { defaultMessage: 'days' }), - value: 'd' + label: i18n.translate('tsvb.durationOptions.daysLabel', { defaultMessage: 'Days' }), + value: 'd', }, { - label: i18n.translate('tsvb.durationOptions.weeksLabel', { defaultMessage: 'weeks' }), - value: 'w' + label: i18n.translate('tsvb.durationOptions.weeksLabel', { defaultMessage: 'Weeks' }), + value: 'w', }, { - label: i18n.translate('tsvb.durationOptions.monthsLabel', { defaultMessage: 'months' }), - value: 'M' + label: i18n.translate('tsvb.durationOptions.monthsLabel', { defaultMessage: 'Months' }), + value: 'M', }, { - label: i18n.translate('tsvb.durationOptions.yearsLabel', { defaultMessage: 'years' }), - value: 'Y' - } + label: i18n.translate('tsvb.durationOptions.yearsLabel', { defaultMessage: 'Years' }), + value: 'Y', + }, +]; + +export const durationOutputOptions = [ + { + label: i18n.translate('tsvb.durationOptions.humanize', { defaultMessage: 'Human readable' }), + value: 'humanize', + }, + ...durationBaseOptions, ]; export const durationInputOptions = [ { - label: i18n.translate('tsvb.durationOptions.picosecondsLabel', { defaultMessage: 'picoseconds' }), - value: 'ps' + label: i18n.translate('tsvb.durationOptions.picosecondsLabel', { defaultMessage: 'Picoseconds' }), + value: 'ps', }, { - label: i18n.translate('tsvb.durationOptions.nanosecondsLabel', { defaultMessage: 'nanoseconds' }), - value: 'ns' + label: i18n.translate('tsvb.durationOptions.nanosecondsLabel', { defaultMessage: 'Nanoseconds' }), + value: 'ns', }, { - label: i18n.translate('tsvb.durationOptions.microsecondsLabel', { defaultMessage: 'microseconds' }), - value: 'us' }, - ...durationOutputOptions + label: i18n.translate('tsvb.durationOptions.microsecondsLabel', { defaultMessage: 'Microseconds' }), + value: 'us', + }, + ...durationBaseOptions, ]; +export const inputFormats = { + 'ps': 'picoseconds', + 'ns': 'nanoseconds', + 'us': 'microseconds', + 'ms': 'milliseconds', + 's': 'seconds', + 'm': 'minutes', + 'h': 'hours', + 'd': 'days', + 'w': 'weeks', + 'M': 'months', + 'Y': 'years', +}; + +export const outputFormats = { + 'humanize': 'humanize', + 'ms': 'asMilliseconds', + 's': 'asSeconds', + 'm': 'asMinutes', + 'h': 'asHours', + 'd': 'asDays', + 'w': 'asWeeks', + 'M': 'asMonths', + 'Y': 'asYears', +}; + +export const isDuration = format => { + const splittedFormat = format.split(','); + const [input, output] = splittedFormat; + + return Boolean(inputFormats[input] && outputFormats[output]) && splittedFormat.length === 3; +}; diff --git a/src/legacy/core_plugins/metrics/public/components/lib/durations.test.js b/src/legacy/core_plugins/metrics/public/components/lib/durations.test.js new file mode 100644 index 0000000000000..cd1f6f4bb7f63 --- /dev/null +++ b/src/legacy/core_plugins/metrics/public/components/lib/durations.test.js @@ -0,0 +1,39 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { isDuration } from './durations'; + +describe('durations', () => { + describe('isDuration', () => { + test('should return true for valid duration formats', () => { + expect(isDuration('ps,m,2')).toBeTruthy(); + expect(isDuration('h,h,1')).toBeTruthy(); + expect(isDuration('m,d,')).toBeTruthy(); + expect(isDuration('s,Y,4')).toBeTruthy(); + expect(isDuration('ps,humanize,')).toBeTruthy(); + }); + + test('should return false for invalid duration formats', () => { + expect(isDuration('ps,j,2')).toBeFalsy(); + expect(isDuration('i,h,1')).toBeFalsy(); + expect(isDuration('m,d')).toBeFalsy(); + expect(isDuration('s')).toBeFalsy(); + expect(isDuration('humanize,s,2')).toBeFalsy(); + }); + }); +}); diff --git a/src/legacy/core_plugins/metrics/public/components/lib/get_default_query_language.js b/src/legacy/core_plugins/metrics/public/components/lib/get_default_query_language.js new file mode 100644 index 0000000000000..7c690d775014a --- /dev/null +++ b/src/legacy/core_plugins/metrics/public/components/lib/get_default_query_language.js @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + + +import chrome from 'ui/chrome'; + +export function getDefaultQueryLanguage() { + return chrome.getUiSettingsClient().get('search:queryLanguage'); +} diff --git a/src/legacy/core_plugins/metrics/public/components/lib/get_interval.js b/src/legacy/core_plugins/metrics/public/components/lib/get_interval.js index 252173a87f1ad..7884856822503 100644 --- a/src/legacy/core_plugins/metrics/public/components/lib/get_interval.js +++ b/src/legacy/core_plugins/metrics/public/components/lib/get_interval.js @@ -23,6 +23,8 @@ import { relativeOptions } from '../../../../../ui/public/timepicker/relative_op import { GTE_INTERVAL_RE, INTERVAL_STRING_RE } from '../../../common/interval_regexp'; +export const AUTO_INTERVAL = 'auto'; + export const unitLookup = { s: i18n.translate('tsvb.getInterval.secondsLabel', { defaultMessage: 'seconds' }), m: i18n.translate('tsvb.getInterval.minutesLabel', { defaultMessage: 'minutes' }), @@ -53,7 +55,7 @@ export const isGteInterval = (interval) => GTE_INTERVAL_RE.test(interval); export const isIntervalValid = (interval) => { return isString(interval) && - (interval === 'auto' || INTERVAL_STRING_RE.test(interval) || isGteInterval(interval)); + (interval === AUTO_INTERVAL || INTERVAL_STRING_RE.test(interval) || isGteInterval(interval)); }; export const getInterval = (visData, model) => { diff --git a/src/legacy/core_plugins/metrics/public/components/lib/new_metric_agg_fn.js b/src/legacy/core_plugins/metrics/public/components/lib/new_metric_agg_fn.js index 9793d53baa874..7b6963d479ba9 100644 --- a/src/legacy/core_plugins/metrics/public/components/lib/new_metric_agg_fn.js +++ b/src/legacy/core_plugins/metrics/public/components/lib/new_metric_agg_fn.js @@ -18,7 +18,8 @@ */ import uuid from 'uuid'; -export default () => { + +export const newMetricAggFn = () => { return { id: uuid.v1(), type: 'count' diff --git a/src/legacy/core_plugins/metrics/public/components/lib/new_series_fn.js b/src/legacy/core_plugins/metrics/public/components/lib/new_series_fn.js index d23057b617d51..a38fdb4cc1639 100644 --- a/src/legacy/core_plugins/metrics/public/components/lib/new_series_fn.js +++ b/src/legacy/core_plugins/metrics/public/components/lib/new_series_fn.js @@ -19,8 +19,9 @@ import uuid from 'uuid'; import _ from 'lodash'; -import newMetricAggFn from './new_metric_agg_fn'; -export default (obj = {}) => { +import { newMetricAggFn } from './new_metric_agg_fn'; + +export const newSeriesFn = (obj = {}) => { return _.assign({ id: uuid.v1(), color: '#68BC00', diff --git a/src/legacy/core_plugins/metrics/public/components/lib/re_id_series.js b/src/legacy/core_plugins/metrics/public/components/lib/re_id_series.js index 6c10ca33044e9..072f9c66b6c07 100644 --- a/src/legacy/core_plugins/metrics/public/components/lib/re_id_series.js +++ b/src/legacy/core_plugins/metrics/public/components/lib/re_id_series.js @@ -19,7 +19,8 @@ import uuid from 'uuid'; import _ from 'lodash'; -export default source => { + +export const reIdSeries = source => { const series = _.cloneDeep(source); series.id = uuid.v1(); series.metrics.forEach((metric) => { diff --git a/src/legacy/core_plugins/metrics/public/components/lib/replace_vars.js b/src/legacy/core_plugins/metrics/public/components/lib/replace_vars.js index f1503ca782b95..cf26f4b3508b7 100644 --- a/src/legacy/core_plugins/metrics/public/components/lib/replace_vars.js +++ b/src/legacy/core_plugins/metrics/public/components/lib/replace_vars.js @@ -21,7 +21,7 @@ import _ from 'lodash'; import handlebars from 'handlebars/dist/handlebars'; import { i18n } from '@kbn/i18n'; -export default function replaceVars(str, args = {}, vars = {}) { +export function replaceVars(str, args = {}, vars = {}) { try { const template = handlebars.compile(str, { strict: true, knownHelpersOnly: true }); diff --git a/src/legacy/core_plugins/metrics/public/components/lib/series_change_handler.js b/src/legacy/core_plugins/metrics/public/components/lib/series_change_handler.js index ac98f77c66ae7..1d0bc5b5213c0 100644 --- a/src/legacy/core_plugins/metrics/public/components/lib/series_change_handler.js +++ b/src/legacy/core_plugins/metrics/public/components/lib/series_change_handler.js @@ -18,13 +18,14 @@ */ import _ from 'lodash'; -import newMetricAggFn from './new_metric_agg_fn'; +import { newMetricAggFn } from './new_metric_agg_fn'; import { isBasicAgg } from '../../../common/agg_lookup'; import { handleAdd, handleChange } from './collection_actions'; -export default (props, items) => doc => { + +export const seriesChangeHandler = (props, items) => doc => { // If we only have one sibling and the user changes to a pipeline // agg we are going to add the pipeline instead of changing the // current item. diff --git a/src/legacy/core_plugins/metrics/public/components/lib/tick_formatter.js b/src/legacy/core_plugins/metrics/public/components/lib/tick_formatter.js index 96ebee73e3ca8..cea8ad5ec0dbc 100644 --- a/src/legacy/core_plugins/metrics/public/components/lib/tick_formatter.js +++ b/src/legacy/core_plugins/metrics/public/components/lib/tick_formatter.js @@ -18,36 +18,29 @@ */ import handlebars from 'handlebars/dist/handlebars'; -import { durationInputOptions } from './durations'; -import { capitalize, isNumber } from 'lodash'; +import { isNumber } from 'lodash'; import { fieldFormats } from 'ui/registry/field_formats'; +import { inputFormats, outputFormats, isDuration } from '../lib/durations'; -const durationsLookup = durationInputOptions.reduce((acc, row) => { - acc[row.value] = row.label; - return acc; -}, {}); - -export default (format = '0,0.[00]', template, getConfig = null) => { +export const tickFormatter = (format = '0,0.[00]', template, getConfig = null) => { if (!template) template = '{{value}}'; const render = handlebars.compile(template, { knownHelpersOnly: true }); - const durationFormatTest = /[pnumshdwMY]+,[pnumshdwMY]+,\d+/; let formatter; - if (durationFormatTest.test(format)) { + + if (isDuration(format)) { const [from, to, decimals] = format.split(','); - const inputFormat = durationsLookup[from]; - const outputFormat = `as${capitalize(durationsLookup[to])}`; const DurationFormat = fieldFormats.getType('duration'); + formatter = new DurationFormat({ - inputFormat, - outputFormat, - outputPrecision: decimals + inputFormat: inputFormats[from], + outputFormat: outputFormats[to], + outputPrecision: decimals, }); } else { let FieldFormat = fieldFormats.getType(format); if (FieldFormat) { formatter = new FieldFormat(null, getConfig); - } - else { + } else { FieldFormat = fieldFormats.getType('number'); formatter = new FieldFormat({ pattern: format }, getConfig); } diff --git a/src/legacy/core_plugins/metrics/public/components/markdown_editor.js b/src/legacy/core_plugins/metrics/public/components/markdown_editor.js index b96db46aa3913..0426497fb37bf 100644 --- a/src/legacy/core_plugins/metrics/public/components/markdown_editor.js +++ b/src/legacy/core_plugins/metrics/public/components/markdown_editor.js @@ -24,8 +24,8 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import tickFormatter from './lib/tick_formatter'; -import convertSeriesToVars from './lib/convert_series_to_vars'; +import { tickFormatter } from './lib/tick_formatter'; +import { convertSeriesToVars } from './lib/convert_series_to_vars'; import _ from 'lodash'; import 'brace/mode/markdown'; import 'brace/theme/github'; @@ -40,7 +40,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; -class MarkdownEditor extends Component { +export class MarkdownEditor extends Component { state = { visData: null, }; @@ -234,5 +234,3 @@ MarkdownEditor.propTypes = { dateFormat: PropTypes.string, visData$: PropTypes.object, }; - -export default MarkdownEditor; diff --git a/src/legacy/core_plugins/metrics/public/components/no_data.js b/src/legacy/core_plugins/metrics/public/components/no_data.js index d00a7a83a6959..b1bf8da710deb 100644 --- a/src/legacy/core_plugins/metrics/public/components/no_data.js +++ b/src/legacy/core_plugins/metrics/public/components/no_data.js @@ -21,7 +21,7 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; -function NoDataComponent() { +export function NoDataComponent() { return (
@@ -39,4 +39,3 @@ function NoDataComponent() {
); } -export default NoDataComponent; diff --git a/src/legacy/core_plugins/metrics/public/components/panel_config.js b/src/legacy/core_plugins/metrics/public/components/panel_config.js index c45c83d5644c2..f889285d94a45 100644 --- a/src/legacy/core_plugins/metrics/public/components/panel_config.js +++ b/src/legacy/core_plugins/metrics/public/components/panel_config.js @@ -19,12 +19,12 @@ import PropTypes from 'prop-types'; import React from 'react'; -import timeseries from './panel_config/timeseries'; -import metric from './panel_config/metric'; -import topN from './panel_config/top_n'; -import table from './panel_config/table'; -import gauge from './panel_config/gauge'; -import markdown from './panel_config/markdown'; +import { TimeseriesPanelConfig as timeseries } from './panel_config/timeseries'; +import { MetricPanelConfig as metric } from './panel_config/metric'; +import { TopNPanelConfig as topN } from './panel_config/top_n'; +import { TablePanelConfig as table } from './panel_config/table'; +import { GaugePanelConfig as gauge } from './panel_config/gauge'; +import { MarkdownPanelConfig as markdown } from './panel_config/markdown'; import { FormattedMessage } from '@kbn/i18n/react'; const types = { @@ -36,7 +36,7 @@ const types = { markdown }; -function PanelConfig(props) { +export function PanelConfig(props) { const { model } = props; const component = types[model.type]; if (component) { @@ -59,5 +59,3 @@ PanelConfig.propTypes = { dateFormat: PropTypes.string, visData$: PropTypes.object, }; - -export default PanelConfig; diff --git a/src/legacy/core_plugins/metrics/public/components/panel_config/gauge.js b/src/legacy/core_plugins/metrics/public/components/panel_config/gauge.js index ebc904e1894d3..8368142532c7b 100644 --- a/src/legacy/core_plugins/metrics/public/components/panel_config/gauge.js +++ b/src/legacy/core_plugins/metrics/public/components/panel_config/gauge.js @@ -19,14 +19,14 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import SeriesEditor from '../series_editor'; +import { SeriesEditor } from '../series_editor'; import { IndexPattern } from '../index_pattern'; -import createSelectHandler from '../lib/create_select_handler'; -import createTextHandler from '../lib/create_text_handler'; -import ColorRules from '../color_rules'; -import ColorPicker from '../color_picker'; +import { createSelectHandler } from '../lib/create_select_handler'; +import { createTextHandler } from '../lib/create_text_handler'; +import { ColorRules } from '../color_rules'; +import { ColorPicker } from '../color_picker'; import uuid from 'uuid'; -import YesNo from '../yes_no'; +import { YesNo } from '../yes_no'; import { htmlIdGenerator, EuiComboBox, @@ -38,15 +38,18 @@ import { EuiFormRow, EuiFormLabel, EuiSpacer, - EuiFieldText, EuiFieldNumber, EuiTitle, EuiHorizontalRule, } from '@elastic/eui'; import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { Storage } from 'ui/storage'; +import { data } from 'plugins/data'; +const { QueryBarInput } = data.query.ui; +const localStorage = new Storage(window.localStorage); +import { getDefaultQueryLanguage } from '../lib/get_default_query_language'; class GaugePanelConfigUi extends Component { - constructor(props) { super(props); this.state = { selectedTab: 'data' }; @@ -74,7 +77,7 @@ class GaugePanelConfigUi extends Component { const { intl } = this.props; const defaults = { gauge_max: '', - filter: '', + filter: { query: '', language: getDefaultQueryLanguage() }, gauge_style: 'circle', gauge_inner_width: '', gauge_width: '' @@ -143,10 +146,15 @@ class GaugePanelConfigUi extends Component { />)} fullWidth > - this.props.onChange({ filter })} + appName={'VisEditor'} + indexPatterns={[model.index_pattern || model.default_index_pattern]} + store={localStorage} /> @@ -344,5 +352,4 @@ GaugePanelConfigUi.propTypes = { visData$: PropTypes.object, }; -const GaugePanelConfig = injectI18n(GaugePanelConfigUi); -export default GaugePanelConfig; +export const GaugePanelConfig = injectI18n(GaugePanelConfigUi); diff --git a/src/legacy/core_plugins/metrics/public/components/panel_config/markdown.js b/src/legacy/core_plugins/metrics/public/components/panel_config/markdown.js index bcad32017c6b4..4d7796c30e32e 100644 --- a/src/legacy/core_plugins/metrics/public/components/panel_config/markdown.js +++ b/src/legacy/core_plugins/metrics/public/components/panel_config/markdown.js @@ -19,14 +19,13 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import SeriesEditor from '../series_editor'; +import { SeriesEditor } from '../series_editor'; import { IndexPattern } from '../index_pattern'; import 'brace/mode/less'; -import createSelectHandler from '../lib/create_select_handler'; -import createTextHandler from '../lib/create_text_handler'; -import ColorPicker from '../color_picker'; -import YesNo from '../yes_no'; -import MarkdownEditor from '../markdown_editor'; +import { createSelectHandler } from '../lib/create_select_handler'; +import { ColorPicker } from '../color_picker'; +import { YesNo } from '../yes_no'; +import { MarkdownEditor } from '../markdown_editor'; import less from 'less/lib/less-browser'; import { htmlIdGenerator, @@ -39,16 +38,19 @@ import { EuiFormRow, EuiFormLabel, EuiSpacer, - EuiFieldText, EuiTitle, EuiHorizontalRule, EuiCodeEditor, } from '@elastic/eui'; const lessC = less(window, { env: 'production' }); import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { Storage } from 'ui/storage'; +import { data } from 'plugins/data'; +import { getDefaultQueryLanguage } from '../lib/get_default_query_language'; +const { QueryBarInput } = data.query.ui; +const localStorage = new Storage(window.localStorage); class MarkdownPanelConfigUi extends Component { - constructor(props) { super(props); this.state = { selectedTab: 'markdown' }; @@ -74,11 +76,10 @@ class MarkdownPanelConfigUi extends Component { } render() { - const defaults = { filter: '' }; + const defaults = { filter: { query: '', language: getDefaultQueryLanguage() } }; const model = { ...defaults, ...this.props.model }; const { selectedTab } = this.state; const handleSelectChange = createSelectHandler(this.props.onChange); - const handleTextChange = createTextHandler(this.props.onChange); const { intl } = this.props; const htmlId = htmlIdGenerator(); @@ -146,10 +147,15 @@ class MarkdownPanelConfigUi extends Component { />)} fullWidth > - this.props.onChange({ filter })} + appName={'VisEditor'} + indexPatterns={[model.index_pattern || model.default_index_pattern]} + store={localStorage} /> @@ -320,5 +326,4 @@ MarkdownPanelConfigUi.propTypes = { visData$: PropTypes.object, }; -const MarkdownPanelConfig = injectI18n(MarkdownPanelConfigUi); -export default MarkdownPanelConfig; +export const MarkdownPanelConfig = injectI18n(MarkdownPanelConfigUi); diff --git a/src/legacy/core_plugins/metrics/public/components/panel_config/metric.js b/src/legacy/core_plugins/metrics/public/components/panel_config/metric.js index edc7e8cd84f1b..2ba4ab69d0a36 100644 --- a/src/legacy/core_plugins/metrics/public/components/panel_config/metric.js +++ b/src/legacy/core_plugins/metrics/public/components/panel_config/metric.js @@ -19,11 +19,10 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import SeriesEditor from '../series_editor'; +import { SeriesEditor } from '../series_editor'; import { IndexPattern } from '../index_pattern'; -import createTextHandler from '../lib/create_text_handler'; -import ColorRules from '../color_rules'; -import YesNo from '../yes_no'; +import { ColorRules } from '../color_rules'; +import { YesNo } from '../yes_no'; import uuid from 'uuid'; import { htmlIdGenerator, @@ -35,14 +34,18 @@ import { EuiFormRow, EuiFormLabel, EuiSpacer, - EuiFieldText, EuiTitle, EuiHorizontalRule, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -class MetricPanelConfig extends Component { +import { Storage } from 'ui/storage'; +import { data } from 'plugins/data'; +import { getDefaultQueryLanguage } from '../lib/get_default_query_language'; +const { QueryBarInput } = data.query.ui; +const localStorage = new Storage(window.localStorage); +export class MetricPanelConfig extends Component { constructor(props) { super(props); this.state = { selectedTab: 'data' }; @@ -63,10 +66,9 @@ class MetricPanelConfig extends Component { render() { const { selectedTab } = this.state; - const defaults = { filter: '' }; + const defaults = { filter: { query: '', language: getDefaultQueryLanguage() } }; const model = { ...defaults, ...this.props.model }; const htmlId = htmlIdGenerator(); - const handleTextChange = createTextHandler(this.props.onChange); let view; if (selectedTab === 'data') { view = ( @@ -112,10 +114,15 @@ class MetricPanelConfig extends Component { />)} fullWidth > - this.props.onChange({ filter })} + appName={'VisEditor'} + indexPatterns={[model.index_pattern || model.default_index_pattern]} + store={localStorage} /> @@ -194,5 +201,3 @@ MetricPanelConfig.propTypes = { onChange: PropTypes.func, visData$: PropTypes.object, }; - -export default MetricPanelConfig; diff --git a/src/legacy/core_plugins/metrics/public/components/panel_config/table.js b/src/legacy/core_plugins/metrics/public/components/panel_config/table.js index cc993d520cede..b88efb1be2af2 100644 --- a/src/legacy/core_plugins/metrics/public/components/panel_config/table.js +++ b/src/legacy/core_plugins/metrics/public/components/panel_config/table.js @@ -19,13 +19,13 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import FieldSelect from '../aggs/field_select'; -import SeriesEditor from '../series_editor'; +import { FieldSelect } from '../aggs/field_select'; +import { SeriesEditor } from '../series_editor'; import { IndexPattern } from '../index_pattern'; -import createTextHandler from '../lib/create_text_handler'; +import { createTextHandler } from '../lib/create_text_handler'; import { get } from 'lodash'; import uuid from 'uuid'; -import YesNo from '../yes_no'; +import { YesNo } from '../yes_no'; import { htmlIdGenerator, EuiTabs, @@ -43,9 +43,12 @@ import { EuiText, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; - -class TablePanelConfig extends Component { - +import { Storage } from 'ui/storage'; +import { data } from 'plugins/data'; +import { getDefaultQueryLanguage } from '../lib/get_default_query_language'; +const { QueryBarInput } = data.query.ui; +const localStorage = new Storage(window.localStorage); +export class TablePanelConfig extends Component { constructor(props) { super(props); this.state = { selectedTab: 'data' }; @@ -78,7 +81,13 @@ class TablePanelConfig extends Component { render() { const { selectedTab } = this.state; - const defaults = { drilldown_url: '', filter: '', pivot_label: '', pivot_rows: 10, pivot_type: '' }; + const defaults = { + drilldown_url: '', + filter: { query: '', language: getDefaultQueryLanguage() }, + pivot_label: '', + pivot_rows: 10, + pivot_type: '', + }; const model = { ...defaults, ...this.props.model }; const handleTextChange = createTextHandler(this.props.onChange); const htmlId = htmlIdGenerator(); @@ -221,10 +230,15 @@ class TablePanelConfig extends Component { />)} fullWidth > - this.props.onChange({ filter })} + appName={'VisEditor'} + indexPatterns={[model.index_pattern || model.default_index_pattern]} + store={localStorage} /> @@ -283,5 +297,3 @@ TablePanelConfig.propTypes = { onChange: PropTypes.func, visData$: PropTypes.object, }; - -export default TablePanelConfig; diff --git a/src/legacy/core_plugins/metrics/public/components/panel_config/timeseries.js b/src/legacy/core_plugins/metrics/public/components/panel_config/timeseries.js index 2f1d23c5211ed..2f6ebfdea5002 100644 --- a/src/legacy/core_plugins/metrics/public/components/panel_config/timeseries.js +++ b/src/legacy/core_plugins/metrics/public/components/panel_config/timeseries.js @@ -19,13 +19,13 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import SeriesEditor from '../series_editor'; -import AnnotationsEditor from '../annotations_editor'; +import { SeriesEditor } from '../series_editor'; +import { AnnotationsEditor } from '../annotations_editor'; import { IndexPattern } from '../index_pattern'; -import createSelectHandler from '../lib/create_select_handler'; -import createTextHandler from '../lib/create_text_handler'; -import ColorPicker from '../color_picker'; -import YesNo from '../yes_no'; +import { createSelectHandler } from '../lib/create_select_handler'; +import { createTextHandler } from '../lib/create_text_handler'; +import { ColorPicker } from '../color_picker'; +import { YesNo } from '../yes_no'; import { htmlIdGenerator, EuiComboBox, @@ -42,9 +42,12 @@ import { EuiHorizontalRule, } from '@elastic/eui'; import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; - +import { Storage } from 'ui/storage'; +import { data } from 'plugins/data'; +import { getDefaultQueryLanguage } from '../lib/get_default_query_language'; +const { QueryBarInput } = data.query.ui; +const localStorage = new Storage(window.localStorage); class TimeseriesPanelConfigUi extends Component { - constructor(props) { super(props); this.state = { selectedTab: 'data' }; @@ -56,7 +59,7 @@ class TimeseriesPanelConfigUi extends Component { render() { const defaults = { - filter: '', + filter: { query: '', language: getDefaultQueryLanguage() }, axis_max: '', axis_min: '', legend_position: 'right', @@ -89,7 +92,8 @@ class TimeseriesPanelConfigUi extends Component { }, { label: intl.formatMessage({ id: 'tsvb.timeseries.scaleOptions.logLabel', defaultMessage: 'Log' }), - value: 'log' } + value: 'log' + } ]; const selectedAxisScaleOption = scaleOptions.find(option => { return model.axis_scale === option.value; @@ -164,10 +168,15 @@ class TimeseriesPanelConfigUi extends Component { />)} fullWidth > - this.props.onChange({ filter })} + appName={'VisEditor'} + indexPatterns={[model.index_pattern || model.default_index_pattern]} + store={localStorage} /> @@ -372,8 +381,6 @@ class TimeseriesPanelConfigUi extends Component {
); } - - } TimeseriesPanelConfigUi.propTypes = { @@ -383,5 +390,4 @@ TimeseriesPanelConfigUi.propTypes = { visData$: PropTypes.object, }; -const TimeseriesPanelConfig = injectI18n(TimeseriesPanelConfigUi); -export default TimeseriesPanelConfig; +export const TimeseriesPanelConfig = injectI18n(TimeseriesPanelConfigUi); diff --git a/src/legacy/core_plugins/metrics/public/components/panel_config/top_n.js b/src/legacy/core_plugins/metrics/public/components/panel_config/top_n.js index de9bc7284c471..2da914bcd2738 100644 --- a/src/legacy/core_plugins/metrics/public/components/panel_config/top_n.js +++ b/src/legacy/core_plugins/metrics/public/components/panel_config/top_n.js @@ -19,13 +19,13 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import SeriesEditor from '../series_editor'; +import { SeriesEditor } from '../series_editor'; import { IndexPattern } from '../index_pattern'; -import createTextHandler from '../lib/create_text_handler'; -import ColorRules from '../color_rules'; -import ColorPicker from '../color_picker'; +import { createTextHandler } from '../lib/create_text_handler'; +import { ColorRules } from '../color_rules'; +import { ColorPicker } from '../color_picker'; import uuid from 'uuid'; -import YesNo from '../yes_no'; +import { YesNo } from '../yes_no'; import { htmlIdGenerator, EuiTabs, @@ -42,8 +42,13 @@ import { EuiCode, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { Storage } from 'ui/storage'; +import { data } from 'plugins/data'; +import { getDefaultQueryLanguage } from '../lib/get_default_query_language'; +const { QueryBarInput } = data.query.ui; +const localStorage = new Storage(window.localStorage); -class TopNPanelConfig extends Component { +export class TopNPanelConfig extends Component { constructor(props) { super(props); @@ -65,7 +70,7 @@ class TopNPanelConfig extends Component { render() { const { selectedTab } = this.state; - const defaults = { drilldown_url: '', filter: '' }; + const defaults = { drilldown_url: '', filter: { query: '', language: getDefaultQueryLanguage() } }; const model = { ...defaults, ...this.props.model }; const htmlId = htmlIdGenerator(); const handleTextChange = createTextHandler(this.props.onChange); @@ -136,10 +141,15 @@ class TopNPanelConfig extends Component { />)} fullWidth > - this.props.onChange({ filter })} + appName={'VisEditor'} + indexPatterns={[model.index_pattern || model.default_index_pattern]} + store={localStorage} /> @@ -249,5 +259,3 @@ TopNPanelConfig.propTypes = { onChange: PropTypes.func, visData$: PropTypes.object, }; - -export default TopNPanelConfig; diff --git a/src/legacy/core_plugins/metrics/public/components/series.js b/src/legacy/core_plugins/metrics/public/components/series.js index cd4af2ea63e4b..880bf4557d00a 100644 --- a/src/legacy/core_plugins/metrics/public/components/series.js +++ b/src/legacy/core_plugins/metrics/public/components/series.js @@ -21,12 +21,12 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { assign, get } from 'lodash'; -import timeseries from './vis_types/timeseries/series'; -import metric from './vis_types/metric/series'; -import topN from './vis_types/top_n/series'; -import table from './vis_types/table/series'; -import gauge from './vis_types/gauge/series'; -import markdown from './vis_types/markdown/series'; +import { TimeseriesSeries as timeseries } from './vis_types/timeseries/series'; +import { MetricSeries as metric } from './vis_types/metric/series'; +import { TopNSeries as topN } from './vis_types/top_n/series'; +import { TableSeries as table } from './vis_types/table/series'; +import { GaugeSeries as gauge } from './vis_types/gauge/series'; +import { MarkdownSeries as markdown } from './vis_types/markdown/series'; import { FormattedMessage } from '@kbn/i18n/react'; const lookup = { @@ -38,7 +38,7 @@ const lookup = { markdown, }; -class Series extends Component { +export class Series extends Component { constructor(props) { super(props); @@ -112,8 +112,8 @@ class Series extends Component { togglePanelActivation: this.togglePanelActivation, visible: this.state.visible, dragHandleProps: this.props.dragHandleProps, + indexPatternForQuery: panel.index_pattern || panel.default_index_pattern, }; - return Boolean(Component) ? () : ( { const defaults = { offset_time: '', value_template: '' }; @@ -43,6 +48,8 @@ export const SeriesConfig = props => { const handleSelectChange = createSelectHandler(props.onChange); const handleTextChange = createTextHandler(props.onChange); const htmlId = htmlIdGenerator(); + const seriesIndexPattern = (props.model.override_index_pattern && props.model.series_index_pattern) ? + props.model.series_index_pattern : props.indexPatternForQuery; return (
@@ -62,10 +69,16 @@ export const SeriesConfig = props => { />)} fullWidth > - props.onChange({ filter })} + appName={'VisEditor'} + indexPatterns={[seriesIndexPattern]} + store={localStorage} + showDatePicker={false} /> @@ -150,5 +163,6 @@ export const SeriesConfig = props => { SeriesConfig.propTypes = { fields: PropTypes.object, model: PropTypes.object, - onChange: PropTypes.func + onChange: PropTypes.func, + indexPatternForQuery: PropTypes.string, }; diff --git a/src/legacy/core_plugins/metrics/public/components/series_editor.js b/src/legacy/core_plugins/metrics/public/components/series_editor.js index fd82faca79fa9..d294bfd417d50 100644 --- a/src/legacy/core_plugins/metrics/public/components/series_editor.js +++ b/src/legacy/core_plugins/metrics/public/components/series_editor.js @@ -20,20 +20,20 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { find } from 'lodash'; -import reIdSeries from './lib/re_id_series'; -import Series from './series'; +import { reIdSeries } from './lib/re_id_series'; +import { Series } from './series'; import { handleAdd, handleDelete, handleChange, } from './lib/collection_actions'; -import newSeriesFn from './lib/new_series_fn'; +import { newSeriesFn } from './lib/new_series_fn'; import { EuiDragDropContext, EuiDroppable, EuiDraggable } from '@elastic/eui'; import { reorder } from './lib/reorder'; const DROPPABLE_ID = 'series_editor_dnd'; -class SeriesEditor extends Component { +export class SeriesEditor extends Component { handleClone = series => { const newSeries = reIdSeries(series); @@ -141,5 +141,3 @@ SeriesEditor.propTypes = { onChange: PropTypes.func, visData$: PropTypes.object, }; - -export default SeriesEditor; diff --git a/src/legacy/core_plugins/metrics/public/components/split.js b/src/legacy/core_plugins/metrics/public/components/split.js index c6e4de601510d..5b68083e736f3 100644 --- a/src/legacy/core_plugins/metrics/public/components/split.js +++ b/src/legacy/core_plugins/metrics/public/components/split.js @@ -21,13 +21,13 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import uuid from 'uuid'; import { get } from 'lodash'; - import { SplitByTerms } from './splits/terms'; import { SplitByFilter } from './splits/filter'; import { SplitByFilters } from './splits/filters'; import { SplitByEverything } from './splits/everything'; import { SplitUnsupported } from './splits/unsupported_split'; import { isGroupByFieldsEnabled } from '../lib/check_ui_restrictions'; +import { getDefaultQueryLanguage } from './lib/get_default_query_language'; const SPLIT_MODES = { FILTERS: 'filters', @@ -36,13 +36,21 @@ const SPLIT_MODES = { EVERYTHING: 'everything', }; -class Split extends Component { +export class Split extends Component { + componentWillReceiveProps(nextProps) { const { model } = nextProps; if (model.split_mode === 'filters' && !model.split_filters) { this.props.onChange({ split_filters: [ - { color: model.color, id: uuid.v1() }, + { + color: model.color, + id: uuid.v1(), + filter: { + query: '', + language: getDefaultQueryLanguage() + } + }, ], }); } @@ -67,9 +75,8 @@ class Split extends Component { render() { const { model, panel, uiRestrictions } = this.props; - const indexPattern = model.override_index_pattern && - model.series_index_pattern || - panel.index_pattern; + const indexPattern = (model.override_index_pattern && model.series_index_pattern) || + (panel.index_pattern || panel.default_index_pattern); const splitMode = get(this.props, 'model.split_mode', SPLIT_MODES.EVERYTHING); @@ -82,7 +89,8 @@ class Split extends Component { fields={this.props.fields} onChange={this.props.onChange} uiRestrictions={uiRestrictions} - />); + /> + ); } } @@ -92,5 +100,3 @@ Split.propTypes = { onChange: PropTypes.func, panel: PropTypes.object, }; - -export default Split; diff --git a/src/legacy/core_plugins/metrics/public/components/splits/everything.js b/src/legacy/core_plugins/metrics/public/components/splits/everything.js index e4ab89579e526..51274dee5d858 100644 --- a/src/legacy/core_plugins/metrics/public/components/splits/everything.js +++ b/src/legacy/core_plugins/metrics/public/components/splits/everything.js @@ -17,7 +17,7 @@ * under the License. */ -import createSelectHandler from '../lib/create_select_handler'; +import { createSelectHandler } from '../lib/create_select_handler'; import { GroupBySelect } from './group_by_select'; import PropTypes from 'prop-types'; import React from 'react'; diff --git a/src/legacy/core_plugins/metrics/public/components/splits/filter.js b/src/legacy/core_plugins/metrics/public/components/splits/filter.js index 0a73c6e9c397e..66c1b1ac7fb63 100644 --- a/src/legacy/core_plugins/metrics/public/components/splits/filter.js +++ b/src/legacy/core_plugins/metrics/public/components/splits/filter.js @@ -17,20 +17,23 @@ * under the License. */ -import createTextHandler from '../lib/create_text_handler'; -import createSelectHandler from '../lib/create_select_handler'; +import { createSelectHandler } from '../lib/create_select_handler'; import { GroupBySelect } from './group_by_select'; import PropTypes from 'prop-types'; import React from 'react'; -import { htmlIdGenerator, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiFieldText } from '@elastic/eui'; +import { data } from 'plugins/data'; +const { QueryBarInput } = data.query.ui; +import { Storage } from 'ui/storage'; +import { htmlIdGenerator, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { getDefaultQueryLanguage } from '../lib/get_default_query_language'; +const localStorage = new Storage(window.localStorage); export const SplitByFilter = props => { - const { onChange, uiRestrictions } = props; - const defaults = { filter: '' }; + const { onChange, uiRestrictions, indexPattern } = props; + const defaults = { filter: { language: getDefaultQueryLanguage(), query: '' } }; const model = { ...defaults, ...props.model }; const htmlId = htmlIdGenerator(); - const handleTextChange = createTextHandler(onChange); const handleSelectChange = createSelectHandler(onChange); return ( @@ -57,9 +60,15 @@ export const SplitByFilter = props => { defaultMessage="Query string" />)} > - onChange({ filter })} + appName={'VisEditor'} + indexPatterns={[indexPattern]} + store={localStorage} /> @@ -71,4 +80,5 @@ SplitByFilter.propTypes = { model: PropTypes.object, onChange: PropTypes.func, uiRestrictions: PropTypes.object, + indexPatterns: PropTypes.string, }; diff --git a/src/legacy/core_plugins/metrics/public/components/splits/filter_items.js b/src/legacy/core_plugins/metrics/public/components/splits/filter_items.js index 5dd0b4eeb8321..6e5dcb7b8d347 100644 --- a/src/legacy/core_plugins/metrics/public/components/splits/filter_items.js +++ b/src/legacy/core_plugins/metrics/public/components/splits/filter_items.js @@ -20,14 +20,18 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import _ from 'lodash'; -import * as collectionActions from '../lib/collection_actions'; -import AddDeleteButtons from '../add_delete_buttons'; -import ColorPicker from '../color_picker'; +import { collectionActions } from '../lib/collection_actions'; +import { AddDeleteButtons } from '../add_delete_buttons'; +import { ColorPicker } from '../color_picker'; import uuid from 'uuid'; +import { data } from 'plugins/data'; +const { QueryBarInput } = data.query.ui; +import { Storage } from 'ui/storage'; import { EuiFieldText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { injectI18n } from '@kbn/i18n/react'; +import { getDefaultQueryLanguage } from '../lib/get_default_query_language'; +const localStorage = new Storage(window.localStorage); class FilterItemsUi extends Component { - constructor(props) { super(props); this.renderRow = this.renderRow.bind(this); @@ -41,15 +45,24 @@ class FilterItemsUi extends Component { })); }; } - + handleQueryChange = (model, filter) => { + const part = { filter }; + collectionActions.handleChange(this.props, _.assign({}, model, part)); + } renderRow(row, i, items) { + const indexPatterns = this.props.indexPatterns; const defaults = { filter: '', label: '' }; const model = { ...defaults, ...row }; const handleChange = (part) => { const fn = collectionActions.handleChange.bind(null, this.props); fn(_.assign({}, model, part)); }; - const newFilter = () => ({ color: this.props.model.color, id: uuid.v1() }); + + const newFilter = () => ({ + color: this.props.model.color, + id: uuid.v1(), + filter: { language: model.filter.language || getDefaultQueryLanguage(), query: '' }, + }); const handleAdd = collectionActions.handleAdd .bind(null, this.props, newFilter); const handleDelete = collectionActions.handleDelete @@ -67,12 +80,15 @@ class FilterItemsUi extends Component { /> - this.handleQueryChange(model, query)} + appName={'VisEditor'} + indexPatterns={[indexPatterns]} + store={localStorage} /> @@ -112,8 +128,8 @@ class FilterItemsUi extends Component { FilterItemsUi.propTypes = { name: PropTypes.string, model: PropTypes.object, - onChange: PropTypes.func + onChange: PropTypes.func, + indexPatterns: PropTypes.string, }; -const FilterItems = injectI18n(FilterItemsUi); -export default FilterItems; +export const FilterItems = injectI18n(FilterItemsUi); diff --git a/src/legacy/core_plugins/metrics/public/components/splits/filters.js b/src/legacy/core_plugins/metrics/public/components/splits/filters.js index 16532a41d49b2..5c3175f5d686b 100644 --- a/src/legacy/core_plugins/metrics/public/components/splits/filters.js +++ b/src/legacy/core_plugins/metrics/public/components/splits/filters.js @@ -17,16 +17,16 @@ * under the License. */ -import createSelectHandler from '../lib/create_select_handler'; +import { createSelectHandler } from '../lib/create_select_handler'; import { GroupBySelect } from './group_by_select'; -import FilterItems from './filter_items'; +import { FilterItems } from './filter_items'; import PropTypes from 'prop-types'; import React from 'react'; import { htmlIdGenerator, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; export const SplitByFilters = (props) => { - const { onChange, model, uiRestrictions } = props; + const { onChange, model, uiRestrictions, indexPattern } = props; const htmlId = htmlIdGenerator(); const handleSelectChange = createSelectHandler(onChange); return ( @@ -53,6 +53,7 @@ export const SplitByFilters = (props) => { name="split_filters" model={model} onChange={onChange} + indexPatterns={indexPattern} />
); @@ -62,4 +63,5 @@ SplitByFilters.propTypes = { model: PropTypes.object, onChange: PropTypes.func, uiRestrictions: PropTypes.object, + indexPatterns: PropTypes.array }; diff --git a/src/legacy/core_plugins/metrics/public/components/splits/terms.js b/src/legacy/core_plugins/metrics/public/components/splits/terms.js index bb2c994b09452..9505a8bd8215f 100644 --- a/src/legacy/core_plugins/metrics/public/components/splits/terms.js +++ b/src/legacy/core_plugins/metrics/public/components/splits/terms.js @@ -21,10 +21,10 @@ import PropTypes from 'prop-types'; import React from 'react'; import { get, find } from 'lodash'; import { GroupBySelect } from './group_by_select'; -import createTextHandler from '../lib/create_text_handler'; -import createSelectHandler from '../lib/create_select_handler'; -import FieldSelect from '../aggs/field_select'; -import MetricSelect from '../aggs/metric_select'; +import { createTextHandler } from '../lib/create_text_handler'; +import { createSelectHandler } from '../lib/create_select_handler'; +import { FieldSelect } from '../aggs/field_select'; +import { MetricSelect } from '../aggs/metric_select'; import { htmlIdGenerator, EuiFlexGroup, diff --git a/src/legacy/core_plugins/metrics/public/components/splits/unsupported_split.js b/src/legacy/core_plugins/metrics/public/components/splits/unsupported_split.js index 2749441f86c81..74ecc5c89f2c5 100644 --- a/src/legacy/core_plugins/metrics/public/components/splits/unsupported_split.js +++ b/src/legacy/core_plugins/metrics/public/components/splits/unsupported_split.js @@ -17,7 +17,7 @@ * under the License. */ -import createSelectHandler from '../lib/create_select_handler'; +import { createSelectHandler } from '../lib/create_select_handler'; import { GroupBySelect } from './group_by_select'; import PropTypes from 'prop-types'; import React from 'react'; diff --git a/src/legacy/core_plugins/metrics/public/components/vis_editor.js b/src/legacy/core_plugins/metrics/public/components/vis_editor.js index 5f28d7710275c..cf194cad978ed 100644 --- a/src/legacy/core_plugins/metrics/public/components/vis_editor.js +++ b/src/legacy/core_plugins/metrics/public/components/vis_editor.js @@ -19,20 +19,23 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import chrome from 'ui/chrome'; import * as Rx from 'rxjs'; import { share } from 'rxjs/operators'; import { isEqual, isEmpty, debounce } from 'lodash'; -import VisEditorVisualization from './vis_editor_visualization'; -import Visualization from './visualization'; -import VisPicker from './vis_picker'; -import PanelConfig from './panel_config'; -import brushHandler from '../lib/create_brush_handler'; +import { fromKueryExpression } from '@kbn/es-query'; +import { VisEditorVisualization } from './vis_editor_visualization'; +import { Visualization } from './visualization'; +import { VisPicker } from './vis_picker'; +import { PanelConfig } from './panel_config'; +import { brushHandler } from '../lib/create_brush_handler'; import { fetchFields } from '../lib/fetch_fields'; -import { extractIndexPatterns } from '../lib/extract_index_patterns'; +import { extractIndexPatterns } from '../../common/extract_index_patterns'; const VIS_STATE_DEBOUNCE_DELAY = 200; +const queryOptions = chrome.getUiSettingsClient().get('query:allowLeadingWildcards'); -class VisEditor extends Component { +export class VisEditor extends Component { constructor(props) { super(props); const { vis } = props; @@ -66,7 +69,18 @@ class VisEditor extends Component { this.props.vis.updateState(); }, VIS_STATE_DEBOUNCE_DELAY); - handleChange = async (partialModel) => { + isValidKueryQuery = (filterQuery) => { + if (filterQuery && filterQuery.language === 'kuery') { + try { + fromKueryExpression(filterQuery.query, { allowLeadingWildcards: queryOptions }); + } catch (error) { + return false; + } + } + return true; + }; + + handleChange = async partialModel => { if (isEmpty(partialModel)) { return; } @@ -76,7 +90,6 @@ class VisEditor extends Component { ...partialModel, }; let dirty = true; - if (this.state.autoApply || hasTypeChanged) { this.updateVisState(); @@ -85,7 +98,6 @@ class VisEditor extends Component { if (this.props.isEditorMode) { const extractedIndexPatterns = extractIndexPatterns(nextModel); - if (!isEqual(this.state.extractedIndexPatterns, extractedIndexPatterns)) { fetchFields(extractedIndexPatterns) .then(visFields => this.setState({ @@ -198,5 +210,3 @@ VisEditor.propTypes = { savedObj: PropTypes.object, timeRange: PropTypes.object, }; - -export default VisEditor; diff --git a/src/legacy/core_plugins/metrics/public/components/vis_editor_visualization.js b/src/legacy/core_plugins/metrics/public/components/vis_editor_visualization.js index 869a0f50a8694..675abea97273d 100644 --- a/src/legacy/core_plugins/metrics/public/components/vis_editor_visualization.js +++ b/src/legacy/core_plugins/metrics/public/components/vis_editor_visualization.js @@ -22,12 +22,12 @@ import { get, isEqual } from 'lodash'; import { keyCodes, EuiFlexGroup, EuiFlexItem, EuiButton, EuiText, EuiSwitch } from '@elastic/eui'; import { getVisualizeLoader } from 'ui/visualize/loader/visualize_loader'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; -import { getInterval, convertIntervalIntoUnit, isIntervalValid, isGteInterval } from './lib/get_interval'; +import { getInterval, convertIntervalIntoUnit, isIntervalValid, isGteInterval, AUTO_INTERVAL } from './lib/get_interval'; import { PANEL_TYPES } from '../../common/panel_types'; const MIN_CHART_HEIGHT = 250; -class VisEditorVisualization extends Component { +class VisEditorVisualizationUI extends Component { constructor(props) { super(props); this.state = { @@ -71,7 +71,7 @@ class VisEditorVisualization extends Component { timeRange, appState, savedObj, - onDataChange + onDataChange, } = this.props; this._handler = loader.embedVisualizationWithSavedObject(this._visEl.current, savedObj, { @@ -112,10 +112,11 @@ class VisEditorVisualization extends Component { }; }); } - } + }; hasShowPanelIntervalValue() { const type = get(this.props, 'model.type', ''); + const interval = get(this.props, 'model.interval', AUTO_INTERVAL); return [ PANEL_TYPES.METRIC, @@ -123,23 +124,14 @@ class VisEditorVisualization extends Component { PANEL_TYPES.GAUGE, PANEL_TYPES.MARKDOWN, PANEL_TYPES.TABLE, - ].includes(type); + ].includes(type) && (interval === AUTO_INTERVAL + || isGteInterval(interval) || !isIntervalValid(interval)); } getFormattedPanelInterval() { - const interval = get(this.props, 'model.interval') || 'auto'; - const isValid = isIntervalValid(interval); - const shouldShowActualInterval = interval === 'auto' || isGteInterval(interval); + const interval = convertIntervalIntoUnit(this.state.panelInterval, false); - if (shouldShowActualInterval || !isValid) { - const autoInterval = convertIntervalIntoUnit(this.state.panelInterval, false); - - if (autoInterval) { - return `${autoInterval.unitValue}${autoInterval.unitString}`; - } - } else { - return interval; - } + return interval ? `${interval.unitValue}${interval.unitString}` : null; } componentWillUnmount() { @@ -173,7 +165,7 @@ class VisEditorVisualization extends Component { title, description, onToggleAutoApply, - onCommit + onCommit, } = this.props; const style = { height: this.state.height }; @@ -237,7 +229,7 @@ class VisEditorVisualization extends Component { {!autoApply && - + ({})); import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import VisEditorVisualization from './vis_editor_visualization'; +import { VisEditorVisualization } from './vis_editor_visualization'; describe('getVisualizeLoader', () => { let updateStub; diff --git a/src/legacy/core_plugins/metrics/public/components/vis_picker.js b/src/legacy/core_plugins/metrics/public/components/vis_picker.js index c4062f049e7ca..0348320ae0a93 100644 --- a/src/legacy/core_plugins/metrics/public/components/vis_picker.js +++ b/src/legacy/core_plugins/metrics/public/components/vis_picker.js @@ -46,7 +46,7 @@ VisPickerItem.propTypes = { selected: PropTypes.bool }; -const VisPicker = injectI18n(function (props) { +export const VisPicker = injectI18n(function (props) { const handleChange = (type) => { props.onChange({ type }); }; @@ -82,5 +82,3 @@ VisPicker.propTypes = { model: PropTypes.object, onChange: PropTypes.func }; - -export default VisPicker; diff --git a/src/legacy/core_plugins/metrics/public/components/vis_types/gauge/series.js b/src/legacy/core_plugins/metrics/public/components/vis_types/gauge/series.js index 802edfd47bc2b..3c4d45347102d 100644 --- a/src/legacy/core_plugins/metrics/public/components/vis_types/gauge/series.js +++ b/src/legacy/core_plugins/metrics/public/components/vis_types/gauge/series.js @@ -19,13 +19,13 @@ import PropTypes from 'prop-types'; import React from 'react'; -import ColorPicker from '../../color_picker'; -import AddDeleteButtons from '../../add_delete_buttons'; +import { ColorPicker } from '../../color_picker'; +import { AddDeleteButtons } from '../../add_delete_buttons'; import { SeriesConfig } from '../../series_config'; -import Split from '../../split'; +import { Split } from '../../split'; import { SeriesDragHandler } from '../../series_drag_handler'; import { EuiTabs, EuiTab, EuiFlexGroup, EuiFlexItem, EuiFieldText, EuiButtonIcon } from '@elastic/eui'; -import createTextHandler from '../../lib/create_text_handler'; +import { createTextHandler } from '../../lib/create_text_handler'; import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { Aggs } from '../../aggs/aggs'; @@ -85,6 +85,7 @@ function GaugeSeriesUi(props) { fields={props.fields} model={props.model} onChange={props.onChange} + indexPatternForQuery={props.indexPatternForQuery} /> ); } @@ -196,7 +197,7 @@ GaugeSeriesUi.propTypes = { visible: PropTypes.bool, uiRestrictions: PropTypes.object, dragHandleProps: PropTypes.object, + indexPatternForQuery: PropTypes.string, }; -const GaugeSeries = injectI18n(GaugeSeriesUi); -export default GaugeSeries; +export const GaugeSeries = injectI18n(GaugeSeriesUi); diff --git a/src/legacy/core_plugins/metrics/public/components/vis_types/gauge/vis.js b/src/legacy/core_plugins/metrics/public/components/vis_types/gauge/vis.js index f92737726bb85..6f1127ba9c158 100644 --- a/src/legacy/core_plugins/metrics/public/components/vis_types/gauge/vis.js +++ b/src/legacy/core_plugins/metrics/public/components/vis_types/gauge/vis.js @@ -20,10 +20,10 @@ import PropTypes from 'prop-types'; import React from 'react'; import { visWithSplits } from '../../vis_with_splits'; -import tickFormatter from '../../lib/tick_formatter'; +import { tickFormatter } from '../../lib/tick_formatter'; import _ from 'lodash'; -import Gauge from '../../../visualizations/components/gauge'; -import getLastValue from '../../../../common/get_last_value'; +import { Gauge } from '../../../visualizations/components/gauge'; +import { getLastValue } from '../../../../common/get_last_value'; function getColors(props) { const { model, visData } = props; @@ -98,4 +98,4 @@ GaugeVisualization.propTypes = { getConfig: PropTypes.func }; -export default visWithSplits(GaugeVisualization); +export const gauge = visWithSplits(GaugeVisualization); diff --git a/src/legacy/core_plugins/metrics/public/components/vis_types/markdown/series.js b/src/legacy/core_plugins/metrics/public/components/vis_types/markdown/series.js index 81cde2e752418..9f5069f55fcb5 100644 --- a/src/legacy/core_plugins/metrics/public/components/vis_types/markdown/series.js +++ b/src/legacy/core_plugins/metrics/public/components/vis_types/markdown/series.js @@ -19,10 +19,10 @@ import PropTypes from 'prop-types'; import React from 'react'; -import AddDeleteButtons from '../../add_delete_buttons'; +import { AddDeleteButtons } from '../../add_delete_buttons'; import { SeriesConfig } from '../../series_config'; -import Split from '../../split'; -import createTextHandler from '../../lib/create_text_handler'; +import { Split } from '../../split'; +import { createTextHandler } from '../../lib/create_text_handler'; import { EuiTabs, EuiTab, EuiFlexGroup, EuiFlexItem, EuiFieldText, EuiButtonIcon } from '@elastic/eui'; import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { Aggs } from '../../aggs/aggs'; @@ -84,6 +84,7 @@ function MarkdownSeriesUi(props) { fields={props.fields} model={props.model} onChange={props.onChange} + indexPatternForQuery={props.indexPatternForQuery} /> ); } @@ -190,7 +191,7 @@ MarkdownSeriesUi.propTypes = { visible: PropTypes.bool, uiRestrictions: PropTypes.object, dragHandleProps: PropTypes.object, + indexPatternForQuery: PropTypes.string, }; -const MarkdownSeries = injectI18n(MarkdownSeriesUi); -export default MarkdownSeries; +export const MarkdownSeries = injectI18n(MarkdownSeriesUi); diff --git a/src/legacy/core_plugins/metrics/public/components/vis_types/markdown/vis.js b/src/legacy/core_plugins/metrics/public/components/vis_types/markdown/vis.js index fe64ed63fba13..c93b46cdccede 100644 --- a/src/legacy/core_plugins/metrics/public/components/vis_types/markdown/vis.js +++ b/src/legacy/core_plugins/metrics/public/components/vis_types/markdown/vis.js @@ -23,14 +23,14 @@ import uuid from 'uuid'; import { get } from 'lodash'; import { Markdown } from 'ui/markdown/markdown'; -import ErrorComponent from '../../error'; -import replaceVars from '../../lib/replace_vars'; -import convertSeriesToVars from '../../lib/convert_series_to_vars'; +import { ErrorComponent } from '../../error'; +import { replaceVars } from '../../lib/replace_vars'; +import { convertSeriesToVars } from '../../lib/convert_series_to_vars'; import { isBackgroundInverted } from '../../../../common/set_is_reversed'; const getMarkdownId = id => `markdown-${id}`; -function MarkdownVisualization(props) { +export function MarkdownVisualization(props) { const { backgroundColor, model, visData, dateFormat } = props; const series = get(visData, `${model.id}.series`, []); const variables = convertSeriesToVars(series, model, dateFormat, props.getConfig); @@ -98,5 +98,3 @@ MarkdownVisualization.propTypes = { dateFormat: PropTypes.string, getConfig: PropTypes.func }; - -export default MarkdownVisualization; diff --git a/src/legacy/core_plugins/metrics/public/components/vis_types/metric/series.js b/src/legacy/core_plugins/metrics/public/components/vis_types/metric/series.js index e05ff8cf6a547..74e4a8c07353c 100644 --- a/src/legacy/core_plugins/metrics/public/components/vis_types/metric/series.js +++ b/src/legacy/core_plugins/metrics/public/components/vis_types/metric/series.js @@ -19,13 +19,13 @@ import PropTypes from 'prop-types'; import React from 'react'; -import ColorPicker from '../../color_picker'; -import AddDeleteButtons from '../../add_delete_buttons'; +import { ColorPicker } from '../../color_picker'; +import { AddDeleteButtons } from '../../add_delete_buttons'; import { SeriesConfig } from '../../series_config'; -import Split from '../../split'; +import { Split } from '../../split'; import { SeriesDragHandler } from '../../series_drag_handler'; import { EuiTabs, EuiTab, EuiFlexGroup, EuiFlexItem, EuiFieldText, EuiButtonIcon } from '@elastic/eui'; -import createTextHandler from '../../lib/create_text_handler'; +import { createTextHandler } from '../../lib/create_text_handler'; import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { Aggs } from '../../aggs/aggs'; @@ -85,6 +85,7 @@ function MetricSeriesUi(props) { fields={props.fields} model={props.model} onChange={props.onChange} + indexPatternForQuery={props.indexPatternForQuery} /> ); } @@ -202,7 +203,7 @@ MetricSeriesUi.propTypes = { togglePanelActivation: PropTypes.func, uiRestrictions: PropTypes.object, dragHandleProps: PropTypes.object, + indexPatternForQuery: PropTypes.string, }; -const MetricSeries = injectI18n(MetricSeriesUi); -export default MetricSeries; +export const MetricSeries = injectI18n(MetricSeriesUi); diff --git a/src/legacy/core_plugins/metrics/public/components/vis_types/metric/vis.js b/src/legacy/core_plugins/metrics/public/components/vis_types/metric/vis.js index 7a6346a66dd77..7bb9ca6400683 100644 --- a/src/legacy/core_plugins/metrics/public/components/vis_types/metric/vis.js +++ b/src/legacy/core_plugins/metrics/public/components/vis_types/metric/vis.js @@ -20,10 +20,10 @@ import PropTypes from 'prop-types'; import React from 'react'; import { visWithSplits } from '../../vis_with_splits'; -import tickFormatter from '../../lib/tick_formatter'; +import { tickFormatter } from '../../lib/tick_formatter'; import _ from 'lodash'; -import Metric from '../../../visualizations/components/metric'; -import getLastValue from '../../../../common/get_last_value'; +import { Metric } from '../../../visualizations/components/metric'; +import { getLastValue } from '../../../../common/get_last_value'; import { isBackgroundInverted } from '../../../../common/set_is_reversed'; function getColors(props) { @@ -93,4 +93,4 @@ MetricVisualization.propTypes = { getConfig: PropTypes.func }; -export default visWithSplits(MetricVisualization); +export const metric = visWithSplits(MetricVisualization); diff --git a/src/legacy/core_plugins/metrics/public/components/vis_types/table/config.js b/src/legacy/core_plugins/metrics/public/components/vis_types/table/config.js index b9a4fe65f10dc..fe2343d8484a6 100644 --- a/src/legacy/core_plugins/metrics/public/components/vis_types/table/config.js +++ b/src/legacy/core_plugins/metrics/public/components/vis_types/table/config.js @@ -20,12 +20,12 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import uuid from 'uuid'; -import DataFormatPicker from '../../data_format_picker'; -import createSelectHandler from '../../lib/create_select_handler'; -import createTextHandler from '../../lib/create_text_handler'; -import FieldSelect from '../../aggs/field_select'; -import YesNo from '../../yes_no'; -import ColorRules from '../../color_rules'; +import { DataFormatPicker } from '../../data_format_picker'; +import { createSelectHandler } from '../../lib/create_select_handler'; +import { createTextHandler } from '../../lib/create_text_handler'; +import { FieldSelect } from '../../aggs/field_select'; +import { YesNo } from '../../yes_no'; +import { ColorRules } from '../../color_rules'; import { htmlIdGenerator, EuiComboBox, @@ -40,8 +40,12 @@ import { EuiTitle, } from '@elastic/eui'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; - -class TableSeriesConfig extends Component { +import { data } from 'plugins/data'; +import { Storage } from 'ui/storage'; +import { getDefaultQueryLanguage } from '../../lib/get_default_query_language'; +const { QueryBarInput } = data.query.ui; +const localStorage = new Storage(window.localStorage); +class TableSeriesConfigUI extends Component { componentWillMount() { const { model } = this.props; @@ -124,10 +128,15 @@ class TableSeriesConfig extends Component { />)} fullWidth > - this.props.onChange({ filter })} + appName={'VisEditor'} + indexPatterns={[this.props.indexPatternForQuery]} + store={localStorage} /> @@ -213,12 +222,13 @@ class TableSeriesConfig extends Component { } -TableSeriesConfig.propTypes = { +TableSeriesConfigUI.propTypes = { fields: PropTypes.object, model: PropTypes.object, - onChange: PropTypes.func + onChange: PropTypes.func, + indexPatternForQuery: PropTypes.string }; -export default injectI18n(TableSeriesConfig); +export const TableSeriesConfig = injectI18n(TableSeriesConfigUI); diff --git a/src/legacy/core_plugins/metrics/public/components/vis_types/table/is_sortable.js b/src/legacy/core_plugins/metrics/public/components/vis_types/table/is_sortable.js index e308d3ce93af0..887652a67f406 100644 --- a/src/legacy/core_plugins/metrics/public/components/vis_types/table/is_sortable.js +++ b/src/legacy/core_plugins/metrics/public/components/vis_types/table/is_sortable.js @@ -17,7 +17,7 @@ * under the License. */ -import basicAggs from '../../../../common/basic_aggs'; +import { basicAggs } from '../../../../common/basic_aggs'; export function isSortable(metric) { return basicAggs.includes(metric.type); diff --git a/src/legacy/core_plugins/metrics/public/components/vis_types/table/series.js b/src/legacy/core_plugins/metrics/public/components/vis_types/table/series.js index e3f7d438d122f..78abe884f4c43 100644 --- a/src/legacy/core_plugins/metrics/public/components/vis_types/table/series.js +++ b/src/legacy/core_plugins/metrics/public/components/vis_types/table/series.js @@ -19,15 +19,15 @@ import React from 'react'; import PropTypes from 'prop-types'; -import AddDeleteButtons from '../../add_delete_buttons'; -import SeriesConfig from './config'; +import { AddDeleteButtons } from '../../add_delete_buttons'; +import { TableSeriesConfig as SeriesConfig } from './config'; import { SeriesDragHandler } from '../../series_drag_handler'; import { EuiTabs, EuiTab, EuiFlexGroup, EuiFlexItem, EuiFieldText, EuiButtonIcon } from '@elastic/eui'; -import createTextHandler from '../../lib/create_text_handler'; +import { createTextHandler } from '../../lib/create_text_handler'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; import { Aggs } from '../../aggs/aggs'; -function TableSeries(props) { +function TableSeriesUI(props) { const { model, onAdd, @@ -73,6 +73,7 @@ function TableSeries(props) { fields={props.fields} model={props.model} onChange={props.onChange} + indexPatternForQuery={props.indexPatternForQuery} /> ); } @@ -154,7 +155,7 @@ function TableSeries(props) { ); } -TableSeries.propTypes = { +TableSeriesUI.propTypes = { className: PropTypes.string, disableAdd: PropTypes.bool, disableDelete: PropTypes.bool, @@ -174,6 +175,7 @@ TableSeries.propTypes = { togglePanelActivation: PropTypes.func, uiRestrictions: PropTypes.object, dragHandleProps: PropTypes.object, + indexPatternForQuery: PropTypes.string, }; -export default injectI18n(TableSeries); +export const TableSeries = injectI18n(TableSeriesUI); diff --git a/src/legacy/core_plugins/metrics/public/components/vis_types/table/vis.js b/src/legacy/core_plugins/metrics/public/components/vis_types/table/vis.js index 94e89f9c746ca..940bbbd7392d6 100644 --- a/src/legacy/core_plugins/metrics/public/components/vis_types/table/vis.js +++ b/src/legacy/core_plugins/metrics/public/components/vis_types/table/vis.js @@ -21,11 +21,11 @@ import _, { isArray, last, get } from 'lodash'; import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { fieldFormats } from 'ui/registry/field_formats'; -import tickFormatter from '../../lib/tick_formatter'; -import calculateLabel from '../../../../common/calculate_label'; +import { tickFormatter } from '../../lib/tick_formatter'; +import { calculateLabel } from '../../../../common/calculate_label'; import { isSortable } from './is_sortable'; import { EuiToolTip, EuiIcon } from '@elastic/eui'; -import replaceVars from '../../lib/replace_vars'; +import { replaceVars } from '../../lib/replace_vars'; import { FormattedMessage } from '@kbn/i18n/react'; import { METRIC_TYPES } from '../../../../common/metric_types'; @@ -46,7 +46,7 @@ function getColor(rules, colorKey, value) { return color; } -class TableVis extends Component { +export class TableVis extends Component { constructor(props) { super(props); @@ -251,5 +251,3 @@ TableVis.propTypes = { pageNumber: PropTypes.number, getConfig: PropTypes.func, }; - -export default TableVis; diff --git a/src/legacy/core_plugins/metrics/public/components/vis_types/timeseries/config.js b/src/legacy/core_plugins/metrics/public/components/vis_types/timeseries/config.js index 92787b189a94a..babce7a0f2178 100644 --- a/src/legacy/core_plugins/metrics/public/components/vis_types/timeseries/config.js +++ b/src/legacy/core_plugins/metrics/public/components/vis_types/timeseries/config.js @@ -19,11 +19,14 @@ import PropTypes from 'prop-types'; import React from 'react'; -import DataFormatPicker from '../../data_format_picker'; -import createSelectHandler from '../../lib/create_select_handler'; -import YesNo from '../../yes_no'; -import createTextHandler from '../../lib/create_text_handler'; +import { DataFormatPicker } from '../../data_format_picker'; +import { createSelectHandler } from '../../lib/create_select_handler'; +import { YesNo } from '../../yes_no'; +import { createTextHandler } from '../../lib/create_text_handler'; import { IndexPattern } from '../../index_pattern'; +import { data } from 'plugins/data'; +const { QueryBarInput } = data.query.ui; +import { Storage } from 'ui/storage'; import { htmlIdGenerator, EuiComboBox, @@ -38,11 +41,12 @@ import { EuiSpacer, } from '@elastic/eui'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { getDefaultQueryLanguage } from '../../lib/get_default_query_language'; +const localStorage = new Storage(window.localStorage); -const TimeseriesConfig = injectI18n(function (props) { +export const TimeseriesConfig = injectI18n(function (props) { const handleSelectChange = createSelectHandler(props.onChange); const handleTextChange = createTextHandler(props.onChange); - const defaults = { fill: '', line_width: '', @@ -58,7 +62,6 @@ const TimeseriesConfig = injectI18n(function (props) { const model = { ...defaults, ...props.model }; const htmlId = htmlIdGenerator(); const { intl } = props; - const stackedOptions = [ { label: intl.formatMessage({ id: 'tsvb.timeSeries.noneLabel', defaultMessage: 'None' }), value: 'none' }, { label: intl.formatMessage({ id: 'tsvb.timeSeries.stackedLabel', defaultMessage: 'Stacked' }), value: 'stacked' }, @@ -269,6 +272,9 @@ const TimeseriesConfig = injectI18n(function (props) { const disableSeparateYaxis = model.separate_axis ? false : true; + const seriesIndexPattern = (props.model.override_index_pattern && props.model.series_index_pattern) ? + props.model.series_index_pattern : props.indexPatternForQuery; + return (
@@ -301,28 +307,35 @@ const TimeseriesConfig = injectI18n(function (props) { onChange={handleTextChange('value_template')} value={model.value_template} fullWidth + data-test-subj="tsvb_series_value" /> - - )} - fullWidth - > - + )} fullWidth - /> - + > + props.onChange({ filter })} + appName={'VisEditor'} + indexPatterns={[seriesIndexPattern]} + store={localStorage} + /> + + { type } @@ -492,7 +505,6 @@ const TimeseriesConfig = injectI18n(function (props) { TimeseriesConfig.propTypes = { fields: PropTypes.object, model: PropTypes.object, - onChange: PropTypes.func + onChange: PropTypes.func, + indexPatternForQuery: PropTypes.string, }; - -export default TimeseriesConfig; diff --git a/src/legacy/core_plugins/metrics/public/components/vis_types/timeseries/series.js b/src/legacy/core_plugins/metrics/public/components/vis_types/timeseries/series.js index ab0f7da5dac5e..19cf5dce04c16 100644 --- a/src/legacy/core_plugins/metrics/public/components/vis_types/timeseries/series.js +++ b/src/legacy/core_plugins/metrics/public/components/vis_types/timeseries/series.js @@ -19,17 +19,17 @@ import PropTypes from 'prop-types'; import React from 'react'; -import ColorPicker from '../../color_picker'; -import AddDeleteButtons from '../../add_delete_buttons'; +import { ColorPicker } from '../../color_picker'; +import { AddDeleteButtons } from '../../add_delete_buttons'; import { Aggs } from '../../../components/aggs/aggs'; -import SeriesConfig from './config'; +import { TimeseriesConfig as SeriesConfig } from './config'; import { SeriesDragHandler } from '../../series_drag_handler'; import { EuiTabs, EuiTab, EuiFlexGroup, EuiFlexItem, EuiFieldText, EuiButtonIcon } from '@elastic/eui'; -import Split from '../../split'; -import createTextHandler from '../../lib/create_text_handler'; +import { Split } from '../../split'; +import { createTextHandler } from '../../lib/create_text_handler'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; -const TimeseriesSeries = injectI18n(function (props) { +const TimeseriesSeriesUI = injectI18n(function (props) { const { panel, fields, @@ -44,7 +44,6 @@ const TimeseriesSeries = injectI18n(function (props) { name, uiRestrictions } = props; - const defaults = { label: '' }; const model = { ...defaults, ...props.model }; @@ -86,6 +85,7 @@ const TimeseriesSeries = injectI18n(function (props) { fields={props.fields} model={props.model} onChange={props.onChange} + indexPatternForQuery={props.indexPatternForQuery} /> ); } @@ -182,7 +182,7 @@ const TimeseriesSeries = injectI18n(function (props) { }); -TimeseriesSeries.propTypes = { +TimeseriesSeriesUI.propTypes = { className: PropTypes.string, colorPicker: PropTypes.bool, disableAdd: PropTypes.bool, @@ -203,6 +203,7 @@ TimeseriesSeries.propTypes = { togglePanelActivation: PropTypes.func, uiRestrictions: PropTypes.object, dragHandleProps: PropTypes.object, + indexPatternForQuery: PropTypes.string, }; -export default injectI18n(TimeseriesSeries); +export const TimeseriesSeries = injectI18n(TimeseriesSeriesUI); diff --git a/src/legacy/core_plugins/metrics/public/components/vis_types/timeseries/vis.js b/src/legacy/core_plugins/metrics/public/components/vis_types/timeseries/vis.js index 72ea297f7e12d..68bfa72c5d319 100644 --- a/src/legacy/core_plugins/metrics/public/components/vis_types/timeseries/vis.js +++ b/src/legacy/core_plugins/metrics/public/components/vis_types/timeseries/vis.js @@ -22,10 +22,10 @@ import React, { Component } from 'react'; import { toastNotifications } from 'ui/notify'; import { MarkdownSimple } from 'ui/markdown'; -import tickFormatter from '../../lib/tick_formatter'; +import { tickFormatter } from '../../lib/tick_formatter'; import _ from 'lodash'; -import Timeseries from '../../../visualizations/components/timeseries'; -import replaceVars from '../../lib/replace_vars'; +import { Timeseries } from '../../../visualizations/components/timeseries'; +import { replaceVars } from '../../lib/replace_vars'; import { getAxisLabelString } from '../../lib/get_axis_label_string'; import { getInterval } from '../../lib/get_interval'; import { createXaxisFormatter } from '../../lib/create_xaxis_formatter'; @@ -34,7 +34,7 @@ function hasSeparateAxis(row) { return row.separate_axis; } -class TimeseriesVisualization extends Component { +export class TimeseriesVisualization extends Component { getInterval = () => { const { visData, model } = this.props; @@ -243,5 +243,3 @@ TimeseriesVisualization.propTypes = { dateFormat: PropTypes.string, getConfig: PropTypes.func }; - -export default TimeseriesVisualization; diff --git a/src/legacy/core_plugins/metrics/public/components/vis_types/top_n/series.js b/src/legacy/core_plugins/metrics/public/components/vis_types/top_n/series.js index c73f54284d2ec..53b7b3b7f43f1 100644 --- a/src/legacy/core_plugins/metrics/public/components/vis_types/top_n/series.js +++ b/src/legacy/core_plugins/metrics/public/components/vis_types/top_n/series.js @@ -19,17 +19,17 @@ import PropTypes from 'prop-types'; import React from 'react'; -import ColorPicker from '../../color_picker'; -import AddDeleteButtons from '../../add_delete_buttons'; +import { ColorPicker } from '../../color_picker'; +import { AddDeleteButtons } from '../../add_delete_buttons'; import { SeriesConfig } from '../../series_config'; -import Split from '../../split'; +import { Split } from '../../split'; import { SeriesDragHandler } from '../../series_drag_handler'; import { EuiTabs, EuiTab, EuiFlexGroup, EuiFlexItem, EuiFieldText, EuiButtonIcon } from '@elastic/eui'; -import createTextHandler from '../../lib/create_text_handler'; +import { createTextHandler } from '../../lib/create_text_handler'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; import { Aggs } from '../../aggs/aggs'; -const TopNSeries = injectI18n(function (props) { +export const TopNSeries = injectI18n(function (props) { const { panel, model, @@ -83,6 +83,7 @@ const TopNSeries = injectI18n(function (props) { fields={props.fields} model={props.model} onChange={props.onChange} + indexPatternForQuery={props.indexPatternForQuery} /> ); } @@ -197,6 +198,5 @@ TopNSeries.propTypes = { togglePanelActivation: PropTypes.func, uiRestrictions: PropTypes.object, dragHandleProps: PropTypes.object, + indexPatternForQuery: PropTypes.string, }; - -export default TopNSeries; diff --git a/src/legacy/core_plugins/metrics/public/components/vis_types/top_n/vis.js b/src/legacy/core_plugins/metrics/public/components/vis_types/top_n/vis.js index 9bd9dfc843482..0bbace6bafb96 100644 --- a/src/legacy/core_plugins/metrics/public/components/vis_types/top_n/vis.js +++ b/src/legacy/core_plugins/metrics/public/components/vis_types/top_n/vis.js @@ -17,11 +17,11 @@ * under the License. */ -import tickFormatter from '../../lib/tick_formatter'; -import TopN from '../../../visualizations/components/top_n'; -import getLastValue from '../../../../common/get_last_value'; +import { tickFormatter } from '../../lib/tick_formatter'; +import { TopN } from '../../../visualizations/components/top_n'; +import { getLastValue } from '../../../../common/get_last_value'; import { isBackgroundInverted } from '../../../../common/set_is_reversed'; -import replaceVars from '../../lib/replace_vars'; +import { replaceVars } from '../../lib/replace_vars'; import PropTypes from 'prop-types'; import React from 'react'; import { sortBy, first, get, gt, gte, lt, lte } from 'lodash'; @@ -47,7 +47,7 @@ function sortSeries(visData, model) { }, []); } -function TopNVisualization(props) { +export function TopNVisualization(props) { const { backgroundColor, model, visData } = props; const series = sortSeries(visData, model) @@ -107,5 +107,3 @@ TopNVisualization.propTypes = { visData: PropTypes.object, getConfig: PropTypes.func }; - -export default TopNVisualization; diff --git a/src/legacy/core_plugins/metrics/public/components/vis_with_splits.js b/src/legacy/core_plugins/metrics/public/components/vis_with_splits.js index 68c9c1616ebf6..cea9258062637 100644 --- a/src/legacy/core_plugins/metrics/public/components/vis_with_splits.js +++ b/src/legacy/core_plugins/metrics/public/components/vis_with_splits.js @@ -20,7 +20,8 @@ import React from 'react'; import { getDisplayName } from './lib/get_display_name'; import { last, findIndex, first } from 'lodash'; -import calculateLabel from '../../common/calculate_label'; +import { calculateLabel } from '../../common/calculate_label'; + export function visWithSplits(WrappedComponent) { function SplitVisComponent(props) { const { model, visData } = props; diff --git a/src/legacy/core_plugins/metrics/public/components/visualization.js b/src/legacy/core_plugins/metrics/public/components/visualization.js index 6e1fcea29016d..d5e4ff8d49369 100644 --- a/src/legacy/core_plugins/metrics/public/components/visualization.js +++ b/src/legacy/core_plugins/metrics/public/components/visualization.js @@ -21,17 +21,17 @@ import PropTypes from 'prop-types'; import React from 'react'; import _ from 'lodash'; -import timeseries from './vis_types/timeseries/vis'; -import metric from './vis_types/metric/vis'; -import topN from './vis_types/top_n/vis'; -import table from './vis_types/table/vis'; -import gauge from './vis_types/gauge/vis'; -import markdown from './vis_types/markdown/vis'; -import ErrorComponent from './error'; -import NoData from './no_data'; +import { TimeseriesVisualization } from './vis_types/timeseries/vis'; +import { metric } from './vis_types/metric/vis'; +import { TopNVisualization as topN } from './vis_types/top_n/vis'; +import { TableVis as table } from './vis_types/table/vis'; +import { gauge } from './vis_types/gauge/vis'; +import { MarkdownVisualization as markdown } from './vis_types/markdown/vis'; +import { ErrorComponent } from './error'; +import { NoDataComponent } from './no_data'; const types = { - timeseries, + timeseries: TimeseriesVisualization, metric, top_n: topN, table, @@ -39,7 +39,7 @@ const types = { markdown }; -function Visualization(props) { +export function Visualization(props) { const { visData, model } = props; // Show the error panel const error = _.get(visData, `${model.id}.error`); @@ -56,7 +56,7 @@ function Visualization(props) { if (noData) { return (
- +
); } @@ -96,5 +96,3 @@ Visualization.propTypes = { Visualization.defaultProps = { className: 'tvbVis' }; - -export default Visualization; diff --git a/src/legacy/core_plugins/metrics/public/components/yes_no.js b/src/legacy/core_plugins/metrics/public/components/yes_no.js index 21448e71577f6..d042adfd9521c 100644 --- a/src/legacy/core_plugins/metrics/public/components/yes_no.js +++ b/src/legacy/core_plugins/metrics/public/components/yes_no.js @@ -23,7 +23,7 @@ import _ from 'lodash'; import { EuiRadio, htmlIdGenerator } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -function YesNo(props) { +export function YesNo(props) { const { name, value } = props; const handleChange = value => { const { name } = props; @@ -73,5 +73,3 @@ YesNo.propTypes = { name: PropTypes.string, value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]) }; - -export default YesNo; diff --git a/src/legacy/core_plugins/metrics/public/components/yes_no.test.js b/src/legacy/core_plugins/metrics/public/components/yes_no.test.js index 09d936fde6f1f..4ecc0ec95757d 100644 --- a/src/legacy/core_plugins/metrics/public/components/yes_no.test.js +++ b/src/legacy/core_plugins/metrics/public/components/yes_no.test.js @@ -21,7 +21,7 @@ import React from 'react'; import { expect } from 'chai'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import sinon from 'sinon'; -import YesNo from './yes_no'; +import { YesNo } from './yes_no'; describe('YesNo', () => { it('call onChange={handleChange} on yes', () => { diff --git a/src/legacy/core_plugins/metrics/public/kbn_vis_types/editor_controller.js b/src/legacy/core_plugins/metrics/public/kbn_vis_types/editor_controller.js index 75072f5541fe2..b7ea7d7cc96fa 100644 --- a/src/legacy/core_plugins/metrics/public/kbn_vis_types/editor_controller.js +++ b/src/legacy/core_plugins/metrics/public/kbn_vis_types/editor_controller.js @@ -23,7 +23,7 @@ import { I18nContext } from 'ui/i18n'; import chrome from 'ui/chrome'; import { fetchIndexPatternFields } from '../lib/fetch_fields'; -function ReactEditorControllerProvider(Private, config) { +function ReactEditorControllerProvider(config) { class ReactEditorController { constructor(el, savedObj) { this.el = el; @@ -39,11 +39,17 @@ function ReactEditorControllerProvider(Private, config) { const savedObjectsClient = chrome.getSavedObjectsClient(); const indexPattern = await savedObjectsClient.get('index-pattern', config.get('defaultIndex')); - return indexPattern.attributes.title; + return indexPattern.attributes; }; fetchDefaultParams = async () => { - this.state.vis.params.default_index_pattern = await this.fetchDefaultIndexPattern(); + const { + title, + timeFieldName, + } = await this.fetchDefaultIndexPattern(); + + this.state.vis.params.default_index_pattern = title; + this.state.vis.params.default_timefield = timeFieldName; this.state.vis.fields = await fetchIndexPatternFields(this.state.vis); this.state.isLoaded = true; diff --git a/src/legacy/core_plugins/metrics/public/kbn_vis_types/index.js b/src/legacy/core_plugins/metrics/public/kbn_vis_types/index.js index 6040fc89076d4..b78d5056930c8 100644 --- a/src/legacy/core_plugins/metrics/public/kbn_vis_types/index.js +++ b/src/legacy/core_plugins/metrics/public/kbn_vis_types/index.js @@ -18,6 +18,7 @@ */ import { MetricsRequestHandlerProvider } from './request_handler'; +import { i18n } from '@kbn/i18n'; import { ReactEditorControllerProvider } from './editor_controller'; import { VisFactoryProvider } from 'ui/vis/vis_factory'; import { defaultFeedbackMessage } from 'ui/vis/default_feedback_message'; @@ -28,15 +29,15 @@ import { PANEL_TYPES } from '../../common/panel_types'; import { VisTypesRegistryProvider } from 'ui/registry/vis_types'; VisTypesRegistryProvider.register(MetricsVisProvider); -export default function MetricsVisProvider(Private, i18n) { +export function MetricsVisProvider(Private) { const VisFactory = Private(VisFactoryProvider); const ReactEditorController = Private(ReactEditorControllerProvider).handler; const metricsRequestHandler = Private(MetricsRequestHandlerProvider).handler; return VisFactory.createReactVisualization({ name: 'metrics', - title: i18n('tsvb.kbnVisTypes.metricsTitle', { defaultMessage: 'Visual Builder' }), - description: i18n('tsvb.kbnVisTypes.metricsDescription', + title: i18n.translate('tsvb.kbnVisTypes.metricsTitle', { defaultMessage: 'Visual Builder' }), + description: i18n.translate('tsvb.kbnVisTypes.metricsDescription', { defaultMessage: 'Build time-series using a visual pipeline interface' }), icon: 'visVisualBuilder', feedbackMessage: defaultFeedbackMessage, @@ -63,20 +64,20 @@ export default function MetricsVisProvider(Private, i18n) { fill: 0.5, stacked: 'none' }], - time_field: '@timestamp', + time_field: '', index_pattern: '', - interval: 'auto', + interval: '', axis_position: 'left', axis_formatter: 'number', axis_scale: 'normal', show_legend: 1, show_grid: 1 }, - component: require('../components/vis_editor') + component: require('../components/vis_editor').VisEditor }, editor: ReactEditorController, editorConfig: { - component: require('../components/vis_editor') + component: require('../components/vis_editor').VisEditor }, options: { showQueryBar: false, diff --git a/src/legacy/core_plugins/metrics/public/lib/__tests__/create_brush_handler.test.js b/src/legacy/core_plugins/metrics/public/lib/__tests__/create_brush_handler.test.js index 54a8b757f968f..92d5e4f5ab731 100644 --- a/src/legacy/core_plugins/metrics/public/lib/__tests__/create_brush_handler.test.js +++ b/src/legacy/core_plugins/metrics/public/lib/__tests__/create_brush_handler.test.js @@ -17,11 +17,11 @@ * under the License. */ -import createBrushHandler from '../create_brush_handler'; +import { brushHandler } from '../create_brush_handler'; import moment from 'moment'; import { expect } from 'chai'; -describe('createBrushHandler', () => { +describe('brushHandler', () => { let mockTimefilter; let onBrush; let range; @@ -31,7 +31,7 @@ describe('createBrushHandler', () => { time: {}, setTime: function (time) { this.time = time; } }; - onBrush = createBrushHandler(mockTimefilter); + onBrush = brushHandler(mockTimefilter); }); test('returns brushHandler() that updates timefilter', () => { diff --git a/src/legacy/core_plugins/metrics/public/lib/__tests__/extract_index_pattern.js b/src/legacy/core_plugins/metrics/public/lib/__tests__/extract_index_pattern.js deleted file mode 100644 index b9d1e8e6b5a58..0000000000000 --- a/src/legacy/core_plugins/metrics/public/lib/__tests__/extract_index_pattern.js +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { extractIndexPatterns } from '../extract_index_patterns'; -import { expect } from 'chai'; -describe('extractIndexPatterns(vis)', () => { - let visParams; - let visFields; - beforeEach(() => { - visFields = { - '*': [] - }; - visParams = { - index_pattern: '*', - series: [ - { - override_index_pattern: 1, - series_index_pattern: 'example-1-*' - }, - { - override_index_pattern: 1, - series_index_pattern: 'example-2-*' - } - ], - annotations: [ - { index_pattern: 'notes-*' }, - { index_pattern: 'example-1-*' } - ] - }; - }); - - it('should return index patterns', () => { - visFields = {}; - expect(extractIndexPatterns(visParams, visFields)).to.eql([ - '*', - 'example-1-*', - 'example-2-*', - 'notes-*' - ]); - }); - - it('should return index patterns that do not exist in visFields', () => { - expect(extractIndexPatterns(visParams, visFields)).to.eql([ - 'example-1-*', - 'example-2-*', - 'notes-*' - ]); - }); -}); diff --git a/src/legacy/core_plugins/metrics/public/lib/create_brush_handler.js b/src/legacy/core_plugins/metrics/public/lib/create_brush_handler.js index e970a4d85960f..5d674c26bfa7b 100644 --- a/src/legacy/core_plugins/metrics/public/lib/create_brush_handler.js +++ b/src/legacy/core_plugins/metrics/public/lib/create_brush_handler.js @@ -18,7 +18,8 @@ */ import moment from 'moment'; -export default (timefilter) => ranges => { + +export const brushHandler = (timefilter) => ranges => { timefilter.setTime({ from: moment(ranges.xaxis.from).toISOString(), to: moment(ranges.xaxis.to).toISOString(), diff --git a/src/legacy/core_plugins/metrics/public/lib/extract_index_patterns.js b/src/legacy/core_plugins/metrics/public/lib/extract_index_patterns.js deleted file mode 100644 index 4b9b8cfa5d213..0000000000000 --- a/src/legacy/core_plugins/metrics/public/lib/extract_index_patterns.js +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { uniq } from 'lodash'; - -export function extractIndexPatterns(params, fetchedFields = {}) { - const patternsToFetch = []; - - if (!fetchedFields[params.index_pattern]) { - patternsToFetch.push(params.index_pattern); - } - - params.series.forEach(series => { - const indexPattern = series.series_index_pattern; - if (series.override_index_pattern && !fetchedFields[indexPattern]) { - patternsToFetch.push(indexPattern); - } - }); - - if (params.annotations) { - params.annotations.forEach(item => { - const indexPattern = item.index_pattern; - if (indexPattern && !fetchedFields[indexPattern]) { - patternsToFetch.push(indexPattern); - } - }); - } - - if (patternsToFetch.length === 0) { - patternsToFetch.push(''); - } - - return uniq(patternsToFetch).sort(); -} diff --git a/src/legacy/core_plugins/metrics/public/lib/fetch_fields.js b/src/legacy/core_plugins/metrics/public/lib/fetch_fields.js index de60b5ac0e7cd..71ec7980713f0 100644 --- a/src/legacy/core_plugins/metrics/public/lib/fetch_fields.js +++ b/src/legacy/core_plugins/metrics/public/lib/fetch_fields.js @@ -19,7 +19,7 @@ import { kfetch } from 'ui/kfetch'; import { toastNotifications } from 'ui/notify'; import { i18n } from '@kbn/i18n'; -import { extractIndexPatterns } from './extract_index_patterns'; +import { extractIndexPatterns } from '../../common/extract_index_patterns'; export async function fetchFields(indexPatterns = ['*']) { const patterns = Array.isArray(indexPatterns) ? indexPatterns : [indexPatterns]; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/components/annotation.js b/src/legacy/core_plugins/metrics/public/visualizations/components/annotation.js index 6a84aca025f06..c1bf4ac84e6db 100644 --- a/src/legacy/core_plugins/metrics/public/visualizations/components/annotation.js +++ b/src/legacy/core_plugins/metrics/public/visualizations/components/annotation.js @@ -22,7 +22,8 @@ import React, { Component } from 'react'; import moment from 'moment'; import reactcss from 'reactcss'; import { EuiToolTip } from '@elastic/eui'; -class Annotation extends Component { + +export class Annotation extends Component { constructor(props) { super(props); @@ -82,5 +83,3 @@ Annotation.propTypes = { color: PropTypes.string, plot: PropTypes.object, }; - -export default Annotation; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/components/flot_chart.js b/src/legacy/core_plugins/metrics/public/visualizations/components/flot_chart.js index 3de834f82c739..3373bf96c3d3e 100644 --- a/src/legacy/core_plugins/metrics/public/visualizations/components/flot_chart.js +++ b/src/legacy/core_plugins/metrics/public/visualizations/components/flot_chart.js @@ -22,13 +22,13 @@ import PropTypes from 'prop-types'; import { findDOMNode } from 'react-dom'; import _ from 'lodash'; import $ from 'ui/flot-charts'; -import eventBus from '../lib/events'; -import Resize from './resize'; -import calculateBarWidth from '../lib/calculate_bar_width'; -import calculateFillColor from '../lib/calculate_fill_color'; -import colors from '../lib/colors'; +import { eventBus } from '../lib/events'; +import { Resize } from './resize'; +import { calculateBarWidth } from '../lib/calculate_bar_width'; +import { calculateFillColor } from '../lib/calculate_fill_color'; +import { COLORS } from '../lib/colors'; -class FlotChart extends Component { +export class FlotChart extends Component { constructor(props) { super(props); this.handleResize = this.handleResize.bind(this); @@ -154,8 +154,8 @@ class FlotChart extends Component { getOptions(props) { const yaxes = props.yaxes || [{}]; - const lineColor = colors.lineColor; - const textColor = props.reversed ? colors.textColorReversed : colors.textColor; + const lineColor = COLORS.lineColor; + const textColor = props.reversed ? COLORS.textColorReversed : COLORS.textColor; const borderWidth = { bottom: 1, top: 0, left: 0, right: 0 }; @@ -344,5 +344,3 @@ FlotChart.propTypes = { showGrid: PropTypes.bool, yaxes: PropTypes.array }; - -export default FlotChart; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/components/gauge.js b/src/legacy/core_plugins/metrics/public/visualizations/components/gauge.js index abac92a218e60..4b73f7424f7a3 100644 --- a/src/legacy/core_plugins/metrics/public/visualizations/components/gauge.js +++ b/src/legacy/core_plugins/metrics/public/visualizations/components/gauge.js @@ -22,13 +22,13 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import classNames from 'classnames'; import { isBackgroundInverted, isBackgroundDark } from '../../../common/set_is_reversed'; -import getLastValue from '../../../common/get_last_value'; -import getValueBy from '../lib/get_value_by'; -import GaugeVis from './gauge_vis'; +import { getLastValue } from '../../../common/get_last_value'; +import { getValueBy } from '../lib/get_value_by'; +import { GaugeVis } from './gauge_vis'; import reactcss from 'reactcss'; -import calculateCoordinates from '../lib/calculate_coordinates'; +import { calculateCoordinates } from '../lib/calculate_coordinates'; -class Gauge extends Component { +export class Gauge extends Component { constructor(props) { super(props); @@ -200,6 +200,3 @@ Gauge.propTypes = { valueColor: PropTypes.string, additionalLabel: PropTypes.string }; - -export default Gauge; - diff --git a/src/legacy/core_plugins/metrics/public/visualizations/components/gauge_vis.js b/src/legacy/core_plugins/metrics/public/visualizations/components/gauge_vis.js index 3aa52f6c81e5c..8798f42117130 100644 --- a/src/legacy/core_plugins/metrics/public/visualizations/components/gauge_vis.js +++ b/src/legacy/core_plugins/metrics/public/visualizations/components/gauge_vis.js @@ -21,10 +21,10 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import _ from 'lodash'; import reactcss from 'reactcss'; -import calculateCoordinates from '../lib/calculate_coordinates'; -import colors from '../lib/colors'; +import { calculateCoordinates } from '../lib/calculate_coordinates'; +import { COLORS } from '../lib/colors'; -class GaugeVis extends Component { +export class GaugeVis extends Component { constructor(props) { super(props); @@ -125,7 +125,7 @@ class GaugeVis extends Component { cx: 60, cy: 60, fill: 'rgba(0,0,0,0)', - stroke: colors.lineColor, + stroke: COLORS.lineColor, strokeDasharray: `${sliceSize * size} ${size}`, strokeWidth: this.props.innerLine } @@ -186,6 +186,3 @@ GaugeVis.propTypes = { value: PropTypes.number, type: PropTypes.oneOf(['half', 'circle']) }; - -export default GaugeVis; - diff --git a/src/legacy/core_plugins/metrics/public/visualizations/components/horizontal_legend.js b/src/legacy/core_plugins/metrics/public/visualizations/components/horizontal_legend.js index b98f4fb561681..02f39c0c472b9 100644 --- a/src/legacy/core_plugins/metrics/public/visualizations/components/horizontal_legend.js +++ b/src/legacy/core_plugins/metrics/public/visualizations/components/horizontal_legend.js @@ -19,12 +19,12 @@ import PropTypes from 'prop-types'; import React from 'react'; -import createLegendSeries from '../lib/create_legend_series'; +import { createLegendSeries } from '../lib/create_legend_series'; import reactcss from 'reactcss'; import { htmlIdGenerator, EuiButtonIcon } from '@elastic/eui'; import { injectI18n } from '@kbn/i18n/react'; -const HorizontalLegend = injectI18n(function (props) { +export const HorizontalLegend = injectI18n(function (props) { const rows = props.series.map(createLegendSeries(props)); const htmlId = htmlIdGenerator(); const styles = reactcss({ @@ -69,5 +69,3 @@ HorizontalLegend.propTypes = { seriesFilter: PropTypes.array, tickFormatter: PropTypes.func }; - -export default HorizontalLegend; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/components/legend.js b/src/legacy/core_plugins/metrics/public/visualizations/components/legend.js index f5023b58623d5..b23a28cacccb5 100644 --- a/src/legacy/core_plugins/metrics/public/visualizations/components/legend.js +++ b/src/legacy/core_plugins/metrics/public/visualizations/components/legend.js @@ -19,10 +19,10 @@ import PropTypes from 'prop-types'; import React from 'react'; -import VerticalLegend from './vertical_legend'; -import HorizontalLegend from './horizontal_legend'; +import { VerticalLegend } from './vertical_legend'; +import { HorizontalLegend } from './horizontal_legend'; -function Legend(props) { +export function Legend(props) { if (props.legendPosition === 'bottom') { return (); } @@ -39,5 +39,3 @@ Legend.propTypes = { seriesFilter: PropTypes.array, tickFormatter: PropTypes.func }; - -export default Legend; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/components/metric.js b/src/legacy/core_plugins/metrics/public/visualizations/components/metric.js index f21de4e69e547..0e8c558b25dfe 100644 --- a/src/legacy/core_plugins/metrics/public/visualizations/components/metric.js +++ b/src/legacy/core_plugins/metrics/public/visualizations/components/metric.js @@ -20,11 +20,11 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import _ from 'lodash'; -import getLastValue from '../../../common/get_last_value'; +import { getLastValue } from '../../../common/get_last_value'; import reactcss from 'reactcss'; -import calculateCoordinates from '../lib/calculate_coordinates'; +import { calculateCoordinates } from '../lib/calculate_coordinates'; -class Metric extends Component { +export class Metric extends Component { constructor(props) { super(props); @@ -167,5 +167,3 @@ Metric.propTypes = { reversed: PropTypes.bool, additionalLabel: PropTypes.string }; - -export default Metric; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/components/resize.js b/src/legacy/core_plugins/metrics/public/visualizations/components/resize.js index 2dd47a35ff942..a164626fe8fe0 100644 --- a/src/legacy/core_plugins/metrics/public/visualizations/components/resize.js +++ b/src/legacy/core_plugins/metrics/public/visualizations/components/resize.js @@ -20,7 +20,8 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { findDOMNode } from 'react-dom'; -class Resize extends Component { + +export class Resize extends Component { constructor(props) { super(props); @@ -81,5 +82,3 @@ Resize.propTypes = { frequency: PropTypes.number, onResize: PropTypes.func }; - -export default Resize; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/components/timeseries.js b/src/legacy/core_plugins/metrics/public/visualizations/components/timeseries.js index fa299c5f1e0fe..dc6b4c1e56f76 100644 --- a/src/legacy/core_plugins/metrics/public/visualizations/components/timeseries.js +++ b/src/legacy/core_plugins/metrics/public/visualizations/components/timeseries.js @@ -21,14 +21,14 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import classNames from 'classnames'; import _ from 'lodash'; -import getLastValue from '../../../common/get_last_value'; +import { getLastValue } from '../../../common/get_last_value'; import { isBackgroundInverted } from '../../../common/set_is_reversed'; -import TimeseriesChart from './timeseries_chart'; -import Legend from './legend'; -import eventBus from '../lib/events'; +import { TimeseriesChart } from './timeseries_chart'; +import { Legend } from './legend'; +import { eventBus } from '../lib/events'; import reactcss from 'reactcss'; -class Timeseries extends Component { +export class Timeseries extends Component { constructor(props) { super(props); @@ -196,5 +196,3 @@ Timeseries.propTypes = { xaxisLabel: PropTypes.string, dateFormat: PropTypes.string }; - -export default Timeseries; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/components/timeseries_chart.js b/src/legacy/core_plugins/metrics/public/visualizations/components/timeseries_chart.js index 39f44b2f821d1..702758b29775f 100644 --- a/src/legacy/core_plugins/metrics/public/visualizations/components/timeseries_chart.js +++ b/src/legacy/core_plugins/metrics/public/visualizations/components/timeseries_chart.js @@ -23,8 +23,8 @@ import classNames from 'classnames'; import { isBackgroundInverted, isBackgroundDark } from '../../../common/set_is_reversed'; import moment from 'moment'; import reactcss from 'reactcss'; -import FlotChart from './flot_chart'; -import Annotation from './annotation'; +import { FlotChart } from './flot_chart'; +import { Annotation } from './annotation'; import { EuiIcon } from '@elastic/eui'; export function scaleUp(value) { @@ -35,7 +35,7 @@ export function scaleDown(value) { return value / window.devicePixelRatio; } -class TimeseriesChart extends Component { +export class TimeseriesChart extends Component { constructor(props) { super(props); @@ -219,5 +219,3 @@ TimeseriesChart.propTypes = { xaxisLabel: PropTypes.string, dateFormat: PropTypes.string }; - -export default TimeseriesChart; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/components/top_n.js b/src/legacy/core_plugins/metrics/public/visualizations/components/top_n.js index 8761dba34cca2..0e587ede48546 100644 --- a/src/legacy/core_plugins/metrics/public/visualizations/components/top_n.js +++ b/src/legacy/core_plugins/metrics/public/visualizations/components/top_n.js @@ -19,10 +19,10 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import getLastValue from '../../../common/get_last_value'; +import { getLastValue } from '../../../common/get_last_value'; import reactcss from 'reactcss'; -class TopN extends Component { +export class TopN extends Component { constructor(props) { super(props); @@ -143,5 +143,3 @@ TopN.propTypes = { reversed: PropTypes.bool, direction: PropTypes.string }; - -export default TopN; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/components/vertical_legend.js b/src/legacy/core_plugins/metrics/public/visualizations/components/vertical_legend.js index 67b2c38b3f8ff..2a0ad5c37e0a7 100644 --- a/src/legacy/core_plugins/metrics/public/visualizations/components/vertical_legend.js +++ b/src/legacy/core_plugins/metrics/public/visualizations/components/vertical_legend.js @@ -19,12 +19,12 @@ import PropTypes from 'prop-types'; import React from 'react'; -import createLegendSeries from '../lib/create_legend_series'; +import { createLegendSeries } from '../lib/create_legend_series'; import reactcss from 'reactcss'; import { htmlIdGenerator, EuiButtonIcon } from '@elastic/eui'; import { injectI18n } from '@kbn/i18n/react'; -const VerticalLegend = injectI18n(function (props) { +export const VerticalLegend = injectI18n(function (props) { const rows = props.series.map(createLegendSeries(props)); const htmlId = htmlIdGenerator(); const hideLegend = !props.showLegend; @@ -81,5 +81,3 @@ VerticalLegend.propTypes = { seriesFilter: PropTypes.array, tickFormatter: PropTypes.func }; - -export default VerticalLegend; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/lib/__tests__/calcualte_bar_width.test.js b/src/legacy/core_plugins/metrics/public/visualizations/lib/__tests__/calcualte_bar_width.test.js index 85bba1c971265..bfcc9eb5a8ca4 100644 --- a/src/legacy/core_plugins/metrics/public/visualizations/lib/__tests__/calcualte_bar_width.test.js +++ b/src/legacy/core_plugins/metrics/public/visualizations/lib/__tests__/calcualte_bar_width.test.js @@ -18,7 +18,7 @@ */ import { expect } from 'chai'; -import calculateBarWidth from '../calculate_bar_width'; +import { calculateBarWidth } from '../calculate_bar_width'; describe('calculateBarWidth(series, divisor, multiplier)', () => { diff --git a/src/legacy/core_plugins/metrics/public/visualizations/lib/__tests__/calculate_fill_color.test.js b/src/legacy/core_plugins/metrics/public/visualizations/lib/__tests__/calculate_fill_color.test.js index 52e48ee0fe2f0..c43c0a16f24dc 100644 --- a/src/legacy/core_plugins/metrics/public/visualizations/lib/__tests__/calculate_fill_color.test.js +++ b/src/legacy/core_plugins/metrics/public/visualizations/lib/__tests__/calculate_fill_color.test.js @@ -18,7 +18,7 @@ */ import { expect } from 'chai'; -import calculateFillColor from '../calculate_fill_color'; +import { calculateFillColor } from '../calculate_fill_color'; describe('calculateFillColor(color, fill)', () => { it('should return "fill" and "fillColor" properties', () => { diff --git a/src/legacy/core_plugins/metrics/public/visualizations/lib/__tests__/get_value_by.test.js b/src/legacy/core_plugins/metrics/public/visualizations/lib/__tests__/get_value_by.test.js index 366e1b13d92e4..a62c8cf2b20da 100644 --- a/src/legacy/core_plugins/metrics/public/visualizations/lib/__tests__/get_value_by.test.js +++ b/src/legacy/core_plugins/metrics/public/visualizations/lib/__tests__/get_value_by.test.js @@ -17,7 +17,7 @@ * under the License. */ -import getValueBy from '../get_value_by'; +import { getValueBy } from '../get_value_by'; import { expect } from 'chai'; describe('getValueBy(fn, data)', () => { diff --git a/src/legacy/core_plugins/metrics/public/visualizations/lib/calc_dimensions.js b/src/legacy/core_plugins/metrics/public/visualizations/lib/calc_dimensions.js index e49724642c8c9..d06ac0809a745 100644 --- a/src/legacy/core_plugins/metrics/public/visualizations/lib/calc_dimensions.js +++ b/src/legacy/core_plugins/metrics/public/visualizations/lib/calc_dimensions.js @@ -17,7 +17,7 @@ * under the License. */ -export default function calcDimensions(el, scale) { +export function calcDimensions(el, scale) { const newWidth = Math.floor(el.clientWidth * scale); const newHeight = Math.floor(el.clientHeight * scale); return [newWidth, newHeight]; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/lib/calculate_bar_width.js b/src/legacy/core_plugins/metrics/public/visualizations/lib/calculate_bar_width.js index bda7375668b63..ce1be3cd6f1e2 100644 --- a/src/legacy/core_plugins/metrics/public/visualizations/lib/calculate_bar_width.js +++ b/src/legacy/core_plugins/metrics/public/visualizations/lib/calculate_bar_width.js @@ -21,7 +21,7 @@ import _ from 'lodash'; // bar sizes are measured in milliseconds so this assumes that the different // between timestamps is in milliseconds. A normal bar size is 70% which gives // enough spacing for the bar. -export default (series, multiplier = 0.7) => { +export const calculateBarWidth = (series, multiplier = 0.7) => { const first = _.first(series); try { return ((first.data[1][0] - first.data[0][0])) * multiplier; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/lib/calculate_coordinates.js b/src/legacy/core_plugins/metrics/public/visualizations/lib/calculate_coordinates.js index 93cd1d61058ed..cbc77b306b0d9 100644 --- a/src/legacy/core_plugins/metrics/public/visualizations/lib/calculate_coordinates.js +++ b/src/legacy/core_plugins/metrics/public/visualizations/lib/calculate_coordinates.js @@ -18,8 +18,9 @@ */ import { findDOMNode } from 'react-dom'; -import calcDimensions from './calc_dimensions'; -export default function calculateCoordinates(innerRef, resizeRef, state) { +import { calcDimensions } from './calc_dimensions'; + +export function calculateCoordinates(innerRef, resizeRef, state) { const inner = findDOMNode(innerRef); const resize = findDOMNode(resizeRef); let scale = state.scale; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/lib/calculate_fill_color.js b/src/legacy/core_plugins/metrics/public/visualizations/lib/calculate_fill_color.js index f84b3df128398..133d0db463e86 100644 --- a/src/legacy/core_plugins/metrics/public/visualizations/lib/calculate_fill_color.js +++ b/src/legacy/core_plugins/metrics/public/visualizations/lib/calculate_fill_color.js @@ -19,7 +19,7 @@ import Color from 'color'; -export default (color, fill = 1) => { +export const calculateFillColor = (color, fill = 1) => { const initialColor = new Color(color).rgb(); const opacity = Math.min(Number(fill), 1) * initialColor.valpha; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/lib/colors.js b/src/legacy/core_plugins/metrics/public/visualizations/lib/colors.js index 83bc12b645b62..14bcec9a31162 100644 --- a/src/legacy/core_plugins/metrics/public/visualizations/lib/colors.js +++ b/src/legacy/core_plugins/metrics/public/visualizations/lib/colors.js @@ -17,7 +17,7 @@ * under the License. */ -export default { +export const COLORS = { lineColor: 'rgba(105,112,125,0.2)', textColor: 'rgba(0,0,0,0.4)', textColorReversed: 'rgba(255,255,255,0.5)', diff --git a/src/legacy/core_plugins/metrics/public/visualizations/lib/create_legend_series.js b/src/legacy/core_plugins/metrics/public/visualizations/lib/create_legend_series.js index 6504ad9e8cdd3..45d9ff019c086 100644 --- a/src/legacy/core_plugins/metrics/public/visualizations/lib/create_legend_series.js +++ b/src/legacy/core_plugins/metrics/public/visualizations/lib/create_legend_series.js @@ -21,7 +21,7 @@ import React from 'react'; import _ from 'lodash'; import { EuiIcon } from '@elastic/eui'; -export default props => (row, index = 0) => { +export const createLegendSeries = props => (row, index = 0) => { function tickFormatter(value) { if (_.isFunction(props.tickFormatter)) return props.tickFormatter(value); diff --git a/src/legacy/core_plugins/metrics/public/visualizations/lib/events.js b/src/legacy/core_plugins/metrics/public/visualizations/lib/events.js index 91b9758444d81..63e5fe5c3a2a5 100644 --- a/src/legacy/core_plugins/metrics/public/visualizations/lib/events.js +++ b/src/legacy/core_plugins/metrics/public/visualizations/lib/events.js @@ -18,5 +18,6 @@ */ import $ from 'jquery'; -export default $({}); + +export const eventBus = $({}); diff --git a/src/legacy/core_plugins/metrics/public/visualizations/lib/get_value_by.js b/src/legacy/core_plugins/metrics/public/visualizations/lib/get_value_by.js index d23ab9cd6ab9e..e5453102bfbe3 100644 --- a/src/legacy/core_plugins/metrics/public/visualizations/lib/get_value_by.js +++ b/src/legacy/core_plugins/metrics/public/visualizations/lib/get_value_by.js @@ -18,7 +18,8 @@ */ import _ from 'lodash'; -export default (fn, data) => { + +export const getValueBy = (fn, data) => { if (_.isNumber(data)) return data; if (!Array.isArray(data)) return 0; const values = data.map(v => v[1]); diff --git a/src/legacy/core_plugins/metrics/server/lib/get_vis_data.js b/src/legacy/core_plugins/metrics/server/lib/get_vis_data.js index 3bb385fbe7d62..4fe3626b5a252 100644 --- a/src/legacy/core_plugins/metrics/server/lib/get_vis_data.js +++ b/src/legacy/core_plugins/metrics/server/lib/get_vis_data.js @@ -18,9 +18,9 @@ */ import _ from 'lodash'; -import getPanelData from './vis_data/get_panel_data'; +import { getPanelData } from './vis_data/get_panel_data'; -function getVisData(req) { +export function getVisData(req) { const promises = req.payload.panels.map(getPanelData(req)); return Promise.all(promises) .then(res => { @@ -30,5 +30,3 @@ function getVisData(req) { }); } -export default getVisData; - diff --git a/src/legacy/core_plugins/metrics/server/lib/search_strategies/default_search_capabilities.js b/src/legacy/core_plugins/metrics/server/lib/search_strategies/default_search_capabilities.js index 0d82cf7cb253f..bbda5c62fc3c2 100644 --- a/src/legacy/core_plugins/metrics/server/lib/search_strategies/default_search_capabilities.js +++ b/src/legacy/core_plugins/metrics/server/lib/search_strategies/default_search_capabilities.js @@ -16,16 +16,15 @@ * specific language governing permissions and limitations * under the License. */ -import { convertIntervalToUnit, parseInterval } from '../vis_data/helpers/unit_to_seconds'; +import { convertIntervalToUnit, parseInterval, getSuitableUnit } from '../vis_data/helpers/unit_to_seconds'; const getTimezoneFromRequest = request => { return request.payload.timerange.timezone; }; export class DefaultSearchCapabilities { - constructor(request, batchRequestsSupport, fieldsCapabilities = {}) { + constructor(request, fieldsCapabilities = {}) { this.request = request; - this.batchRequestsSupport = batchRequestsSupport; this.fieldsCapabilities = fieldsCapabilities; } @@ -63,6 +62,10 @@ export class DefaultSearchCapabilities { return parseInterval(interval); } + getSuitableUnit(intervalInSeconds) { + return getSuitableUnit(intervalInSeconds); + } + convertIntervalToUnit(intervalString, unit) { const parsedInterval = this.parseInterval(intervalString); diff --git a/src/legacy/core_plugins/metrics/server/lib/search_strategies/default_search_capabilities.test.js b/src/legacy/core_plugins/metrics/server/lib/search_strategies/default_search_capabilities.test.js index 5d41e03722d9d..1294d2dc72110 100644 --- a/src/legacy/core_plugins/metrics/server/lib/search_strategies/default_search_capabilities.test.js +++ b/src/legacy/core_plugins/metrics/server/lib/search_strategies/default_search_capabilities.test.js @@ -20,18 +20,15 @@ import { DefaultSearchCapabilities } from './default_search_capabilities'; describe('DefaultSearchCapabilities', () => { let defaultSearchCapabilities; - let batchRequestsSupport; let req; beforeEach(() => { req = {}; - batchRequestsSupport = true; - defaultSearchCapabilities = new DefaultSearchCapabilities(req, batchRequestsSupport); + defaultSearchCapabilities = new DefaultSearchCapabilities(req); }); test('should init default search capabilities', () => { expect(defaultSearchCapabilities.request).toBe(req); - expect(defaultSearchCapabilities.batchRequestsSupport).toBe(batchRequestsSupport); expect(defaultSearchCapabilities.fieldsCapabilities).toEqual({}); }); diff --git a/src/legacy/core_plugins/metrics/server/lib/search_strategies/search_strategies_register.js b/src/legacy/core_plugins/metrics/server/lib/search_strategies/search_strategies_register.js index 159e25191fc94..5fbb4060862a4 100644 --- a/src/legacy/core_plugins/metrics/server/lib/search_strategies/search_strategies_register.js +++ b/src/legacy/core_plugins/metrics/server/lib/search_strategies/search_strategies_register.js @@ -21,6 +21,8 @@ import { AbstractSearchRequest } from './searh_requests/abstract_request'; import { DefaultSearchStrategy } from './strategies/default_search_strategy'; import { DefaultSearchCapabilities } from './default_search_capabilities'; +import { extractIndexPatterns } from '../../../common/extract_index_patterns'; + const strategies = []; const addStrategy = searchStrategy => { @@ -52,4 +54,10 @@ export class SearchStrategiesRegister { } } } + + static async getViableStrategyForPanel(req, panel) { + const indexPattern = extractIndexPatterns(panel).join(','); + + return SearchStrategiesRegister.getViableStrategy(req, indexPattern); + } } diff --git a/src/legacy/core_plugins/metrics/server/lib/search_strategies/searh_requests/abstract_request.js b/src/legacy/core_plugins/metrics/server/lib/search_strategies/searh_requests/abstract_request.js index cfacaf18a5abf..abd2a4c65d35c 100644 --- a/src/legacy/core_plugins/metrics/server/lib/search_strategies/searh_requests/abstract_request.js +++ b/src/legacy/core_plugins/metrics/server/lib/search_strategies/searh_requests/abstract_request.js @@ -17,10 +17,9 @@ * under the License. */ export class AbstractSearchRequest { - constructor(req, callWithRequest, indexPattern) { + constructor(req, callWithRequest) { this.req = req; this.callWithRequest = callWithRequest; - this.indexPattern = indexPattern; } search() { diff --git a/src/legacy/core_plugins/metrics/server/lib/search_strategies/searh_requests/abstract_request.test.js b/src/legacy/core_plugins/metrics/server/lib/search_strategies/searh_requests/abstract_request.test.js index 16dbd9b580f69..778fe46b862d6 100644 --- a/src/legacy/core_plugins/metrics/server/lib/search_strategies/searh_requests/abstract_request.test.js +++ b/src/legacy/core_plugins/metrics/server/lib/search_strategies/searh_requests/abstract_request.test.js @@ -22,19 +22,16 @@ describe('AbstractSearchRequest', () => { let searchRequest; let req; let callWithRequest; - let indexPattern; beforeEach(() => { req = {}; callWithRequest = jest.fn(); - indexPattern = 'indexPattern'; - searchRequest = new AbstractSearchRequest(req, callWithRequest, indexPattern); + searchRequest = new AbstractSearchRequest(req, callWithRequest); }); test('should init an AbstractSearchRequest instance', () => { expect(searchRequest.req).toBe(req); expect(searchRequest.callWithRequest).toBe(callWithRequest); - expect(searchRequest.indexPattern).toBe(indexPattern); expect(searchRequest.search).toBeDefined(); }); diff --git a/src/legacy/core_plugins/metrics/server/lib/search_strategies/searh_requests/multi_search_request.js b/src/legacy/core_plugins/metrics/server/lib/search_strategies/searh_requests/multi_search_request.js index 793448073397b..0eefff40d2097 100644 --- a/src/legacy/core_plugins/metrics/server/lib/search_strategies/searh_requests/multi_search_request.js +++ b/src/legacy/core_plugins/metrics/server/lib/search_strategies/searh_requests/multi_search_request.js @@ -21,10 +21,19 @@ import { AbstractSearchRequest } from './abstract_request'; const SEARCH_METHOD = 'msearch'; export class MultiSearchRequest extends AbstractSearchRequest { - async search(options) { + async search(searches) { const includeFrozen = await this.req.getUiSettingsService().get('search:includeFrozen'); + const multiSearchBody = searches.reduce((acc, { body, index }) => ([ + ...acc, + { + index, + ignoreUnavailable: true, + }, + body, + ]), []); + const { responses } = await this.callWithRequest(this.req, SEARCH_METHOD, { - ...options, + body: multiSearchBody, rest_total_hits_as_int: true, ignore_throttled: !includeFrozen, }); diff --git a/src/legacy/core_plugins/metrics/server/lib/search_strategies/searh_requests/multi_search_request.test.js b/src/legacy/core_plugins/metrics/server/lib/search_strategies/searh_requests/multi_search_request.test.js index 48d24f7622796..1e28965a35793 100644 --- a/src/legacy/core_plugins/metrics/server/lib/search_strategies/searh_requests/multi_search_request.test.js +++ b/src/legacy/core_plugins/metrics/server/lib/search_strategies/searh_requests/multi_search_request.test.js @@ -22,7 +22,6 @@ describe('MultiSearchRequest', () => { let searchRequest; let req; let callWithRequest; - let indexPattern; let getServiceMock; let includeFrozen; @@ -30,30 +29,36 @@ describe('MultiSearchRequest', () => { includeFrozen = false; getServiceMock = jest.fn().mockResolvedValue(includeFrozen); req = { - getUiSettingsService: jest.fn().mockReturnValue({ get: getServiceMock }) + getUiSettingsService: jest.fn().mockReturnValue({ get: getServiceMock }), }; callWithRequest = jest.fn().mockReturnValue({ responses: [] }); - indexPattern = 'indexPattern'; - searchRequest = new MultiSearchRequest(req, callWithRequest, indexPattern); + searchRequest = new MultiSearchRequest(req, callWithRequest); }); test('should init an MultiSearchRequest instance', () => { expect(searchRequest.req).toBe(req); expect(searchRequest.callWithRequest).toBe(callWithRequest); - expect(searchRequest.indexPattern).toBe(indexPattern); expect(searchRequest.search).toBeDefined(); }); test('should get the response from elastic msearch', async () => { - const options = {}; + const searches = [ + { body: 'body1', index: 'index' }, + { body: 'body2', index: 'index' }, + ]; - const responses = await searchRequest.search(options); + const responses = await searchRequest.search(searches); expect(responses).toEqual([]); expect(req.getUiSettingsService).toHaveBeenCalled(); expect(getServiceMock).toHaveBeenCalledWith('search:includeFrozen'); expect(callWithRequest).toHaveBeenCalledWith(req, 'msearch', { - ...options, + body: [ + { ignoreUnavailable: true, index: 'index' }, + 'body1', + { ignoreUnavailable: true, index: 'index' }, + 'body2', + ], rest_total_hits_as_int: true, ignore_throttled: !includeFrozen, }); diff --git a/src/legacy/core_plugins/metrics/server/lib/search_strategies/searh_requests/search_request.js b/src/legacy/core_plugins/metrics/server/lib/search_strategies/searh_requests/search_request.js index afab9d37f3018..e6e3bcb527286 100644 --- a/src/legacy/core_plugins/metrics/server/lib/search_strategies/searh_requests/search_request.js +++ b/src/legacy/core_plugins/metrics/server/lib/search_strategies/searh_requests/search_request.js @@ -22,11 +22,11 @@ import { MultiSearchRequest } from './multi_search_request'; import { SingleSearchRequest } from './single_search_request'; export class SearchRequest extends AbstractSearchRequest { - getSearchRequestType(options) { - const isMultiSearch = Array.isArray(options.body); + getSearchRequestType(searches) { + const isMultiSearch = Array.isArray(searches) && searches.length > 1; const SearchRequest = isMultiSearch ? MultiSearchRequest : SingleSearchRequest; - return new SearchRequest(this.req, this.callWithRequest, this.indexPattern); + return new SearchRequest(this.req, this.callWithRequest); } async search(options) { diff --git a/src/legacy/core_plugins/metrics/server/lib/search_strategies/searh_requests/search_request.test.js b/src/legacy/core_plugins/metrics/server/lib/search_strategies/searh_requests/search_request.test.js index 608f8abddf95d..538ad3a8875ff 100644 --- a/src/legacy/core_plugins/metrics/server/lib/search_strategies/searh_requests/search_request.test.js +++ b/src/legacy/core_plugins/metrics/server/lib/search_strategies/searh_requests/search_request.test.js @@ -24,7 +24,6 @@ describe('SearchRequest', () => { let searchRequest; let req; let callWithRequest; - let indexPattern; let getServiceMock; let includeFrozen; @@ -32,23 +31,21 @@ describe('SearchRequest', () => { includeFrozen = false; getServiceMock = jest.fn().mockResolvedValue(includeFrozen); req = { - getUiSettingsService: jest.fn().mockReturnValue({ get: getServiceMock }) + getUiSettingsService: jest.fn().mockReturnValue({ get: getServiceMock }), }; callWithRequest = jest.fn().mockReturnValue({ responses: [] }); - indexPattern = 'indexPattern'; - searchRequest = new SearchRequest(req, callWithRequest, indexPattern); + searchRequest = new SearchRequest(req, callWithRequest); }); test('should init an AbstractSearchRequest instance', () => { expect(searchRequest.req).toBe(req); expect(searchRequest.callWithRequest).toBe(callWithRequest); - expect(searchRequest.indexPattern).toBe(indexPattern); expect(searchRequest.search).toBeDefined(); }); test('should return search value', async () => { const concreteSearchRequest = { - search: jest.fn().mockReturnValue('concreteSearchRequest') + search: jest.fn().mockReturnValue('concreteSearchRequest'), }; const options = {}; searchRequest.getSearchRequestType = jest.fn().mockReturnValue(concreteSearchRequest); @@ -58,20 +55,18 @@ describe('SearchRequest', () => { expect(result).toBe('concreteSearchRequest'); }); - test('should return a MultiSearchRequest if options has body as an array', () => { - const options = { - body: [] - }; + test('should return a MultiSearchRequest for multi searches', () => { + const searches = [{ index: 'index', body: 'body' }, { index: 'index', body: 'body' }]; - const result = searchRequest.getSearchRequestType(options); + const result = searchRequest.getSearchRequestType(searches); expect(result instanceof MultiSearchRequest).toBe(true); }); - test('should return a SingleSearchRequest if options has body', () => { - const options = {}; + test('should return a SingleSearchRequest for single search', () => { + const searches = [{ index: 'index', body: 'body' }]; - const result = searchRequest.getSearchRequestType(options); + const result = searchRequest.getSearchRequestType(searches); expect(result instanceof SingleSearchRequest).toBe(true); }); diff --git a/src/legacy/core_plugins/metrics/server/lib/search_strategies/searh_requests/single_search_request.js b/src/legacy/core_plugins/metrics/server/lib/search_strategies/searh_requests/single_search_request.js index df27b890f0fd0..110deb6a9bc1b 100644 --- a/src/legacy/core_plugins/metrics/server/lib/search_strategies/searh_requests/single_search_request.js +++ b/src/legacy/core_plugins/metrics/server/lib/search_strategies/searh_requests/single_search_request.js @@ -21,12 +21,12 @@ import { AbstractSearchRequest } from './abstract_request'; const SEARCH_METHOD = 'search'; export class SingleSearchRequest extends AbstractSearchRequest { - async search(options) { + async search([{ body, index }]) { const includeFrozen = await this.req.getUiSettingsService().get('search:includeFrozen'); const resp = await this.callWithRequest(this.req, SEARCH_METHOD, { - ...options, - index: this.indexPattern, ignore_throttled: !includeFrozen, + body, + index, }); return [resp]; diff --git a/src/legacy/core_plugins/metrics/server/lib/search_strategies/searh_requests/single_search_request.test.js b/src/legacy/core_plugins/metrics/server/lib/search_strategies/searh_requests/single_search_request.test.js index 97cbaa188cee4..043bd52d87aad 100644 --- a/src/legacy/core_plugins/metrics/server/lib/search_strategies/searh_requests/single_search_request.test.js +++ b/src/legacy/core_plugins/metrics/server/lib/search_strategies/searh_requests/single_search_request.test.js @@ -22,7 +22,6 @@ describe('SingleSearchRequest', () => { let searchRequest; let req; let callWithRequest; - let indexPattern; let getServiceMock; let includeFrozen; @@ -30,31 +29,29 @@ describe('SingleSearchRequest', () => { includeFrozen = false; getServiceMock = jest.fn().mockResolvedValue(includeFrozen); req = { - getUiSettingsService: jest.fn().mockReturnValue({ get: getServiceMock }) + getUiSettingsService: jest.fn().mockReturnValue({ get: getServiceMock }), }; callWithRequest = jest.fn().mockReturnValue({}); - indexPattern = 'indexPattern'; - searchRequest = new SingleSearchRequest(req, callWithRequest, indexPattern); + searchRequest = new SingleSearchRequest(req, callWithRequest); }); test('should init an SingleSearchRequest instance', () => { expect(searchRequest.req).toBe(req); expect(searchRequest.callWithRequest).toBe(callWithRequest); - expect(searchRequest.indexPattern).toBe(indexPattern); expect(searchRequest.search).toBeDefined(); }); test('should get the response from elastic search', async () => { - const options = {}; + const searches = [{ body: 'body', index: 'index' }]; - const responses = await searchRequest.search(options); + const responses = await searchRequest.search(searches); expect(responses).toEqual([{}]); expect(req.getUiSettingsService).toHaveBeenCalled(); expect(getServiceMock).toHaveBeenCalledWith('search:includeFrozen'); expect(callWithRequest).toHaveBeenCalledWith(req, 'search', { - ...options, - index: indexPattern, + body: 'body', + index: 'index', ignore_throttled: !includeFrozen, }); }); diff --git a/src/legacy/core_plugins/metrics/server/lib/search_strategies/strategies/abstract_search_strategy.js b/src/legacy/core_plugins/metrics/server/lib/search_strategies/strategies/abstract_search_strategy.js index 2df4652e8179c..6df0bce65096d 100644 --- a/src/legacy/core_plugins/metrics/server/lib/search_strategies/strategies/abstract_search_strategy.js +++ b/src/legacy/core_plugins/metrics/server/lib/search_strategies/strategies/abstract_search_strategy.js @@ -21,10 +21,10 @@ export class AbstractSearchStrategy { constructor(server, callWithRequestFactory, SearchRequest) { this.getCallWithRequestInstance = req => callWithRequestFactory(server, req); - this.getSearchRequest = (req, indexPattern) => { + this.getSearchRequest = (req) => { const callWithRequest = this.getCallWithRequestInstance(req); - return new SearchRequest(req, callWithRequest, indexPattern); + return new SearchRequest(req, callWithRequest); }; } diff --git a/src/legacy/core_plugins/metrics/server/lib/search_strategies/strategies/abstract_search_strategy.test.js b/src/legacy/core_plugins/metrics/server/lib/search_strategies/strategies/abstract_search_strategy.test.js index c063c5047d069..16a85c6c0e49a 100644 --- a/src/legacy/core_plugins/metrics/server/lib/search_strategies/strategies/abstract_search_strategy.test.js +++ b/src/legacy/core_plugins/metrics/server/lib/search_strategies/strategies/abstract_search_strategy.test.js @@ -19,10 +19,9 @@ import { AbstractSearchStrategy } from './abstract_search_strategy'; class SearchRequest { - constructor(req, callWithRequest, indexPattern) { + constructor(req, callWithRequest) { this.req = req; this.callWithRequest = callWithRequest; - this.indexPattern = indexPattern; } } @@ -38,7 +37,6 @@ describe('AbstractSearchStrategy', () => { server = {}; callWithRequestFactory = jest.fn().mockReturnValue('callWithRequest'); mockedFields = {}; - indexPattern = '*'; req = { pre: { indexPatternsService: { @@ -73,10 +71,9 @@ describe('AbstractSearchStrategy', () => { }); test('should return a search request', () => { - const searchRequest = abstractSearchStrategy.getSearchRequest(req, indexPattern); + const searchRequest = abstractSearchStrategy.getSearchRequest(req); expect(searchRequest instanceof SearchRequest).toBe(true); - expect(searchRequest.indexPattern).toBe(indexPattern); expect(searchRequest.callWithRequest).toBe('callWithRequest'); expect(searchRequest.req).toBe(req); }); diff --git a/src/legacy/core_plugins/metrics/server/lib/search_strategies/strategies/default_search_strategy.js b/src/legacy/core_plugins/metrics/server/lib/search_strategies/strategies/default_search_strategy.js index 15a796b5e511a..5e35f157d92af 100644 --- a/src/legacy/core_plugins/metrics/server/lib/search_strategies/strategies/default_search_strategy.js +++ b/src/legacy/core_plugins/metrics/server/lib/search_strategies/strategies/default_search_strategy.js @@ -25,7 +25,6 @@ const callWithRequestFactory = (server, request) => { return callWithRequest; }; -const batchRequestsSupport = true; export class DefaultSearchStrategy extends AbstractSearchStrategy { name = 'default'; @@ -37,7 +36,7 @@ export class DefaultSearchStrategy extends AbstractSearchStrategy { checkForViability(req) { return { isViable: true, - capabilities: new DefaultSearchCapabilities(req, batchRequestsSupport) + capabilities: new DefaultSearchCapabilities(req) }; } } diff --git a/src/legacy/core_plugins/metrics/server/lib/search_strategies/strategies/default_search_strategy.test.js b/src/legacy/core_plugins/metrics/server/lib/search_strategies/strategies/default_search_strategy.test.js index 9f1750c1b3db2..46fe0ad76c1d1 100644 --- a/src/legacy/core_plugins/metrics/server/lib/search_strategies/strategies/default_search_strategy.test.js +++ b/src/legacy/core_plugins/metrics/server/lib/search_strategies/strategies/default_search_strategy.test.js @@ -62,7 +62,6 @@ describe('DefaultSearchStrategy', () => { expect(value.isViable).toBe(true); expect(value.capabilities).toEqual({ request: req, - batchRequestsSupport: true, fieldsCapabilities: {}, }); }); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/build_processor_function.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/build_processor_function.js index 40620bd02bdc5..5029b829eea5c 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/build_processor_function.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/build_processor_function.js @@ -19,7 +19,7 @@ import sinon from 'sinon'; import { expect } from 'chai'; -import buildProcessorFunction from '../build_processor_function'; +import { buildProcessorFunction } from '../build_processor_function'; describe('buildProcessorFunction(chain, ...args)', () => { const req = {}; diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/get_interval_and_timefield.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/get_interval_and_timefield.js index f5ed0a95de9a5..1fe0af23ef222 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/get_interval_and_timefield.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/get_interval_and_timefield.js @@ -18,7 +18,7 @@ */ import { expect } from 'chai'; -import getIntervalAndTimefield from '../get_interval_and_timefield'; +import { getIntervalAndTimefield } from '../get_interval_and_timefield'; describe('getIntervalAndTimefield(panel, series)', () => { diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/bucket_transform.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/bucket_transform.js index 3981af4e0c361..7de3244274277 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/bucket_transform.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/bucket_transform.js @@ -18,7 +18,7 @@ */ import { expect } from 'chai'; -import bucketTransform from '../../helpers/bucket_transform'; +import { bucketTransform } from '../../helpers/bucket_transform'; describe('bucketTransform', () => { describe('count', () => { diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_agg_value.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_agg_value.js index 8b6d194c92aad..bd5062cc99f75 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_agg_value.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_agg_value.js @@ -18,7 +18,7 @@ */ import { expect } from 'chai'; -import getAggValue from '../../helpers/get_agg_value'; +import { getAggValue } from '../../helpers/get_agg_value'; function testAgg(row, metric, expected) { let name = metric.type; diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_bucket_size.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_bucket_size.js index 987051b29d207..acefbfdc5afb2 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_bucket_size.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_bucket_size.js @@ -18,7 +18,7 @@ */ import { expect } from 'chai'; -import getBucketSize from '../../helpers/get_bucket_size'; +import { getBucketSize } from '../../helpers/get_bucket_size'; describe('getBucketSize', () => { const req = { diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_buckets_path.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_buckets_path.js index 59ea0baa97e3c..a97b1a3512149 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_buckets_path.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_buckets_path.js @@ -18,7 +18,7 @@ */ import { expect } from 'chai'; -import getBucketsPath from '../../helpers/get_buckets_path'; +import { getBucketsPath } from '../../helpers/get_buckets_path'; describe('getBucketsPath', () => { diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_default_decoration.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_default_decoration.js index 210db17f85fb5..fa22e350b4888 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_default_decoration.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_default_decoration.js @@ -18,7 +18,7 @@ */ import { expect } from 'chai'; -import getDefaultDecoration from '../../helpers/get_default_decoration'; +import { getDefaultDecoration } from '../../helpers/get_default_decoration'; describe('getDefaultDecoration', () => { diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_last_metric.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_last_metric.js index 9fc22296ce46f..c047e9f5bd072 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_last_metric.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_last_metric.js @@ -18,7 +18,7 @@ */ import { expect } from 'chai'; -import getLastMetric from '../../helpers/get_last_metric'; +import { getLastMetric } from '../../helpers/get_last_metric'; describe('getLastMetric(series)', () => { it('returns the last metric', () => { diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_sibling_agg_value.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_sibling_agg_value.js index 31c02d0c7a2af..83654e3509976 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_sibling_agg_value.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_sibling_agg_value.js @@ -18,7 +18,7 @@ */ import { expect } from 'chai'; -import getSiblingAggValue from '../../helpers/get_sibling_agg_value'; +import { getSiblingAggValue } from '../../helpers/get_sibling_agg_value'; describe('getSiblingAggValue', () => { const row = { diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_splits.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_splits.js index 12b8480b7cf60..76714168a9639 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_splits.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_splits.js @@ -18,7 +18,7 @@ */ import { expect } from 'chai'; -import getSplits from '../../helpers/get_splits'; +import { getSplits } from '../../helpers/get_splits'; describe('getSplits(resp, panel, series)', () => { diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_timerange.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_timerange.js index 74a928d824b67..db8b91a8b930f 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_timerange.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_timerange.js @@ -18,7 +18,7 @@ */ import { expect } from 'chai'; -import getTimerange from '../../helpers/get_timerange'; +import { getTimerange } from '../../helpers/get_timerange'; import moment from 'moment'; describe('getTimerange(req)', () => { diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/map_bucket.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/map_bucket.js index 405357ced9635..e308b2985a989 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/map_bucket.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/map_bucket.js @@ -17,7 +17,7 @@ * under the License. */ -import mapBucket from '../../helpers/map_bucket'; +import { mapBucket } from '../../helpers/map_bucket'; import { expect } from 'chai'; describe('mapBucket(metric)', () => { diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/parse_settings.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/parse_settings.js index 9d95356a15c41..1d03e2dce4415 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/parse_settings.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/parse_settings.js @@ -18,7 +18,7 @@ */ import { expect } from 'chai'; -import parseSettings from '../../helpers/parse_settings'; +import { parseSettings } from '../../helpers/parse_settings'; describe('parseSettings', () => { it('returns the true for "true"', () => { diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/offset_time.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/offset_time.js index b3d8a476e4a23..6a5742773c32f 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/offset_time.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/offset_time.js @@ -19,7 +19,7 @@ import { expect } from 'chai'; import moment from 'moment'; -import offsetTime from '../offset_time'; +import { offsetTime } from '../offset_time'; describe('offsetTime(req, by)', () => { it('should return a moment object for to and from', () => { diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/annorations/build_request_body.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/annorations/build_request_body.js index 91f55b1eb2200..9ea62e2700517 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/annorations/build_request_body.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/annorations/build_request_body.js @@ -16,8 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import buildProcessorFunction from '../build_processor_function'; -import processors from '../request_processors/annotations'; +import { buildProcessorFunction } from '../build_processor_function'; +import { processors } from '../request_processors/annotations'; /** * Builds annotation request body diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/annorations/get_request_params.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/annorations/get_request_params.js index 4a97fb658ca21..c0b4add2de986 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/annorations/get_request_params.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/annorations/get_request_params.js @@ -21,24 +21,16 @@ import { getEsShardTimeout } from '../helpers/get_es_shard_timeout'; import { getIndexPatternObject } from '../helpers/get_index_pattern'; export async function getAnnotationRequestParams(req, panel, annotation, esQueryConfig, capabilities) { - const bodies = []; const esShardTimeout = await getEsShardTimeout(req); const indexPattern = annotation.index_pattern; const { indexPatternObject, indexPatternString } = await getIndexPatternObject(req, indexPattern); const request = buildAnnotationRequest(req, panel, annotation, esQueryConfig, indexPatternObject, capabilities); - if (capabilities.batchRequestsSupport) { - bodies.push({ - index: indexPatternString, - ignoreUnavailable: true, - }); - } - - if (esShardTimeout > 0) { - request.timeout = `${esShardTimeout}ms`; - } - - bodies.push(request); - - return bodies; + return { + index: indexPatternString, + body: { + ...request, + timeout: esShardTimeout > 0 ? `${esShardTimeout}ms` : undefined, + }, + }; } diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/build_processor_function.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/build_processor_function.js index 7c85af3ac27e4..14da49ce47bb9 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/build_processor_function.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/build_processor_function.js @@ -17,7 +17,7 @@ * under the License. */ -export default function buildProcessorFunction(chain, ...args) { +export function buildProcessorFunction(chain, ...args) { return chain.reduceRight((next, fn) => { return fn(...args)(next); }, doc => doc); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/get_annotations.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/get_annotations.js index 0c15585a597f7..150806e0ef5f9 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/get_annotations.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/get_annotations.js @@ -37,24 +37,23 @@ export async function getAnnotations({ capabilities, series }) { - const panelIndexPattern = panel.index_pattern; - const searchRequest = searchStrategy.getSearchRequest(req, panelIndexPattern); + const searchRequest = searchStrategy.getSearchRequest(req); const annotations = panel.annotations.filter(validAnnotation); const lastSeriesTimestamp = getLastSeriesTimestamp(series); const handleAnnotationResponseBy = handleAnnotationResponse(lastSeriesTimestamp); const bodiesPromises = annotations.map(annotation => getAnnotationRequestParams(req, panel, annotation, esQueryConfig, capabilities)); - const body = (await Promise.all(bodiesPromises)) + const searches = (await Promise.all(bodiesPromises)) .reduce((acc, items) => acc.concat(items), []); - if (!body.length) return { responses: [] }; + if (!searches.length) return { responses: [] }; try { - const responses = await searchRequest.search({ body }); + const data = await searchRequest.search(searches); return annotations .reduce((acc, annotation, index) => { - acc[annotation.id] = handleAnnotationResponseBy(responses[index], annotation); + acc[annotation.id] = handleAnnotationResponseBy(data[index], annotation); return acc; }, {}); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/get_interval_and_timefield.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/get_interval_and_timefield.js index 269f8c339c801..ebcf38f26e840 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/get_interval_and_timefield.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/get_interval_and_timefield.js @@ -17,8 +17,18 @@ * under the License. */ -export default function getIntervalAndTimefield(panel, series = {}) { - const timeField = series.override_index_pattern && series.series_time_field || panel.time_field; +import { get } from 'lodash'; + +const DEFAULT_TIME_FIELD = '@timestamp'; + +export function getIntervalAndTimefield(panel, series = {}, indexPatternObject) { + const getDefaultTimeField = () => get(indexPatternObject, 'timeFieldName', DEFAULT_TIME_FIELD); + + const timeField = (series.override_index_pattern && series.series_time_field || panel.time_field) || getDefaultTimeField(); const interval = series.override_index_pattern && series.series_interval || panel.interval; - return { timeField, interval }; + + return { + timeField, + interval, + }; } diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/get_panel_data.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/get_panel_data.js index 865e97f370288..eefb192e626f7 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/get_panel_data.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/get_panel_data.js @@ -19,7 +19,8 @@ import { getTableData } from './get_table_data'; import { getSeriesData } from './get_series_data'; -export default function getPanelData(req) { + +export function getPanelData(req) { return panel => { if (panel.type === 'table') { return getTableData(req, panel); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/get_series_data.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/get_series_data.js index dfbe7d337ec5f..4802efc8186de 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/get_series_data.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/get_series_data.js @@ -17,32 +17,30 @@ * under the License. */ import { getSeriesRequestParams } from './series/get_request_params'; -import handleResponseBody from './series/handle_response_body'; -import handleErrorResponse from './handle_error_response'; +import { handleResponseBody } from './series/handle_response_body'; +import { handleErrorResponse } from './handle_error_response'; import { getAnnotations } from './get_annotations'; import { SearchStrategiesRegister } from '../search_strategies/search_strategies_register'; import { getEsQueryConfig } from './helpers/get_es_query_uisettings'; import { getActiveSeries } from './helpers/get_active_series'; export async function getSeriesData(req, panel) { - const panelIndexPattern = panel.index_pattern; - const { searchStrategy, capabilities } = await SearchStrategiesRegister.getViableStrategy(req, panelIndexPattern); - const searchRequest = searchStrategy.getSearchRequest(req, panelIndexPattern); + const { searchStrategy, capabilities } = await SearchStrategiesRegister.getViableStrategyForPanel(req, panel); + const searchRequest = searchStrategy.getSearchRequest(req); const esQueryConfig = await getEsQueryConfig(req); - - const bodiesPromises = getActiveSeries(panel) - .map(series => getSeriesRequestParams(req, panel, series, esQueryConfig, capabilities)); - - const body = (await Promise.all(bodiesPromises)) - .reduce((acc, items) => acc.concat(items), []); - const meta = { type: panel.type, uiRestrictions: capabilities.uiRestrictions, }; try { - const data = await searchRequest.search({ body }); + const bodiesPromises = getActiveSeries(panel) + .map(series => getSeriesRequestParams(req, panel, series, esQueryConfig, capabilities)); + + const searches = (await Promise.all(bodiesPromises)) + .reduce((acc, items) => acc.concat(items), []); + + const data = await searchRequest.search(searches); const series = data.map(handleResponseBody(panel)); let annotations = null; @@ -50,11 +48,11 @@ export async function getSeriesData(req, panel) { if (panel.annotations && panel.annotations.length) { annotations = await getAnnotations({ req, + panel, + series, esQueryConfig, searchStrategy, - panel, capabilities, - series }); } @@ -68,7 +66,7 @@ export async function getSeriesData(req, panel) { }; } catch (err) { - if (err.body) { + if (err.body || err.name === 'KQLSyntaxError') { err.response = err.body; return { diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/get_table_data.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/get_table_data.js index 1de3913435166..9a43e7aee8073 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/get_table_data.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/get_table_data.js @@ -16,10 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -import buildRequestBody from './table/build_request_body'; -import handleErrorResponse from './handle_error_response'; +import { buildRequestBody } from './table/build_request_body'; +import { handleErrorResponse } from './handle_error_response'; import { get } from 'lodash'; -import processBucket from './table/process_bucket'; +import { processBucket } from './table/process_bucket'; import { SearchStrategiesRegister } from '../search_strategies/search_strategies_register'; import { getEsQueryConfig } from './helpers/get_es_query_uisettings'; import { getIndexPatternObject } from './helpers/get_index_pattern'; @@ -27,10 +27,9 @@ import { getIndexPatternObject } from './helpers/get_index_pattern'; export async function getTableData(req, panel) { const panelIndexPattern = panel.index_pattern; const { searchStrategy, capabilities } = await SearchStrategiesRegister.getViableStrategy(req, panelIndexPattern); - const searchRequest = searchStrategy.getSearchRequest(req, panelIndexPattern); + const searchRequest = searchStrategy.getSearchRequest(req); const esQueryConfig = await getEsQueryConfig(req); const { indexPatternObject } = await getIndexPatternObject(req, panelIndexPattern); - const body = buildRequestBody(req, panel, esQueryConfig, indexPatternObject, capabilities); const meta = { type: panel.type, @@ -38,7 +37,11 @@ export async function getTableData(req, panel) { }; try { - const [resp] = await searchRequest.search({ body }); + const body = buildRequestBody(req, panel, esQueryConfig, indexPatternObject, capabilities); + const [resp] = await searchRequest.search([{ + body, + index: panelIndexPattern, + }]); const buckets = get(resp, 'aggregations.pivot.buckets', []); return { @@ -46,7 +49,7 @@ export async function getTableData(req, panel) { series: buckets.map(processBucket(panel)), }; } catch (err) { - if (err.body) { + if (err.body || err.name === 'KQLSyntaxError') { err.response = err.body; return { diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/handle_error_response.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/handle_error_response.js index 0910c57820793..a99c0671b2135 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/handle_error_response.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/handle_error_response.js @@ -17,7 +17,7 @@ * under the License. */ -export default panel => error => { +export const handleErrorResponse = panel => error => { if (error.isBoom && error.status === 401) throw error; const result = {}; let errorResponse; @@ -26,12 +26,18 @@ export default panel => error => { } catch (e) { errorResponse = error.response; } - if (!errorResponse) { + if (!errorResponse && !(error.name === 'KQLSyntaxError')) { errorResponse = { message: error.message, stack: error.stack }; } + if (error.name === 'KQLSyntaxError') { + errorResponse = { + message: error.shortMessage, + stack: error.stack + }; + } result[panel.id] = { id: panel.id, statusCode: error.statusCode, diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/bucket_transform.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/bucket_transform.js index 14981bed44ed4..400536e3a7943 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/bucket_transform.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/bucket_transform.js @@ -17,8 +17,8 @@ * under the License. */ -import parseSettings from './parse_settings'; -import getBucketsPath from './get_buckets_path'; +import { parseSettings } from './parse_settings'; +import { getBucketsPath } from './get_buckets_path'; import { parseInterval } from './parse_interval'; import { set, isEmpty } from 'lodash'; import { i18n } from '@kbn/i18n'; @@ -63,7 +63,7 @@ function extendStatsBucket(bucket, metrics) { return body; } -export default { +export const bucketTransform = { count: () => { return { bucket_script: { diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/calculate_auto.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/calculate_auto.js index e851ef493b42c..138b535980c4b 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/calculate_auto.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/calculate_auto.js @@ -71,7 +71,7 @@ function find(rules, check, last) { }; } -export default { +export const calculateAuto = { near: find(revRoundingRules, function near(bound, interval, target) { if (bound > target) return interval; }, true), diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_agg_value.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_agg_value.js index 5714efb904951..b8c023a2ed155 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_agg_value.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_agg_value.js @@ -30,7 +30,7 @@ const aggFns = { avg: values => sum(values) / values.length, }; -export default (row, metric) => { +export const getAggValue = (row, metric) => { // Extended Stats if (includes(EXTENDED_STATS_TYPES, metric.type)) { const isStdDeviation = /^std_deviation/.test(metric.type); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_bucket_size.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_bucket_size.js index 687cc02853038..05fdbbacc8af4 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_bucket_size.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_bucket_size.js @@ -17,9 +17,9 @@ * under the License. */ -import calculateAuto from './calculate_auto'; +import { calculateAuto } from './calculate_auto'; import moment from 'moment'; -import unitToSeconds from './unit_to_seconds'; +import { getUnitValue } from './unit_to_seconds'; import { INTERVAL_STRING_RE, GTE_INTERVAL_RE, @@ -29,7 +29,7 @@ const calculateBucketData = (timeInterval, capabilities) => { const intervalString = capabilities ? capabilities.getValidTimeInterval(timeInterval) : timeInterval; const intervalStringMatch = intervalString.match(INTERVAL_STRING_RE); - let bucketSize = Number(intervalStringMatch[1]) * unitToSeconds(intervalStringMatch[2]); + let bucketSize = Number(intervalStringMatch[1]) * getUnitValue(intervalStringMatch[2]); // don't go too small if (bucketSize < 1) { @@ -50,7 +50,7 @@ const getTimeRangeBucketSize = ({ min, max }) => { return calculateAuto.near(100, duration).asSeconds(); }; -export default (req, interval, capabilities) => { +export const getBucketSize = (req, interval, capabilities) => { const bucketSize = getTimeRangeBucketSize(req.payload.timerange); let intervalString = `${bucketSize}s`; diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_buckets_path.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_buckets_path.js index ad7f88ec4fb82..9bbd80519dc8f 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_buckets_path.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_buckets_path.js @@ -23,7 +23,7 @@ import { METRIC_TYPES } from '../../../../common/metric_types'; const percentileTest = /\[[0-9\.]+\]$/; -export default (id, metrics) => { +export const getBucketsPath = (id, metrics) => { const metric = metrics.find(m => startsWith(id, m.id)); let bucketsPath = String(id); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_default_decoration.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_default_decoration.js index 9e2c495df0cb1..0b2ad3bb86b1d 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_default_decoration.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_default_decoration.js @@ -17,7 +17,7 @@ * under the License. */ -export default series => { +export const getDefaultDecoration = series => { const pointSize = series.point_size != null ? Number(series.point_size) : Number(series.line_width); const showPoints = series.chart_type === 'line' && pointSize !== 0; let stack; diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_index_pattern.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_index_pattern.js index 2166620de73e2..f1809cace989b 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_index_pattern.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_index_pattern.js @@ -29,7 +29,7 @@ export async function getIndexPatternObject(req, indexPatternString) { const savedObjectClient = req.getSavedObjectsClient(); const indexPatternObjects = await savedObjectClient.find({ type: 'index-pattern', - fields: ['title', 'fields'], + fields: ['title', 'fields', 'timeFieldName'], search: indexPatternString ? `"${indexPatternString}"` : null, search_fields: ['title'], }); @@ -38,9 +38,10 @@ export async function getIndexPatternObject(req, indexPatternString) { const indexPatterns = indexPatternObjects.saved_objects .filter(obj => obj.attributes.title === indexPatternString || (defaultIndex && obj.id === defaultIndex)) .map(indexPattern => { - const { title, fields } = indexPattern.attributes; + const { title, fields, timeFieldName } = indexPattern.attributes; return { title, + timeFieldName, fields: JSON.parse(fields), }; }); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_last_metric.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_last_metric.js index 9e60f446e0675..a683c0f65f6a8 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_last_metric.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_last_metric.js @@ -18,8 +18,7 @@ */ import _ from 'lodash'; -export default function getLastMetric(series) { - return _.last(series.metrics.filter(s => s.type !== 'series_agg')); +export function getLastMetric(series) { + return _.last(series.metrics.filter(s => s.type !== 'series_agg')); } - diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_sibling_agg_value.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_sibling_agg_value.js index cee9b03daff73..4308c7e78485f 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_sibling_agg_value.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_sibling_agg_value.js @@ -18,7 +18,8 @@ */ import _ from 'lodash'; -export default (row, metric) => { + +export const getSiblingAggValue = (row, metric) => { let key = metric.type.replace(/_bucket$/, ''); if (key === 'std_deviation' && _.includes(['upper', 'lower'], metric.mode)) { key = `std_deviation_bounds.${metric.mode}`; diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_split_colors.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_split_colors.js index d473a67647414..c0bb0f927c485 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_split_colors.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_split_colors.js @@ -18,7 +18,8 @@ */ import Color from 'color'; -export default function getSplitColors(inputColor, size = 10, style = 'gradient') { + +export function getSplitColors(inputColor, size = 10, style = 'gradient') { const color = new Color(inputColor); const colors = []; let workingColor = Color.hsl(color.hsl().object()); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_splits.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_splits.js index 75d1b4d565fe5..ac307074a9bdd 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_splits.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_splits.js @@ -18,17 +18,17 @@ */ import Color from 'color'; -import calculateLabel from '../../../../common/calculate_label'; +import { calculateLabel } from '../../../../common/calculate_label'; import _ from 'lodash'; -import getLastMetric from './get_last_metric'; -import getSplitColors from './get_split_colors'; +import { getLastMetric } from './get_last_metric'; +import { getSplitColors } from './get_split_colors'; import { formatKey } from './format_key'; const getTimeSeries = (resp, series) => _.get(resp, `aggregations.timeseries`) || _.get(resp, `aggregations.${series.id}.timeseries`); -export default function getSplits(resp, panel, series, meta) { +export function getSplits(resp, panel, series, meta) { if (!meta) { meta = _.get(resp, `aggregations.${series.id}.meta`); } @@ -55,7 +55,7 @@ export default function getSplits(resp, panel, series, meta) { bucket.id = `${series.id}:${filter.id}`; bucket.key = filter.id; bucket.color = filter.color; - bucket.label = filter.label || filter.filter || '*'; + bucket.label = filter.label || filter.filter.query || '*'; bucket.meta = meta; return bucket; }); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_timerange.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_timerange.js index d83ad16f53d36..2c6a41ab1b968 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_timerange.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_timerange.js @@ -18,7 +18,8 @@ */ import moment from 'moment'; -export default function getTimerange(req) { + +export function getTimerange(req) { const from = moment.utc(req.payload.timerange.min); const to = moment.utc(req.payload.timerange.max); return { from, to }; diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/index.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/index.js index 01df1a6f54acd..db6365f88d0ff 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/index.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/index.js @@ -17,24 +17,23 @@ * under the License. */ -import bucketTransform from './bucket_transform'; -import getAggValue from './get_agg_value'; -import getBucketSize from './get_bucket_size'; -import getBucketPath from './get_buckets_path'; -import getDefaultDecoration from './get_default_decoration'; -import getLastMetric from './get_last_metric'; -import getSiblingAggValue from './get_sibling_agg_value'; -import getSplits from './get_splits'; -import getTimerange from './get_timerange'; -import mapBucket from './map_bucket'; -import parseSettings from './parse_settings'; -import unitToSeconds from './unit_to_seconds'; +import { bucketTransform } from './bucket_transform'; +import { getAggValue } from './get_agg_value'; +import { getBucketSize } from './get_bucket_size'; +import { getBucketsPath } from './get_buckets_path'; +import { getDefaultDecoration } from './get_default_decoration'; +import { getLastMetric } from './get_last_metric'; +import { getSiblingAggValue } from './get_sibling_agg_value'; +import { getSplits } from './get_splits'; +import { getTimerange } from './get_timerange'; +import { mapBucket } from './map_bucket'; +import { parseSettings } from './parse_settings'; -export default { +export const helpers = { bucketTransform, getAggValue, getBucketSize, - getBucketPath, + getBucketsPath, getDefaultDecoration, getLastMetric, getSiblingAggValue, @@ -42,5 +41,4 @@ export default { getTimerange, mapBucket, parseSettings, - unitToSeconds, }; diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/map_bucket.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/map_bucket.js index 186c8ee38f5d2..5628355a63584 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/map_bucket.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/map_bucket.js @@ -17,7 +17,8 @@ * under the License. */ -import getAggValue from './get_agg_value'; -export default function mapBucket(metric) { +import { getAggValue } from './get_agg_value'; + +export function mapBucket(metric) { return bucket => [ bucket.key, getAggValue(bucket, metric)]; } diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/parse_settings.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/parse_settings.js index 435ce7bd79634..479e18de1296a 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/parse_settings.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/parse_settings.js @@ -38,7 +38,7 @@ function castBasedOnKey(key, val) { } return val; } -export default (settingsStr) => { +export const parseSettings = (settingsStr) => { return settingsStr.split(/\s/).reduce((acc, value) => { const [key, val] = value.split(/=/); acc[key] = castBasedOnKey(key, val); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/unit_to_seconds.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/unit_to_seconds.js index 4494883c44edc..10f1fa24463aa 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/unit_to_seconds.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/unit_to_seconds.js @@ -17,6 +17,7 @@ * under the License. */ import { INTERVAL_STRING_RE } from '../../../../common/interval_regexp'; +import { sortBy, isNumber } from 'lodash'; const units = { ms: 0.001, @@ -29,6 +30,8 @@ const units = { y: 86400 * 7 * 4 * 12, // Leap year? }; +const sortedUnits = sortBy(Object.keys(units), key => units[key]); + export const parseInterval = (intervalString) => { let value; let unit; @@ -36,21 +39,35 @@ export const parseInterval = (intervalString) => { if (intervalString) { const matches = intervalString.match(INTERVAL_STRING_RE); - value = Number(matches[1]); - unit = matches[2]; + if (matches) { + value = Number(matches[1]); + unit = matches[2]; + } } return { value, unit }; }; -export const convertIntervalToUnit = (intervalString, unit) => { +export const convertIntervalToUnit = (intervalString, newUnit) => { const parsedInterval = parseInterval(intervalString); - const value = Number((parsedInterval.value * units[parsedInterval.unit] / units[unit]).toFixed(2)); + let value; + let unit; + + if (parsedInterval.value && units[newUnit]) { + value = Number((parsedInterval.value * units[parsedInterval.unit] / units[newUnit]).toFixed(2)); + unit = newUnit; + } return { value, unit }; }; -export default (unit) => { - return units[unit]; -}; +export const getSuitableUnit = intervalInSeconds => sortedUnits.find((key, index, array) => { + const nextUnit = array[index + 1]; + const isValidInput = isNumber(intervalInSeconds) && intervalInSeconds > 0; + const isLastItem = index + 1 === array.length; + + return isValidInput && (intervalInSeconds >= units[key] && intervalInSeconds < units[nextUnit] || isLastItem); +}); + +export const getUnitValue = unit => units[unit]; diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/unit_to_seconds.test.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/unit_to_seconds.test.js new file mode 100644 index 0000000000000..e09241b2c1b28 --- /dev/null +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/unit_to_seconds.test.js @@ -0,0 +1,160 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getUnitValue, parseInterval, convertIntervalToUnit, getSuitableUnit } from './unit_to_seconds'; + +describe('unit_to_seconds', () => { + describe('parseInterval()', () => { + test('should parse "1m" interval (positive)', () => + expect(parseInterval('1m')).toEqual({ + value: 1, + unit: 'm', + })); + + test('should parse "134d" interval (positive)', () => + expect(parseInterval('134d')).toEqual({ + value: 134, + unit: 'd', + })); + + test('should parse "0.5d" interval (positive)', () => + expect(parseInterval('0.5d')).toEqual({ + value: 0.5, + unit: 'd', + })); + + test('should parse "30M" interval (positive)', () => + expect(parseInterval('30M')).toEqual({ + value: 30, + unit: 'M', + })); + + test('should not parse "gm" interval (negative)', () => + expect(parseInterval('gm')).toEqual({ + value: undefined, + unit: undefined, + })); + + test('should not parse "-1d" interval (negative)', () => + expect(parseInterval('-1d')).toEqual({ + value: undefined, + unit: undefined, + })); + + test('should not parse "M" interval (negative)', () => + expect(parseInterval('M')).toEqual({ + value: undefined, + unit: undefined, + })); + }); + + describe('convertIntervalToUnit()', () => { + test('should convert "30m" interval to "h" unit (positive)', () => + expect(convertIntervalToUnit('30m', 'h')).toEqual({ + value: 0.5, + unit: 'h', + })); + + test('should convert "0.5h" interval to "m" unit (positive)', () => + expect(convertIntervalToUnit('0.5h', 'm')).toEqual({ + value: 30, + unit: 'm', + })); + + test('should convert "1h" interval to "m" unit (positive)', () => + expect(convertIntervalToUnit('1h', 'm')).toEqual({ + value: 60, + unit: 'm', + })); + + test('should convert "1h" interval to "ms" unit (positive)', () => + expect(convertIntervalToUnit('1h', 'ms')).toEqual({ + value: 3600000, + unit: 'ms', + })); + + + test('should not convert "30m" interval to "0" unit (positive)', () => + expect(convertIntervalToUnit('30m', 'o')).toEqual({ + value: undefined, + unit: undefined, + })); + + test('should not convert "m" interval to "s" unit (positive)', () => + expect(convertIntervalToUnit('m', 's')).toEqual({ + value: undefined, + unit: undefined, + })); + }); + + describe('getSuitableUnit()', () => { + test('should return "d" unit for oneDayInSeconds (positive)', () => { + const oneDayInSeconds = getUnitValue('d') * 1; + + expect(getSuitableUnit(oneDayInSeconds)).toBe('d'); + }); + + test('should return "d" unit for twoDaysInSeconds (positive)', () => { + const twoDaysInSeconds = getUnitValue('d') * 2; + + expect(getSuitableUnit(twoDaysInSeconds)).toBe('d'); + }); + + test('should return "w" unit for threeWeeksInSeconds (positive)', () => { + const threeWeeksInSeconds = getUnitValue('w') * 3; + + expect(getSuitableUnit(threeWeeksInSeconds)).toBe('w'); + }); + + test('should return "y" unit for aroundOneYearInSeconds (positive)', () => { + const aroundOneYearInSeconds = getUnitValue('d') * 370; + + expect(getSuitableUnit(aroundOneYearInSeconds)).toBe('y'); + }); + + test('should return "y" unit for twoYearsInSeconds (positive)', () => { + const twoYearsInSeconds = getUnitValue('y') * 2; + + expect(getSuitableUnit(twoYearsInSeconds)).toBe('y'); + }); + + + test('should return "undefined" unit for negativeNumber (negative)', () => { + const negativeNumber = -12; + + expect(getSuitableUnit(negativeNumber)).toBeUndefined(); + }); + + test('should return "undefined" unit for string value (negative)', () => { + const stringValue = 'string'; + + expect(getSuitableUnit(stringValue)).toBeUndefined(); + }); + + test('should return "undefined" unit for number string value (negative)', () => { + const stringValue = '-12'; + + expect(getSuitableUnit(stringValue)).toBeUndefined(); + }); + + test('should return "undefined" in case of no input value(negative)', () => + expect(getSuitableUnit()).toBeUndefined()); + + }); +}); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/offset_time.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/offset_time.js index 474fc8874c634..4acaaead3b8f8 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/offset_time.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/offset_time.js @@ -17,8 +17,9 @@ * under the License. */ -import getTimerange from './helpers/get_timerange'; -export default function offsetTime(req, by) { +import { getTimerange } from './helpers/get_timerange'; + +export function offsetTime(req, by) { const { from, to } = getTimerange(req); if (!/^[+-]?([\d]+)([shmdwMy]|ms)$/.test(by)) return { from, to }; const matches = by.match(/^([+-]?[\d]+)([shmdwMy]|ms)$/); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/annotations/date_histogram.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/annotations/date_histogram.js index 05657842a5e2a..1c7242b23371b 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/annotations/date_histogram.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/annotations/date_histogram.js @@ -18,9 +18,10 @@ */ import _ from 'lodash'; -import getBucketSize from '../../helpers/get_bucket_size'; -import getTimerange from '../../helpers/get_timerange'; -export default function dateHistogram(req, panel, annotation, esQueryConfig, indexPatternObject, capabilities) { +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { getTimerange } from '../../helpers/get_timerange'; + +export function dateHistogram(req, panel, annotation, esQueryConfig, indexPatternObject, capabilities) { return next => doc => { const timeField = annotation.time_field; const { bucketSize, intervalString } = getBucketSize(req, 'auto', capabilities); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/annotations/index.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/annotations/index.js index 0077dfcf616ba..76efcd75e3be7 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/annotations/index.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/annotations/index.js @@ -17,10 +17,11 @@ * under the License. */ -import query from './query'; -import dateHistogram from './date_histogram'; -import topHits from './top_hits'; -export default [ +import { query } from './query'; +import { dateHistogram } from './date_histogram'; +import { topHits } from './top_hits'; + +export const processors = [ query, dateHistogram, topHits diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/annotations/query.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/annotations/query.js index 9247314a0739f..9c4e2ce72421d 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/annotations/query.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/annotations/query.js @@ -17,11 +17,11 @@ * under the License. */ -import getBucketSize from '../../helpers/get_bucket_size'; -import getTimerange from '../../helpers/get_timerange'; +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { getTimerange } from '../../helpers/get_timerange'; import { buildEsQuery } from '@kbn/es-query'; -export default function query(req, panel, annotation, esQueryConfig, indexPattern, capabilities) { +export function query(req, panel, annotation, esQueryConfig, indexPattern, capabilities) { return next => doc => { const timeField = annotation.time_field; const { bucketSize } = getBucketSize(req, 'auto', capabilities); @@ -43,21 +43,11 @@ export default function query(req, panel, annotation, esQueryConfig, indexPatter doc.query.bool.must.push(timerange); if (annotation.query_string) { - doc.query.bool.must.push({ - query_string: { - query: annotation.query_string, - analyze_wildcard: true, - }, - }); + doc.query.bool.must.push(buildEsQuery(indexPattern, [annotation.query_string], [], esQueryConfig)); } if (!annotation.ignore_panel_filters && panel.filter) { - doc.query.bool.must.push({ - query_string: { - query: panel.filter, - analyze_wildcard: true, - }, - }); + doc.query.bool.must.push(buildEsQuery(indexPattern, [panel.filter], [], esQueryConfig)); } if (annotation.fields) { diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/annotations/top_hits.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/annotations/top_hits.js index 4d394f270bd90..3d53e86b091b4 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/annotations/top_hits.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/annotations/top_hits.js @@ -18,7 +18,8 @@ */ import _ from 'lodash'; -export default function topHits(req, panel, annotation) { + +export function topHits(req, panel, annotation) { return next => doc => { const fields = annotation.fields && annotation.fields.split(/[,\s]+/) || []; const timeField = annotation.time_field; diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/date_histogram.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/date_histogram.js index 34bcf090fecf2..7266c96db31cb 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/date_histogram.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/date_histogram.js @@ -17,7 +17,7 @@ * under the License. */ -import dateHistogram from '../date_histogram'; +import { dateHistogram } from '../date_histogram'; import { expect } from 'chai'; import sinon from 'sinon'; import { DefaultSearchCapabilities } from '../../../../search_strategies/default_search_capabilities'; @@ -52,7 +52,7 @@ describe('dateHistogram(req, panel, series)', () => { queryStringOptions: {}, }; indexPatternObject = {}; - capabilities = new DefaultSearchCapabilities(req, true); + capabilities = new DefaultSearchCapabilities(req); }); it('calls next when finished', () => { diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/filter_ratios.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/filter_ratios.js index 708ff3544f9ba..3040dea353139 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/filter_ratios.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/filter_ratios.js @@ -17,7 +17,7 @@ * under the License. */ -import ratios from '../filter_ratios'; +import { ratios } from '../filter_ratios'; import { expect } from 'chai'; import sinon from 'sinon'; diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/metric_buckets.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/metric_buckets.js index dfda6a14011a0..d24e08658791e 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/metric_buckets.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/metric_buckets.js @@ -17,7 +17,7 @@ * under the License. */ -import metricBuckets from '../metric_buckets'; +import { metricBuckets } from '../metric_buckets'; import { expect } from 'chai'; import sinon from 'sinon'; diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/query.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/query.js index aee274633915c..efe735355645c 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/query.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/query.js @@ -17,7 +17,7 @@ * under the License. */ -import query from '../query'; +import { query } from '../query'; import { expect } from 'chai'; import sinon from 'sinon'; @@ -26,9 +26,10 @@ describe('query(req, panel, series)', () => { let panel; let series; let req; + const config = { allowLeadingWildcards: true, - queryStringOptions: {}, + queryStringOptions: { analyze_wildcard: true } }; beforeEach(() => { req = { @@ -45,6 +46,7 @@ describe('query(req, panel, series)', () => { interval: '10s' }; series = { id: 'test' }; + }); it('calls next when finished', () => { @@ -157,7 +159,7 @@ describe('query(req, panel, series)', () => { }); it('returns doc with series filter', () => { - series.filter = 'host:web-server'; + series.filter = { query: 'host:web-server', language: 'lucene' }; const next = doc => doc; const doc = query(req, panel, series, config)(next)({}); expect(doc).to.eql({ @@ -176,11 +178,20 @@ describe('query(req, panel, series)', () => { } }, { - query_string: { - query: series.filter, - analyze_wildcard: true + bool: { + filter: [], + must: [ + { + query_string: { + analyze_wildcard: true, + query: series.filter.query, + } + }, + ], + must_not: [], + should: [], } - }, + } ], must_not: [], should: [], @@ -202,7 +213,7 @@ describe('query(req, panel, series)', () => { } } ]; - panel.filter = 'host:web-server'; + panel.filter = { query: 'host:web-server', language: 'lucene' }; const next = doc => doc; const doc = query(req, panel, series, config)(next)({}); expect(doc).to.eql({ @@ -232,9 +243,18 @@ describe('query(req, panel, series)', () => { } }, { - query_string: { - query: panel.filter, - analyze_wildcard: true + bool: { + filter: [], + must: [ + { + query_string: { + query: panel.filter.query, + analyze_wildcard: true + } + } + ], + must_not: [], + should: [], } } ], @@ -259,7 +279,7 @@ describe('query(req, panel, series)', () => { } } ]; - panel.filter = 'host:web-server'; + panel.filter = { query: 'host:web-server', language: 'lucene' }; panel.ignore_global_filter = true; const next = doc => doc; const doc = query(req, panel, series, config)(next)({}); @@ -279,11 +299,20 @@ describe('query(req, panel, series)', () => { } }, { - query_string: { - query: panel.filter, - analyze_wildcard: true + bool: { + filter: [], + must: [ + { + query_string: { + query: panel.filter.query, + analyze_wildcard: true + } + } + ], + must_not: [], + should: [], } - }, + } ], must_not: [], should: [], diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/sibling_buckets.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/sibling_buckets.js index e3ac591a5a10b..4ecef19129632 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/sibling_buckets.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/sibling_buckets.js @@ -17,7 +17,7 @@ * under the License. */ -import siblingBuckets from '../sibling_buckets'; +import { siblingBuckets } from '../sibling_buckets'; import { expect } from 'chai'; import sinon from 'sinon'; diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/split_by_everything.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/split_by_everything.js index fcba02c8534c0..6fdfa550b39b5 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/split_by_everything.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/split_by_everything.js @@ -17,7 +17,7 @@ * under the License. */ -import splitByEverything from '../split_by_everything'; +import { splitByEverything } from '../split_by_everything'; import { expect } from 'chai'; import sinon from 'sinon'; diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/split_by_filter.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/split_by_filter.js index 62d13bbe49def..12c5956029f00 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/split_by_filter.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/split_by_filter.js @@ -17,7 +17,7 @@ * under the License. */ -import splitByFilter from '../split_by_filter'; +import { splitByFilter } from '../split_by_filter'; import { expect } from 'chai'; import sinon from 'sinon'; @@ -28,7 +28,7 @@ describe('splitByFilter(req, panel, series)', () => { let req; beforeEach(() => { panel = {}; - series = { id: 'test', split_mode: 'filter', filter: 'host:example-01' }; + series = { id: 'test', split_mode: 'filter', filter: { query: 'host:example-01', language: 'lucene' } }; req = { payload: { timerange: { @@ -52,9 +52,17 @@ describe('splitByFilter(req, panel, series)', () => { aggs: { test: { filter: { - query_string: { - query: 'host:example-01', - analyze_wildcard: true + bool: { + filter: [], + must: [ + { + query_string: { + query: 'host:example-01' + } + } + ], + must_not: [], + should: [] } } } diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/split_by_filters.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/split_by_filters.js index d159fac6fdaab..17507100cb2b2 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/split_by_filters.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/split_by_filters.js @@ -17,7 +17,7 @@ * under the License. */ -import splitByFilters from '../split_by_filters'; +import { splitByFilters } from '../split_by_filters'; import { expect } from 'chai'; import sinon from 'sinon'; @@ -37,13 +37,13 @@ describe('splitByFilters(req, panel, series)', () => { { id: 'filter-1', color: '#F00', - filter: 'status_code:[* TO 200]', + filter: { query: 'status_code:[* TO 200]', language: 'lucene' }, label: '200s' }, { id: 'filter-2', color: '#0F0', - filter: 'status_code:[300 TO *]', + filter: { query: 'status_code:[300 TO *]', language: 'lucene' }, label: '300s' } @@ -75,15 +75,31 @@ describe('splitByFilters(req, panel, series)', () => { filters: { filters: { 'filter-1': { - query_string: { - query: 'status_code:[* TO 200]', - analyze_wildcard: true + bool: { + filter: [], + must: [ + { + query_string: { + query: 'status_code:[* TO 200]', + } + } + ], + must_not: [], + should: [], } }, 'filter-2': { - query_string: { - query: 'status_code:[300 TO *]', - analyze_wildcard: true + bool: { + filter: [], + must: [ + { + query_string: { + query: 'status_code:[300 TO *]', + } + } + ], + must_not: [], + should: [], } } } diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/split_by_terms.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/split_by_terms.js index 77c4ee1dccfbd..b2787ba539f09 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/split_by_terms.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/split_by_terms.js @@ -17,7 +17,7 @@ * under the License. */ -import splitByTerms from '../split_by_terms'; +import { splitByTerms } from '../split_by_terms'; import { expect } from 'chai'; import sinon from 'sinon'; diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/date_histogram.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/date_histogram.js index c04684aab7e4f..d3dcd93d563bf 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/date_histogram.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/date_histogram.js @@ -17,13 +17,14 @@ * under the License. */ -import getBucketSize from '../../helpers/get_bucket_size'; -import offsetTime from '../../offset_time'; -import getIntervalAndTimefield from '../../get_interval_and_timefield'; +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { offsetTime } from '../../offset_time'; +import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; import { set } from 'lodash'; -export default function dateHistogram(req, panel, series, esQueryConfig, indexPatternObject, capabilities) { + +export function dateHistogram(req, panel, series, esQueryConfig, indexPatternObject, capabilities) { return next => doc => { - const { timeField, interval } = getIntervalAndTimefield(panel, series); + const { timeField, interval } = getIntervalAndTimefield(panel, series, indexPatternObject); const { bucketSize, intervalString } = getBucketSize(req, interval, capabilities); const { from, to } = offsetTime(req, series.offset_time); const timezone = capabilities.searchTimezone; diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/filter_ratios.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/filter_ratios.js index 8b8ccd24ba380..94cb5c4456025 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/filter_ratios.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/filter_ratios.js @@ -19,9 +19,10 @@ /* eslint max-len:0 */ const filter = metric => metric.type === 'filter_ratio'; -import bucketTransform from '../../helpers/bucket_transform'; +import { bucketTransform } from '../../helpers/bucket_transform'; import _ from 'lodash'; -export default function ratios(req, panel, series) { + +export function ratios(req, panel, series) { return next => doc => { if (series.metrics.some(filter)) { series.metrics.filter(filter).forEach(metric => { diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/index.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/index.js index 02e084bc779a1..ec9a97fe9f737 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/index.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/index.js @@ -17,18 +17,18 @@ * under the License. */ -import query from './query'; -import splitByEverything from './split_by_everything'; -import splitByFilter from './split_by_filter'; -import splitByFilters from './split_by_filters'; -import splitByTerms from './split_by_terms'; -import dateHistogram from './date_histogram'; -import metricBuckets from './metric_buckets'; -import siblingBuckets from './sibling_buckets'; -import filterRatios from './filter_ratios'; -import normalizeQuery from './normalize_query'; +import { query } from './query'; +import { splitByEverything } from './split_by_everything'; +import { splitByFilter } from './split_by_filter'; +import { splitByFilters } from './split_by_filters'; +import { splitByTerms } from './split_by_terms'; +import { dateHistogram } from './date_histogram'; +import { metricBuckets } from './metric_buckets'; +import { siblingBuckets } from './sibling_buckets'; +import { ratios as filterRatios } from './filter_ratios'; +import { normalizeQuery } from './normalize_query'; -export default [ +export const processors = [ query, splitByTerms, splitByFilter, diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/metric_buckets.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/metric_buckets.js index 19aad8f37d825..9c569c3c4b4ad 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/metric_buckets.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/metric_buckets.js @@ -18,14 +18,13 @@ */ import _ from 'lodash'; -import getBucketSize from '../../helpers/get_bucket_size'; -import bucketTransform from '../../helpers/bucket_transform'; -import getIntervalAndTimefield from '../../get_interval_and_timefield'; -export default function metricBuckets(req, panel, series, esQueryConfig, indexPatternObject, capabilities) { +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { bucketTransform } from '../../helpers/bucket_transform'; +import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; + +export function metricBuckets(req, panel, series, esQueryConfig, indexPatternObject, capabilities) { return next => doc => { - const { - interval - } = getIntervalAndTimefield(panel, series); + const { interval } = getIntervalAndTimefield(panel, series, indexPatternObject); const { intervalString } = getBucketSize(req, interval, capabilities); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/normalize_query.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/normalize_query.js index df2a829b90024..a9c603ed76184 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/normalize_query.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/normalize_query.js @@ -40,7 +40,7 @@ function removeEmptyTopLevelAggregation(doc, series) { /* Last query handler in the chain. You can use this handler * as the last place where you can modify the "doc" (request body) object before sending it to ES. */ -export default function normalizeQuery(req, panel, series) { +export function normalizeQuery(req, panel, series) { return next => doc => { return next(removeEmptyTopLevelAggregation(doc, series)); }; diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/query.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/query.js index 8c0923c624def..81e9c34b19b65 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/query.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/query.js @@ -17,19 +17,19 @@ * under the License. */ -import offsetTime from '../../offset_time'; -import getIntervalAndTimefield from '../../get_interval_and_timefield'; +import { offsetTime } from '../../offset_time'; +import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; import { buildEsQuery } from '@kbn/es-query'; -export default function query(req, panel, series, esQueryConfig, indexPattern) { +export function query(req, panel, series, esQueryConfig, indexPatternObject) { return next => doc => { - const { timeField } = getIntervalAndTimefield(panel, series); + const { timeField } = getIntervalAndTimefield(panel, series, indexPatternObject); const { from, to } = offsetTime(req, series.offset_time); doc.size = 0; const queries = !panel.ignore_global_filter ? req.payload.query : []; const filters = !panel.ignore_global_filter ? req.payload.filters : []; - doc.query = buildEsQuery(indexPattern, queries, filters, esQueryConfig); + doc.query = buildEsQuery(indexPatternObject, queries, filters, esQueryConfig); const timerange = { range: { @@ -43,21 +43,11 @@ export default function query(req, panel, series, esQueryConfig, indexPattern) { doc.query.bool.must.push(timerange); if (panel.filter) { - doc.query.bool.must.push({ - query_string: { - query: panel.filter, - analyze_wildcard: true, - }, - }); + doc.query.bool.must.push(buildEsQuery(indexPatternObject, [panel.filter], [], esQueryConfig)); } if (series.filter) { - doc.query.bool.must.push({ - query_string: { - query: series.filter, - analyze_wildcard: true, - }, - }); + doc.query.bool.must.push(buildEsQuery(indexPatternObject, [series.filter], [], esQueryConfig)); } return next(doc); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/sibling_buckets.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/sibling_buckets.js index 351ff2bf27f62..db5af12e4ab57 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/sibling_buckets.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/sibling_buckets.js @@ -18,14 +18,13 @@ */ import _ from 'lodash'; -import getBucketSize from '../../helpers/get_bucket_size'; -import bucketTransform from '../../helpers/bucket_transform'; -import getIntervalAndTimefield from '../../get_interval_and_timefield'; -export default function siblingBuckets(req, panel, series, esQueryConfig, indexPatternObject, capabilities) { +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { bucketTransform } from '../../helpers/bucket_transform'; +import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; + +export function siblingBuckets(req, panel, series, esQueryConfig, indexPatternObject, capabilities) { return next => doc => { - const { - interval - } = getIntervalAndTimefield(panel, series); + const { interval } = getIntervalAndTimefield(panel, series, indexPatternObject); const { bucketSize } = getBucketSize(req, interval, capabilities); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/split_by_everything.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/split_by_everything.js index b36605ff9745f..ee3b03c8b1560 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/split_by_everything.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/split_by_everything.js @@ -18,7 +18,8 @@ */ import _ from 'lodash'; -export default function splitByEverything(req, panel, series) { + +export function splitByEverything(req, panel, series) { return next => doc => { if (series.split_mode === 'everything' || (series.split_mode === 'terms' && diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/split_by_filter.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/split_by_filter.js index 7e1915c79c6b5..5d7d5829bcf0b 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/split_by_filter.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/split_by_filter.js @@ -18,11 +18,13 @@ */ import _ from 'lodash'; -export default function splitByFilter(req, panel, series) { +import { buildEsQuery } from '@kbn/es-query'; + +export function splitByFilter(req, panel, series, esQueryConfig, indexPattern) { return next => doc => { if (series.split_mode !== 'filter') return next(doc); - _.set(doc, `aggs.${series.id}.filter.query_string.query`, series.filter || '*'); - _.set(doc, `aggs.${series.id}.filter.query_string.analyze_wildcard`, true); + _.set(doc, `aggs.${series.id}.filter`, buildEsQuery(indexPattern, [series.filter], [], esQueryConfig)); return next(doc); }; } + diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/split_by_filters.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/split_by_filters.js index c6ebaa4b6744d..4d26670c40452 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/split_by_filters.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/split_by_filters.js @@ -18,12 +18,13 @@ */ import _ from 'lodash'; -export default function splitByFilter(req, panel, series) { +import { buildEsQuery } from '@kbn/es-query'; +export function splitByFilters(req, panel, series, esQueryConfig, indexPattern) { return next => doc => { if (series.split_mode === 'filters' && series.split_filters) { series.split_filters.forEach(filter => { - _.set(doc, `aggs.${series.id}.filters.filters.${filter.id}.query_string.query`, filter.filter || '*'); - _.set(doc, `aggs.${series.id}.filters.filters.${filter.id}.query_string.analyze_wildcard`, true); + const builtEsQuery = buildEsQuery(indexPattern, [filter.filter], [], esQueryConfig); + _.set(doc, `aggs.${series.id}.filters.filters.${filter.id}`, builtEsQuery); }); } return next(doc); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/split_by_terms.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/split_by_terms.js index 6ee9b801cf6fa..bc468fbdf841f 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/split_by_terms.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/split_by_terms.js @@ -18,11 +18,11 @@ */ import { set } from 'lodash'; -import basicAggs from '../../../../../common/basic_aggs'; -import getBucketsPath from '../../helpers/get_buckets_path'; -import bucketTransform from '../../helpers/bucket_transform'; +import { basicAggs } from '../../../../../common/basic_aggs'; +import { getBucketsPath } from '../../helpers/get_buckets_path'; +import { bucketTransform } from '../../helpers/bucket_transform'; -export default function splitByTerm(req, panel, series) { +export function splitByTerms(req, panel, series) { return next => doc => { if (series.split_mode === 'terms' && series.terms_field) { const direction = series.terms_direction || 'desc'; diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/calculate_agg_root.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/calculate_agg_root.js index 15fb2eb4d5f01..b7d25d2be577b 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/calculate_agg_root.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/calculate_agg_root.js @@ -18,6 +18,7 @@ */ import _ from 'lodash'; + export function calculateAggRoot(doc, column) { let aggRoot = `aggs.pivot.aggs.${column.id}.aggs`; if (_.has(doc, `aggs.pivot.aggs.${column.id}.aggs.column_filter`)) { diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/date_histogram.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/date_histogram.js index 738a71198041a..c46fe6f824178 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/date_histogram.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/date_histogram.js @@ -18,14 +18,14 @@ */ import { set } from 'lodash'; -import getBucketSize from '../../helpers/get_bucket_size'; -import getIntervalAndTimefield from '../../get_interval_and_timefield'; -import getTimerange from '../../helpers/get_timerange'; +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; +import { getTimerange } from '../../helpers/get_timerange'; import { calculateAggRoot } from './calculate_agg_root'; -export default function dateHistogram(req, panel, esQueryConfig, indexPatternObject, capabilities) { +export function dateHistogram(req, panel, esQueryConfig, indexPatternObject, capabilities) { return next => doc => { - const { timeField, interval } = getIntervalAndTimefield(panel); + const { timeField, interval } = getIntervalAndTimefield(panel, {}, indexPatternObject); const { bucketSize, intervalString } = getBucketSize(req, interval, capabilities); const { from, to } = getTimerange(req); const timezone = capabilities.searchTimezone; diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/filter_ratios.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/filter_ratios.js index ef4bf22e14c0f..f416b3a74adfb 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/filter_ratios.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/filter_ratios.js @@ -19,10 +19,11 @@ /* eslint max-len:0 */ const filter = metric => metric.type === 'filter_ratio'; -import bucketTransform from '../../helpers/bucket_transform'; +import { bucketTransform } from '../../helpers/bucket_transform'; import _ from 'lodash'; import { calculateAggRoot } from './calculate_agg_root'; -export default function ratios(req, panel) { + +export function ratios(req, panel) { return next => doc => { panel.series.forEach(column => { const aggRoot = calculateAggRoot(doc, column); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/index.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/index.js index 0e160e8333c5c..e3041b6e4b955 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/index.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/index.js @@ -17,17 +17,17 @@ * under the License. */ -import pivot from './pivot'; -import query from './query'; -import splitByEverything from './split_by_everything'; -import splitByTerms from './split_by_terms'; -import dateHistogram from './date_histogram'; -import metricBuckets from './metric_buckets'; -import siblingBuckets from './sibling_buckets'; -import filterRatios from './filter_ratios'; -import normalizeQuery from './normalize_query'; +import { pivot } from './pivot'; +import { query } from './query'; +import { splitByEverything } from './split_by_everything'; +import { splitByTerms } from './split_by_terms'; +import { dateHistogram } from './date_histogram'; +import { metricBuckets } from './metric_buckets'; +import { siblingBuckets } from './sibling_buckets'; +import { ratios as filterRatios } from './filter_ratios'; +import { normalizeQuery } from './normalize_query'; -export default [ +export const processors = [ query, pivot, splitByTerms, diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/metric_buckets.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/metric_buckets.js index 082ff319b5ee4..44418efe42dbb 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/metric_buckets.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/metric_buckets.js @@ -18,13 +18,14 @@ */ import _ from 'lodash'; -import getBucketSize from '../../helpers/get_bucket_size'; -import bucketTransform from '../../helpers/bucket_transform'; -import getIntervalAndTimefield from '../../get_interval_and_timefield'; +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { bucketTransform } from '../../helpers/bucket_transform'; +import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; import { calculateAggRoot } from './calculate_agg_root'; -export default function metricBuckets(req, panel) { + +export function metricBuckets(req, panel, esQueryConfig, indexPatternObject) { return next => doc => { - const { interval } = getIntervalAndTimefield(panel); + const { interval } = getIntervalAndTimefield(panel, {}, indexPatternObject); const { intervalString } = getBucketSize(req, interval); panel.series.forEach(column => { const aggRoot = calculateAggRoot(doc, column); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/normalize_query.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/normalize_query.js index 2d8eb281858bf..03006e1573b32 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/normalize_query.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/normalize_query.js @@ -23,7 +23,7 @@ const isEmptyFilter = (filter = {}) => Boolean(filter.match_all) && isEmpty(filt /* Last query handler in the chain. You can use this handler * as the last place where you can modify the "doc" (request body) object before sending it to ES. */ -export default function normalizeQuery() { +export function normalizeQuery() { return () => doc => { const series = get(doc, 'aggs.pivot.aggs'); const normalizedSeries = {}; diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/pivot.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/pivot.js index c3563d636a19b..2a354a5f66efb 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/pivot.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/pivot.js @@ -19,11 +19,11 @@ import { get, set, last } from 'lodash'; -import basicAggs from '../../../../../common/basic_aggs'; -import getBucketsPath from '../../helpers/get_buckets_path'; -import bucketTransform from '../../helpers/bucket_transform'; +import { basicAggs } from '../../../../../common/basic_aggs'; +import { getBucketsPath } from '../../helpers/get_buckets_path'; +import { bucketTransform } from '../../helpers/bucket_transform'; -export default function pivot(req, panel) { +export function pivot(req, panel) { return next => doc => { const { sort } = req.payload.state; if (panel.pivot_id) { diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/query.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/query.js index 7505c466282c2..212e7a615dcad 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/query.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/query.js @@ -17,19 +17,19 @@ * under the License. */ import { buildEsQuery } from '@kbn/es-query'; -import getTimerange from '../../helpers/get_timerange'; -import getIntervalAndTimefield from '../../get_interval_and_timefield'; +import { getTimerange } from '../../helpers/get_timerange'; +import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; -export default function query(req, panel, esQueryConfig, indexPattern) { +export function query(req, panel, esQueryConfig, indexPatternObject) { return next => doc => { - const { timeField } = getIntervalAndTimefield(panel); + const { timeField } = getIntervalAndTimefield(panel, {}, indexPatternObject); const { from, to } = getTimerange(req); doc.size = 0; const queries = !panel.ignore_global_filter ? req.payload.query : []; const filters = !panel.ignore_global_filter ? req.payload.filters : []; - doc.query = buildEsQuery(indexPattern, queries, filters, esQueryConfig); + doc.query = buildEsQuery(indexPatternObject, queries, filters, esQueryConfig); const timerange = { range: { @@ -41,14 +41,8 @@ export default function query(req, panel, esQueryConfig, indexPattern) { }, }; doc.query.bool.must.push(timerange); - if (panel.filter) { - doc.query.bool.must.push({ - query_string: { - query: panel.filter, - analyze_wildcard: true, - }, - }); + doc.query.bool.must.push(buildEsQuery(indexPatternObject, [panel.filter], [], esQueryConfig)); } return next(doc); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/sibling_buckets.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/sibling_buckets.js index 9cd1af9898053..758da28e93232 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/sibling_buckets.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/sibling_buckets.js @@ -18,13 +18,14 @@ */ import _ from 'lodash'; -import getBucketSize from '../../helpers/get_bucket_size'; -import bucketTransform from '../../helpers/bucket_transform'; -import getIntervalAndTimefield from '../../get_interval_and_timefield'; +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { bucketTransform } from '../../helpers/bucket_transform'; +import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; import { calculateAggRoot } from './calculate_agg_root'; -export default function siblingBuckets(req, panel) { + +export function siblingBuckets(req, panel, esQueryConfig, indexPatternObject) { return next => doc => { - const { interval } = getIntervalAndTimefield(panel); + const { interval } = getIntervalAndTimefield(panel, {}, indexPatternObject); const { bucketSize } = getBucketSize(req, interval); panel.series.forEach(column => { const aggRoot = calculateAggRoot(doc, column); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/split_by_everything.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/split_by_everything.js index 1a400f288e985..1896030d95111 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/split_by_everything.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/split_by_everything.js @@ -18,12 +18,12 @@ */ import _ from 'lodash'; -export default function splitByEverything(req, panel) { +import { buildEsQuery } from '@kbn/es-query'; +export function splitByEverything(req, panel, esQueryConfig, indexPattern) { return next => doc => { panel.series.filter(c => !(c.aggregate_by && c.aggregate_function)).forEach(column => { if (column.filter) { - _.set(doc, `aggs.pivot.aggs.${column.id}.filter.query_string.query`, column.filter); - _.set(doc, `aggs.pivot.aggs.${column.id}.filter.query_string.analyze_wildcard`, true); + _.set(doc, `aggs.pivot.aggs.${column.id}.filter`, buildEsQuery(indexPattern, [column.filter], [], esQueryConfig)); } else { _.set(doc, `aggs.pivot.aggs.${column.id}.filter.match_all`, {}); } diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/split_by_terms.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/split_by_terms.js index ad0e63a707951..443518302faee 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/split_by_terms.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/split_by_terms.js @@ -18,15 +18,14 @@ */ import _ from 'lodash'; - -export default function splitByTerm(req, panel) { +import { buildEsQuery } from '@kbn/es-query'; +export function splitByTerms(req, panel, esQueryConfig, indexPattern) { return next => doc => { panel.series.filter(c => c.aggregate_by && c.aggregate_function).forEach(column => { _.set(doc, `aggs.pivot.aggs.${column.id}.terms.field`, column.aggregate_by); _.set(doc, `aggs.pivot.aggs.${column.id}.terms.size`, 100); if (column.filter) { - _.set(doc, `aggs.pivot.aggs.${column.id}.column_filter.filter.query_string.query`, column.filter); - _.set(doc, `aggs.pivot.aggs.${column.id}.column_filter.filter.query_string.analyze_wildcard`, true); + _.set(doc, `aggs.pivot.aggs.${column.id}.column_filter.filter`, buildEsQuery(indexPattern, [column.filter], [], esQueryConfig)); } }); return next(doc); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/annotations/buckets.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/annotations/buckets.js index 61b8f1c5fe9ff..6170f9b7e4f63 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/annotations/buckets.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/annotations/buckets.js @@ -17,15 +17,13 @@ * under the License. */ -import { get } from 'lodash'; +import { get, isEmpty } from 'lodash'; export function getAnnotationBuckets(resp, annotation) { return get(resp, `aggregations.${annotation.id}.buckets`, []) - .filter(bucket => bucket.hits.hits.total) - .map((bucket) => { - return { - key: bucket.key, - docs: bucket.hits.hits.hits.map(doc => doc._source) - }; - }); + .filter(bucket => !isEmpty(bucket.hits.hits.hits)) + .map(bucket => ({ + key: bucket.key, + docs: bucket.hits.hits.hits.map(doc => doc._source), + })); } diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/_series_agg.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/_series_agg.js index 4e906afcb4e6f..43723577c47ed 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/_series_agg.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/_series_agg.js @@ -18,7 +18,7 @@ */ import { expect } from 'chai'; -import seriesAgg from '../_series_agg'; +import { SeriesAgg as seriesAgg } from '../_series_agg'; describe('seriesAgg', () => { const series = [ diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/percentile.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/percentile.js index e7f5598f65618..0576457994153 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/percentile.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/percentile.js @@ -17,7 +17,7 @@ * under the License. */ -import percentile from '../percentile'; +import { percentile } from '../percentile'; import { expect } from 'chai'; import sinon from 'sinon'; diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/series_agg.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/series_agg.js index b5210e8acbf64..803932a152bfc 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/series_agg.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/series_agg.js @@ -17,8 +17,8 @@ * under the License. */ -import seriesAgg from '../series_agg'; -import stdMetric from '../std_metric'; +import { seriesAgg } from '../series_agg'; +import { stdMetric } from '../std_metric'; import { expect } from 'chai'; import sinon from 'sinon'; diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/std_deviation_bands.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/std_deviation_bands.js index a882fad3d8bab..47f766c560ad7 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/std_deviation_bands.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/std_deviation_bands.js @@ -17,7 +17,7 @@ * under the License. */ -import stdDeviationBands from '../std_deviation_bands'; +import { stdDeviationBands } from '../std_deviation_bands'; import { expect } from 'chai'; import sinon from 'sinon'; diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/std_deviation_sibling.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/std_deviation_sibling.js index 1f9736e890687..fbf7b7643b315 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/std_deviation_sibling.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/std_deviation_sibling.js @@ -17,7 +17,7 @@ * under the License. */ -import stdDeviationSibling from '../std_deviation_sibling'; +import { stdDeviationSibling } from '../std_deviation_sibling'; import { expect } from 'chai'; import sinon from 'sinon'; diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/std_metric.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/std_metric.js index e4330a4577f3a..c8d5fb1ad9f02 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/std_metric.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/std_metric.js @@ -19,7 +19,7 @@ import sinon from 'sinon'; import { expect } from 'chai'; -import stdMetric from '../std_metric'; +import { stdMetric } from '../std_metric'; describe('stdMetric(resp, panel, series)', () => { let panel; diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/std_sibling.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/std_sibling.js index ae70fd384b618..4792100be6e5d 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/std_sibling.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/std_sibling.js @@ -17,7 +17,7 @@ * under the License. */ -import stdSibling from '../std_sibling'; +import { stdSibling } from '../std_sibling'; import { expect } from 'chai'; import sinon from 'sinon'; diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/time_shift.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/time_shift.js index 18b3ddd6f2fc1..bccfa6be407c5 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/time_shift.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/time_shift.js @@ -19,8 +19,8 @@ import sinon from 'sinon'; import { expect } from 'chai'; -import timeShift from '../time_shift'; -import stdMetric from '../std_metric'; +import { timeShift } from '../time_shift'; +import { stdMetric } from '../std_metric'; describe('timeShift(resp, panel, series)', () => { let panel; diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/_series_agg.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/_series_agg.js index dd5a2c3c1787c..1e616916992a5 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/_series_agg.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/_series_agg.js @@ -23,11 +23,13 @@ function mean(values) { return _.sum(values) / values.length; } +const extractValue = r => r && r[1] || 0; + const basic = fnName => targetSeries => { const data = []; _.zip(...targetSeries).forEach(row => { const key = row[0][0]; - const values = row.map(r => r && r[1] || 0); + const values = row.map(extractValue); const fn = _[fnName] || (() => null); data.push([key, fn(values)]); }); @@ -40,13 +42,13 @@ const overall = fnName => targetSeries => { const values = []; _.zip(...targetSeries).forEach(row => { keys.push(row[0][0]); - values.push(fn(row.map(r => r && r[1] || 0))); + values.push(fn(row.map(extractValue))); }); return [keys.map(k => [k, fn(values)])]; }; -export default { +export const SeriesAgg = { sum: basic('sum'), max: basic('max'), min: basic('min'), @@ -54,7 +56,7 @@ export default { const data = []; _.zip(...targetSeries).forEach(row => { const key = row[0][0]; - const values = row.map(r => r && r[1] || 0); + const values = row.map(extractValue); data.push([key, mean(values)]); }); return [data]; @@ -71,7 +73,7 @@ export default { const values = []; _.zip(...targetSeries).forEach(row => { keys.push(row[0][0]); - values.push(_.sum(row.map(r => r && r[1] || 0))); + values.push(_.sum(row.map(extractValue))); }); return [keys.map(k => [k, fn(values)])]; }, @@ -81,10 +83,10 @@ export default { let sum = 0; _.zip(...targetSeries).forEach(row => { const key = row[0][0]; - sum += _.sum(row.map(r => r && r[1] || 0)); + sum += _.sum(row.map(extractValue)); data.push([key, sum]); }); return [data]; - } + }, }; diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/index.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/index.js index 1c7b38b6c4bcf..d418c35f03331 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/index.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/index.js @@ -17,19 +17,19 @@ * under the License. */ -import percentile from './percentile'; -import percentileRank from './percentile_rank'; +import { percentile } from './percentile'; +import { percentileRank } from './percentile_rank'; -import seriesAgg from './series_agg'; -import stdDeviationBands from './std_deviation_bands'; -import stdDeviationSibling from './std_deviation_sibling'; -import stdMetric from './std_metric'; -import stdSibling from './std_sibling'; -import timeShift from './time_shift'; +import { seriesAgg } from './series_agg'; +import { stdDeviationBands } from './std_deviation_bands'; +import { stdDeviationSibling } from './std_deviation_sibling'; +import { stdMetric } from './std_metric'; +import { stdSibling } from './std_sibling'; +import { timeShift } from './time_shift'; import { dropLastBucket } from './drop_last_bucket'; import { mathAgg } from './math'; -export default [ +export const processors = [ percentile, percentileRank, stdDeviationBands, diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/math.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/math.js index ada43f1e35cf5..f9fad714b8953 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/math.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/math.js @@ -19,10 +19,10 @@ const percentileValueMatch = /\[([0-9\.]+)\]$/; import { startsWith, flatten, values, first, last } from 'lodash'; -import getDefaultDecoration from '../../helpers/get_default_decoration'; -import getSiblingAggValue from '../../helpers/get_sibling_agg_value'; -import getSplits from '../../helpers/get_splits'; -import mapBucket from '../../helpers/map_bucket'; +import { getDefaultDecoration } from '../../helpers/get_default_decoration'; +import { getSiblingAggValue } from '../../helpers/get_sibling_agg_value'; +import { getSplits } from '../../helpers/get_splits'; +import { mapBucket } from '../../helpers/map_bucket'; import { evaluate } from 'tinymath'; export function mathAgg(resp, panel, series, meta) { diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/percentile.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/percentile.js index 487e5dda240b0..d4c55122d8ec9 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/percentile.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/percentile.js @@ -18,13 +18,13 @@ */ import _ from 'lodash'; -import getAggValue from '../../helpers/get_agg_value'; -import getDefaultDecoration from '../../helpers/get_default_decoration'; -import getSplits from '../../helpers/get_splits'; -import getLastMetric from '../../helpers/get_last_metric'; +import { getAggValue } from '../../helpers/get_agg_value'; +import { getDefaultDecoration } from '../../helpers/get_default_decoration'; +import { getSplits } from '../../helpers/get_splits'; +import { getLastMetric } from '../../helpers/get_last_metric'; import { METRIC_TYPES } from '../../../../../common/metric_types'; -export default function percentile(resp, panel, series, meta) { +export function percentile(resp, panel, series, meta) { return next => results => { const metric = getLastMetric(series); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/percentile_rank.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/percentile_rank.js index 1a3a7e20fca05..07a6d328dd62c 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/percentile_rank.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/percentile_rank.js @@ -16,14 +16,14 @@ * specific language governing permissions and limitations * under the License. */ -import getAggValue from '../../helpers/get_agg_value'; -import getDefaultDecoration from '../../helpers/get_default_decoration'; -import getSplits from '../../helpers/get_splits'; -import getLastMetric from '../../helpers/get_last_metric'; +import { getAggValue } from '../../helpers/get_agg_value'; +import { getDefaultDecoration } from '../../helpers/get_default_decoration'; +import { getSplits } from '../../helpers/get_splits'; +import { getLastMetric } from '../../helpers/get_last_metric'; import { toPercentileNumber } from '../../../../../common/to_percentile_number'; import { METRIC_TYPES } from '../../../../../common/metric_types'; -export default function percentileRank(resp, panel, series, meta) { +export function percentileRank(resp, panel, series, meta) { return next => results => { const metric = getLastMetric(series); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/series_agg.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/series_agg.js index 9369188aad15a..21126ff56cf75 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/series_agg.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/series_agg.js @@ -17,11 +17,12 @@ * under the License. */ -import SeriesAgg from './_series_agg'; +import { SeriesAgg } from './_series_agg'; import _ from 'lodash'; -import getDefaultDecoration from '../../helpers/get_default_decoration'; -import calculateLabel from '../../../../../common/calculate_label'; -export default function seriesAgg(resp, panel, series) { +import { getDefaultDecoration } from '../../helpers/get_default_decoration'; +import { calculateLabel } from '../../../../../common/calculate_label'; + +export function seriesAgg(resp, panel, series) { return next => results => { if (series.metrics.some(m => m.type === 'series_agg')) { const decoration = getDefaultDecoration(series); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/std_deviation_bands.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/std_deviation_bands.js index 5d4e6991fe524..a7f8e1baa27e6 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/std_deviation_bands.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/std_deviation_bands.js @@ -18,10 +18,11 @@ */ import _ from 'lodash'; -import getSplits from '../../helpers/get_splits'; -import getLastMetric from '../../helpers/get_last_metric'; -import mapBucket from '../../helpers/map_bucket'; -export default function stdDeviationBands(resp, panel, series, meta) { +import { getSplits } from '../../helpers/get_splits'; +import { getLastMetric } from '../../helpers/get_last_metric'; +import { mapBucket } from '../../helpers/map_bucket'; + +export function stdDeviationBands(resp, panel, series, meta) { return next => results => { const metric = getLastMetric(series); if (metric.type === 'std_deviation' && metric.mode === 'band') { diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/std_deviation_sibling.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/std_deviation_sibling.js index 45f5a0eb6bf73..604ea9c3c1763 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/std_deviation_sibling.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/std_deviation_sibling.js @@ -18,10 +18,11 @@ */ import _ from 'lodash'; -import getSplits from '../../helpers/get_splits'; -import getLastMetric from '../../helpers/get_last_metric'; -import getSiblingAggValue from '../../helpers/get_sibling_agg_value'; -export default function stdDeviationSibling(resp, panel, series, meta) { +import { getSplits } from '../../helpers/get_splits'; +import { getLastMetric } from '../../helpers/get_last_metric'; +import { getSiblingAggValue } from '../../helpers/get_sibling_agg_value'; + +export function stdDeviationSibling(resp, panel, series, meta) { return next => results => { const metric = getLastMetric(series); if (metric.mode === 'band' && metric.type === 'std_deviation_bucket') { diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/std_metric.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/std_metric.js index b35ec19a9c2aa..0fa723c8629b1 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/std_metric.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/std_metric.js @@ -17,13 +17,13 @@ * under the License. */ -import getDefaultDecoration from '../../helpers/get_default_decoration'; -import getSplits from '../../helpers/get_splits'; -import getLastMetric from '../../helpers/get_last_metric'; -import mapBucket from '../../helpers/map_bucket'; +import { getDefaultDecoration } from '../../helpers/get_default_decoration'; +import { getSplits } from '../../helpers/get_splits'; +import { getLastMetric } from '../../helpers/get_last_metric'; +import { mapBucket } from '../../helpers/map_bucket'; import { METRIC_TYPES } from '../../../../../common/metric_types'; -export default function stdMetric(resp, panel, series, meta) { +export function stdMetric(resp, panel, series, meta) { return next => results => { const metric = getLastMetric(series); if (metric.type === METRIC_TYPES.STD_DEVIATION && metric.mode === 'band') { diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/std_sibling.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/std_sibling.js index c3784e96e7390..f1dbe48be5dc9 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/std_sibling.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/std_sibling.js @@ -17,11 +17,12 @@ * under the License. */ -import getDefaultDecoration from '../../helpers/get_default_decoration'; -import getSplits from '../../helpers/get_splits'; -import getLastMetric from '../../helpers/get_last_metric'; -import getSiblingAggValue from '../../helpers/get_sibling_agg_value'; -export default function stdSibling(resp, panel, series, meta) { +import { getDefaultDecoration } from '../../helpers/get_default_decoration'; +import { getSplits } from '../../helpers/get_splits'; +import { getLastMetric } from '../../helpers/get_last_metric'; +import { getSiblingAggValue } from '../../helpers/get_sibling_agg_value'; + +export function stdSibling(resp, panel, series, meta) { return next => results => { const metric = getLastMetric(series); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/time_shift.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/time_shift.js index 0712a8ceec740..f24f219a135c0 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/time_shift.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/time_shift.js @@ -19,7 +19,8 @@ import _ from 'lodash'; import moment from 'moment'; -export default function timeShift(resp, panel, series) { + +export function timeShift(resp, panel, series) { return next => results => { if (/^([+-]?[\d]+)([shmdwMy]|ms)$/.test(series.offset_time)) { const matches = series.offset_time.match(/^([+-]?[\d]+)([shmdwMy]|ms)$/); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/table/_series_agg.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/table/_series_agg.js index e21b1cdf69a77..80cf72d280d95 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/table/_series_agg.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/table/_series_agg.js @@ -46,7 +46,7 @@ const overall = fnName => targetSeries => { }; -export default { +export const SeriesAgg = { sum: basic('sum'), max: basic('max'), min: basic('min'), diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/table/index.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/table/index.js index 3a8dd94845a1b..6b36a2882b07e 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/table/index.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/table/index.js @@ -18,16 +18,16 @@ */ // import percentile from './percentile'; -import stdMetric from './std_metric'; -import stdSibling from './std_sibling'; -import seriesAgg from './series_agg'; -import percentile from './percentile'; -import percentileRank from './percentile_rank'; +import { stdMetric } from './std_metric'; +import { stdSibling } from './std_sibling'; +import { seriesAgg } from './series_agg'; +import { percentile } from './percentile'; +import { percentileRank } from './percentile_rank'; import { math } from './math'; import { dropLastBucketFn } from './drop_last_bucket'; -export default [ +export const processors = [ percentile, percentileRank, stdMetric, diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/table/percentile.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/table/percentile.js index 4221f385414f0..a4b0fa427e94f 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/table/percentile.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/table/percentile.js @@ -17,12 +17,12 @@ * under the License. */ import { last } from 'lodash'; -import getSplits from '../../helpers/get_splits'; -import getLastMetric from '../../helpers/get_last_metric'; +import { getSplits } from '../../helpers/get_splits'; +import { getLastMetric } from '../../helpers/get_last_metric'; import { toPercentileNumber } from '../../../../../common/to_percentile_number'; import { METRIC_TYPES } from '../../../../../common/metric_types'; -export default function percentile(bucket, panel, series) { +export function percentile(bucket, panel, series) { return next => results => { const metric = getLastMetric(series); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/table/percentile_rank.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/table/percentile_rank.js index 192e7ffa4e0f6..d6723e58a9bb2 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/table/percentile_rank.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/table/percentile_rank.js @@ -17,13 +17,13 @@ * under the License. */ import { last } from 'lodash'; -import getSplits from '../../helpers/get_splits'; -import getLastMetric from '../../helpers/get_last_metric'; +import { getSplits } from '../../helpers/get_splits'; +import { getLastMetric } from '../../helpers/get_last_metric'; import { toPercentileNumber } from '../../../../../common/to_percentile_number'; -import getAggValue from '../../helpers/get_agg_value'; +import { getAggValue } from '../../helpers/get_agg_value'; import { METRIC_TYPES } from '../../../../../common/metric_types'; -export default function percentileRank(bucket, panel, series) { +export function percentileRank(bucket, panel, series) { return next => results => { const metric = getLastMetric(series); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/table/series_agg.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/table/series_agg.js index 11063213d0400..3b8cfadf69455 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/table/series_agg.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/table/series_agg.js @@ -17,10 +17,11 @@ * under the License. */ -import SeriesAgg from './_series_agg'; +import { SeriesAgg } from './_series_agg'; import _ from 'lodash'; -import calculateLabel from '../../../../../common/calculate_label'; -export default function seriesAgg(resp, panel, series) { +import { calculateLabel } from '../../../../../common/calculate_label'; + +export function seriesAgg(resp, panel, series) { return next => results => { if (series.aggregate_by && series.aggregate_function) { diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/table/std_metric.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/table/std_metric.js index 4241dae82268e..63985e07493b9 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/table/std_metric.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/table/std_metric.js @@ -17,12 +17,12 @@ * under the License. */ -import getSplits from '../../helpers/get_splits'; -import getLastMetric from '../../helpers/get_last_metric'; -import mapBucket from '../../helpers/map_bucket'; +import { getSplits } from '../../helpers/get_splits'; +import { getLastMetric } from '../../helpers/get_last_metric'; +import { mapBucket } from '../../helpers/map_bucket'; import { METRIC_TYPES } from '../../../../../common/metric_types'; -export default function stdMetric(bucket, panel, series) { +export function stdMetric(bucket, panel, series) { return next => results => { const metric = getLastMetric(series); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/table/std_sibling.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/table/std_sibling.js index 5103cbfbfc797..5ba489005bcf4 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/table/std_sibling.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/table/std_sibling.js @@ -17,10 +17,11 @@ * under the License. */ -import getSplits from '../../helpers/get_splits'; -import getLastMetric from '../../helpers/get_last_metric'; -import getSiblingAggValue from '../../helpers/get_sibling_agg_value'; -export default function stdSibling(bucket, panel, series) { +import { getSplits } from '../../helpers/get_splits'; +import { getLastMetric } from '../../helpers/get_last_metric'; +import { getSiblingAggValue } from '../../helpers/get_sibling_agg_value'; + +export function stdSibling(bucket, panel, series) { return next => results => { const metric = getLastMetric(series); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/series/build_request_body.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/series/build_request_body.js index a5b724e11ef55..fe3137a8f86ba 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/series/build_request_body.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/series/build_request_body.js @@ -17,8 +17,8 @@ * under the License. */ -import buildProcessorFunction from '../build_processor_function'; -import processors from '../request_processors/series'; +import { buildProcessorFunction } from '../build_processor_function'; +import { processors } from '../request_processors/series'; /** * Builds series request body diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/series/get_request_params.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/series/get_request_params.js index 581561a0cbce4..6e30e232bf94f 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/series/get_request_params.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/series/get_request_params.js @@ -21,23 +21,16 @@ import { getEsShardTimeout } from '../helpers/get_es_shard_timeout'; import { getIndexPatternObject } from '../helpers/get_index_pattern'; export async function getSeriesRequestParams(req, panel, series, esQueryConfig, capabilities) { - const bodies = []; const indexPattern = series.override_index_pattern && series.series_index_pattern || panel.index_pattern; const { indexPatternObject, indexPatternString } = await getIndexPatternObject(req, indexPattern); const request = buildRequestBody(req, panel, series, esQueryConfig, indexPatternObject, capabilities); const esShardTimeout = await getEsShardTimeout(req); - if (capabilities.batchRequestsSupport) { - bodies.push({ - index: indexPatternString, - }); - } - - if (esShardTimeout > 0) { - request.timeout = `${esShardTimeout}ms`; - } - - bodies.push(request); - - return bodies; + return { + index: indexPatternString, + body: { + ...request, + timeout: esShardTimeout > 0 ? `${esShardTimeout}ms` : undefined + } + }; } diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/series/handle_response_body.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/series/handle_response_body.js index cd108139fffe7..e823346019b40 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/series/handle_response_body.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/series/handle_response_body.js @@ -17,12 +17,12 @@ * under the License. */ -import buildProcessorFunction from '../build_processor_function'; -import processors from '../response_processors/series'; +import { buildProcessorFunction } from '../build_processor_function'; +import { processors } from '../response_processors/series'; import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; -export default function handleResponseBody(panel) { +export function handleResponseBody(panel) { return resp => { if (resp.error) { const err = new Error(resp.error.type); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/table/build_request_body.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/table/build_request_body.js index 349863d9cb6c0..7715c803d374b 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/table/build_request_body.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/table/build_request_body.js @@ -17,13 +17,11 @@ * under the License. */ -import buildProcessorFunction from '../build_processor_function'; -import processors from '../request_processors/table'; +import { buildProcessorFunction } from '../build_processor_function'; +import { processors } from '../request_processors/table'; -function buildRequestBody(...args) { +export function buildRequestBody(...args) { const processor = buildProcessorFunction(processors, ...args); const doc = processor({}); return doc; } - -export default buildRequestBody; diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/table/process_bucket.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/table/process_bucket.js index 2b8bc55d55dce..cbbdbb5540bda 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/table/process_bucket.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/table/process_bucket.js @@ -17,14 +17,14 @@ * under the License. */ -import buildProcessorFunction from '../build_processor_function'; -import processors from '../response_processors/table'; -import getLastValue from '../../../../common/get_last_value'; +import { buildProcessorFunction } from '../build_processor_function'; +import { processors } from '../response_processors/table'; +import { getLastValue } from '../../../../common/get_last_value'; import regression from 'regression'; import { first, get, set } from 'lodash'; import { getActiveSeries } from '../helpers/get_active_series'; -export default function processBucket(panel) { +export function processBucket(panel) { return bucket => { const series = getActiveSeries(panel) .map(series => { diff --git a/src/legacy/core_plugins/metrics/server/routes/fields.js b/src/legacy/core_plugins/metrics/server/routes/fields.js index 762d551217ee3..e7f4a9078ee84 100644 --- a/src/legacy/core_plugins/metrics/server/routes/fields.js +++ b/src/legacy/core_plugins/metrics/server/routes/fields.js @@ -19,7 +19,8 @@ import { getFields } from '../lib/get_fields'; import { getIndexPatternService } from '../lib/get_index_pattern_service'; -export default (server) => { + +export const fieldsRoutes = (server) => { server.route({ config: { diff --git a/src/legacy/core_plugins/metrics/server/routes/vis.js b/src/legacy/core_plugins/metrics/server/routes/vis.js index 9af52211f17aa..6b60b2783bd90 100644 --- a/src/legacy/core_plugins/metrics/server/routes/vis.js +++ b/src/legacy/core_plugins/metrics/server/routes/vis.js @@ -17,9 +17,10 @@ * under the License. */ -import getVisData from '../lib/get_vis_data'; +import { getVisData } from '../lib/get_vis_data'; import Boom from 'boom'; -export default (server) => { + +export const visDataRoutes = (server) => { server.route({ path: '/api/metrics/vis/data', diff --git a/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js b/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js index c223660f869ba..0788d5fc6c4de 100644 --- a/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js +++ b/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js @@ -29,6 +29,9 @@ import worldJson from './world.json'; import EMS_CATALOGUE from '../../../../ui/public/vis/__tests__/map/ems_mocks/sample_manifest.json'; import EMS_FILES from '../../../../ui/public/vis/__tests__/map/ems_mocks/sample_files.json'; import EMS_TILES from '../../../../ui/public/vis/__tests__/map/ems_mocks/sample_tiles.json'; +import EMS_STYLE_ROAD_MAP_BRIGHT from '../../../../ui/public/vis/__tests__/map/ems_mocks/sample_style_bright'; +import EMS_STYLE_ROAD_MAP_DESATURATED from '../../../../ui/public/vis/__tests__/map/ems_mocks/sample_style_desaturated'; +import EMS_STYLE_DARK_MAP from '../../../../ui/public/vis/__tests__/map/ems_mocks/sample_style_dark'; import initialPng from './initial.png'; import toiso3Png from './toiso3.png'; @@ -105,6 +108,14 @@ describe('RegionMapsVisualizationTests', function () { return EMS_TILES; } else if (url.startsWith('https://files.foobar')) { return EMS_FILES; + } else if (url.startsWith('https://raster-style.foobar')) { + if (url.includes('osm-bright-desaturated')) { + return EMS_STYLE_ROAD_MAP_DESATURATED; + } else if (url.includes('osm-bright')) { + return EMS_STYLE_ROAD_MAP_BRIGHT; + } else if (url.includes('dark-matter')) { + return EMS_STYLE_DARK_MAP; + } } }); diff --git a/src/legacy/core_plugins/region_map/public/region_map_vis.js b/src/legacy/core_plugins/region_map/public/region_map_vis.js index 6028e9d7dc943..9556c9b66ad16 100644 --- a/src/legacy/core_plugins/region_map/public/region_map_vis.js +++ b/src/legacy/core_plugins/region_map/public/region_map_vis.js @@ -18,6 +18,7 @@ */ import './region_map_vis_params'; +import { i18n } from '@kbn/i18n'; import { VisFactoryProvider } from 'ui/vis/vis_factory'; import { Schemas } from 'ui/vis/editors/default/schemas'; import { VisTypesRegistryProvider } from 'ui/registry/vis_types'; @@ -27,7 +28,7 @@ import { RegionMapsVisualizationProvider } from './region_map_visualization'; import { Status } from 'ui/vis/update_status'; import { ORIGIN } from '../../../../legacy/core_plugins/tile_map/common/origin'; -VisTypesRegistryProvider.register(function RegionMapProvider(Private, regionmapsConfig, config, i18n) { +VisTypesRegistryProvider.register(function RegionMapProvider(Private, regionmapsConfig, config) { const VisFactory = Private(VisFactoryProvider); const RegionMapsVisualization = Private(RegionMapsVisualizationProvider); @@ -39,8 +40,8 @@ VisTypesRegistryProvider.register(function RegionMapProvider(Private, regionmaps return VisFactory.createBaseVisualization({ name: 'region_map', - title: i18n('regionMap.mapVis.regionMapTitle', { defaultMessage: 'Region Map' }), - description: i18n('regionMap.mapVis.regionMapDescription', { defaultMessage: 'Show metrics on a thematic map. Use one of the \ + title: i18n.translate('regionMap.mapVis.regionMapTitle', { defaultMessage: 'Region Map' }), + description: i18n.translate('regionMap.mapVis.regionMapDescription', { defaultMessage: 'Show metrics on a thematic map. Use one of the \ provided base maps, or add your own. Darker colors represent higher values.' }), icon: 'visMapRegion', visConfig: { @@ -66,16 +67,16 @@ provided base maps, or add your own. Darker colors represent higher values.' }), collections: { legendPositions: [{ value: 'bottomleft', - text: i18n('regionMap.mapVis.regionMapEditorConfig.bottomLeftText', { defaultMessage: 'bottom left' }), + text: i18n.translate('regionMap.mapVis.regionMapEditorConfig.bottomLeftText', { defaultMessage: 'bottom left' }), }, { value: 'bottomright', - text: i18n('regionMap.mapVis.regionMapEditorConfig.bottomRightText', { defaultMessage: 'bottom right' }), + text: i18n.translate('regionMap.mapVis.regionMapEditorConfig.bottomRightText', { defaultMessage: 'bottom right' }), }, { value: 'topleft', - text: i18n('regionMap.mapVis.regionMapEditorConfig.topLeftText', { defaultMessage: 'top left' }), + text: i18n.translate('regionMap.mapVis.regionMapEditorConfig.topLeftText', { defaultMessage: 'top left' }), }, { value: 'topright', - text: i18n('regionMap.mapVis.regionMapEditorConfig.topRightText', { defaultMessage: 'top right' }), + text: i18n.translate('regionMap.mapVis.regionMapEditorConfig.topRightText', { defaultMessage: 'top right' }), }], colorSchemas: Object.values(truncatedColorMaps).map(value => ({ id: value.id, label: value.label })), vectorLayers: vectorLayers, @@ -85,7 +86,7 @@ provided base maps, or add your own. Darker colors represent higher values.' }), { group: 'metrics', name: 'metric', - title: i18n('regionMap.mapVis.regionMapEditorConfig.schemas.metricTitle', { defaultMessage: 'Value' }), + title: i18n.translate('regionMap.mapVis.regionMapEditorConfig.schemas.metricTitle', { defaultMessage: 'Value' }), min: 1, max: 1, aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality', 'top_hits', @@ -98,7 +99,7 @@ provided base maps, or add your own. Darker colors represent higher values.' }), group: 'buckets', name: 'segment', icon: 'fa fa-globe', - title: i18n('regionMap.mapVis.regionMapEditorConfig.schemas.segmentTitle', { defaultMessage: 'shape field' }), + title: i18n.translate('regionMap.mapVis.regionMapEditorConfig.schemas.segmentTitle', { defaultMessage: 'shape field' }), min: 1, max: 1, aggFilter: ['terms'] diff --git a/src/legacy/core_plugins/region_map/public/region_map_visualization.js b/src/legacy/core_plugins/region_map/public/region_map_visualization.js index 8f3bb3b93d125..9ec1a61349c73 100644 --- a/src/legacy/core_plugins/region_map/public/region_map_visualization.js +++ b/src/legacy/core_plugins/region_map/public/region_map_visualization.js @@ -18,6 +18,7 @@ */ import 'plugins/kbn_vislib_vis_types/controls/vislib_basic_options'; +import { i18n } from '@kbn/i18n'; import { BaseMapsVisualizationProvider } from '../../tile_map/public/base_maps_visualization'; import ChoroplethLayer from './choropleth_layer'; import { truncatedColorMaps } from 'ui/vislib/components/color/truncated_colormaps'; @@ -26,7 +27,7 @@ import { TileMapTooltipFormatter } from './tooltip_formatter'; import 'ui/vis/map/service_settings'; import { toastNotifications } from 'ui/notify'; -export function RegionMapsVisualizationProvider(Private, config, i18n) { +export function RegionMapsVisualizationProvider(Private, config) { const tooltipFormatter = Private(TileMapTooltipFormatter); const BaseMapsVisualization = Private(BaseMapsVisualizationProvider); @@ -162,14 +163,14 @@ export function RegionMapsVisualizationProvider(Private, config, i18n) { const shouldShowWarning = this._params.isDisplayWarning && config.get('visualization:regionmap:showWarnings'); if (event.mismatches.length > 0 && shouldShowWarning) { toastNotifications.addWarning({ - title: i18n('regionMap.visualization.unableToShowMismatchesWarningTitle', { + title: i18n.translate('regionMap.visualization.unableToShowMismatchesWarningTitle', { defaultMessage: 'Unable to show {mismatchesLength} {oneMismatch, plural, one {result} other {results}} on map', values: { mismatchesLength: event.mismatches.length, oneMismatch: event.mismatches.length > 1 ? 0 : 1, }, }), - text: i18n('regionMap.visualization.unableToShowMismatchesWarningText', { + text: i18n.translate('regionMap.visualization.unableToShowMismatchesWarningText', { defaultMessage: 'Ensure that each of these term matches a shape on that shape\'s join field: {mismatches}', values: { mismatches: event.mismatches ? event.mismatches.join(', ') : '', diff --git a/src/legacy/core_plugins/region_map/public/tooltip_formatter.js b/src/legacy/core_plugins/region_map/public/tooltip_formatter.js index e96eac3dd358c..1ba73695c08f5 100644 --- a/src/legacy/core_plugins/region_map/public/tooltip_formatter.js +++ b/src/legacy/core_plugins/region_map/public/tooltip_formatter.js @@ -18,10 +18,11 @@ */ import $ from 'jquery'; -export const TileMapTooltipFormatter = ($compile, $rootScope) => { +import template from './tooltip.html'; +export const TileMapTooltipFormatter = ($compile, $rootScope) => { const $tooltipScope = $rootScope.$new(); - const $el = $('
').html(require('./tooltip.html')); + const $el = $('
').html(template); $compile($el)($tooltipScope); return function tooltipFormatter(metric, fieldFormatter, fieldName, metricName) { diff --git a/src/legacy/core_plugins/status_page/public/lib/load_status.test.js b/src/legacy/core_plugins/status_page/public/lib/load_status.test.js index f2fc9789f5dd7..e523fc06019ca 100644 --- a/src/legacy/core_plugins/status_page/public/lib/load_status.test.js +++ b/src/legacy/core_plugins/status_page/public/lib/load_status.test.js @@ -17,16 +17,9 @@ * under the License. */ +import './load_status.test.mocks'; import loadStatus from './load_status'; -// Make importing the ui/notify module work in jest -jest.mock('ui/metadata', () => ({ - metadata: { - branch: 'my-metadata-branch', - version: 'my-metadata-version' - } -})); - // A faked response to the `fetch` call const mockFetch = async () => ({ status: 200, diff --git a/src/legacy/core_plugins/status_page/public/lib/load_status.test.mocks.js b/src/legacy/core_plugins/status_page/public/lib/load_status.test.mocks.js new file mode 100644 index 0000000000000..498a7344830fd --- /dev/null +++ b/src/legacy/core_plugins/status_page/public/lib/load_status.test.mocks.js @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { fatalErrorsServiceMock, notificationServiceMock } from '../../../../../core/public/mocks'; + +jest.doMock('ui/new_platform', () => ({ + npSetup: { + core: { + fatalErrors: fatalErrorsServiceMock.createSetupContract(), + notifications: notificationServiceMock.createSetupContract(), + } + }, +})); + +// Make importing the ui/notify module work in jest +jest.doMock('ui/metadata', () => ({ + metadata: { + branch: 'my-metadata-branch', + version: 'my-metadata-version' + } +})); diff --git a/src/legacy/core_plugins/status_page/public/status_page.js b/src/legacy/core_plugins/status_page/public/status_page.js index 140550256303a..b545a261c8739 100644 --- a/src/legacy/core_plugins/status_page/public/status_page.js +++ b/src/legacy/core_plugins/status_page/public/status_page.js @@ -20,15 +20,14 @@ import 'ui/autoload/styles'; import 'ui/i18n'; import chrome from 'ui/chrome'; -import { onStart } from 'ui/new_platform'; +import { npStart } from 'ui/new_platform'; import { destroyStatusPage, renderStatusPage } from './components/render'; +import template from 'plugins/status_page/status_page.html'; -onStart(({ core }) => { - core.chrome.navLinks.enableForcedAppSwitcherNavigation(); -}); +npStart.core.chrome.navLinks.enableForcedAppSwitcherNavigation(); chrome - .setRootTemplate(require('plugins/status_page/status_page.html')) + .setRootTemplate(template) .setRootController('ui', function ($scope, buildNum, buildSha) { $scope.$$postDigest(() => { renderStatusPage(buildNum, buildSha.substr(0, 8)); diff --git a/src/legacy/core_plugins/table_vis/public/__tests__/_table_vis_controller.js b/src/legacy/core_plugins/table_vis/public/__tests__/_table_vis_controller.js index ac46ec373f9f0..a1b5ebbe8edca 100644 --- a/src/legacy/core_plugins/table_vis/public/__tests__/_table_vis_controller.js +++ b/src/legacy/core_plugins/table_vis/public/__tests__/_table_vis_controller.js @@ -20,7 +20,7 @@ import $ from 'jquery'; import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import { LegacyResponseHandlerProvider } from 'ui/vis/response_handlers/legacy'; +import { legacyResponseHandlerProvider } from 'ui/vis/response_handlers/legacy'; import { VisProvider } from 'ui/vis'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; import { AppStateProvider } from 'ui/state_management/app_state'; @@ -46,7 +46,7 @@ describe('Table Vis Controller', function () { fixtures = require('fixtures/fake_hierarchical_data'); AppState = Private(AppStateProvider); Vis = Private(VisProvider); - tableAggResponse = Private(LegacyResponseHandlerProvider).handler; + tableAggResponse = legacyResponseHandlerProvider().handler; })); function OneRangeVis(params) { diff --git a/src/legacy/core_plugins/table_vis/public/agg_table/__tests__/_group.js b/src/legacy/core_plugins/table_vis/public/agg_table/__tests__/_group.js index a9bbbdea6f1f4..919ccbfbb4ef5 100644 --- a/src/legacy/core_plugins/table_vis/public/agg_table/__tests__/_group.js +++ b/src/legacy/core_plugins/table_vis/public/agg_table/__tests__/_group.js @@ -21,7 +21,7 @@ import $ from 'jquery'; import ngMock from 'ng_mock'; import expect from '@kbn/expect'; import fixtures from 'fixtures/fake_hierarchical_data'; -import { LegacyResponseHandlerProvider } from 'ui/vis/response_handlers/legacy'; +import { legacyResponseHandlerProvider } from 'ui/vis/response_handlers/legacy'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; import { VisProvider } from 'ui/vis'; import { tabifyAggResponse } from 'ui/agg_response/tabify'; @@ -56,7 +56,7 @@ describe('AggTableGroup Directive', function () { beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function ($injector, Private) { - tableAggResponse = Private(LegacyResponseHandlerProvider).handler; + tableAggResponse = legacyResponseHandlerProvider().handler; indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); Vis = Private(VisProvider); diff --git a/src/legacy/core_plugins/table_vis/public/agg_table/__tests__/_table.js b/src/legacy/core_plugins/table_vis/public/agg_table/__tests__/_table.js index 0916617104726..592dbe6666fc6 100644 --- a/src/legacy/core_plugins/table_vis/public/agg_table/__tests__/_table.js +++ b/src/legacy/core_plugins/table_vis/public/agg_table/__tests__/_table.js @@ -23,7 +23,7 @@ import ngMock from 'ng_mock'; import expect from '@kbn/expect'; import fixtures from 'fixtures/fake_hierarchical_data'; import sinon from 'sinon'; -import { LegacyResponseHandlerProvider } from 'ui/vis/response_handlers/legacy'; +import { legacyResponseHandlerProvider } from 'ui/vis/response_handlers/legacy'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; import { VisProvider } from 'ui/vis'; import { tabifyAggResponse } from 'ui/agg_response/tabify'; @@ -81,7 +81,7 @@ describe('AggTable Directive', function () { beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function ($injector, Private, config) { - tableAggResponse = Private(LegacyResponseHandlerProvider).handler; + tableAggResponse = legacyResponseHandlerProvider().handler; indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); Vis = Private(VisProvider); settings = config; diff --git a/src/legacy/core_plugins/table_vis/public/agg_table/agg_table.js b/src/legacy/core_plugins/table_vis/public/agg_table/agg_table.js index 3548497f2cd4c..9012c2ea9d15c 100644 --- a/src/legacy/core_plugins/table_vis/public/agg_table/agg_table.js +++ b/src/legacy/core_plugins/table_vis/public/agg_table/agg_table.js @@ -27,7 +27,7 @@ import { getFormat } from 'ui/visualize/loader/pipeline_helpers/utilities'; uiModules .get('kibana', ['RecursionHelper']) - .directive('kbnAggTable', function ($filter, config, Private, RecursionHelper) { + .directive('kbnAggTable', function (config, RecursionHelper) { return { restrict: 'E', diff --git a/src/legacy/core_plugins/table_vis/public/table_vis.js b/src/legacy/core_plugins/table_vis/public/table_vis.js index afe49d8695e27..d9ccdca626a11 100644 --- a/src/legacy/core_plugins/table_vis/public/table_vis.js +++ b/src/legacy/core_plugins/table_vis/public/table_vis.js @@ -26,7 +26,7 @@ import { VisFactoryProvider } from 'ui/vis/vis_factory'; import { Schemas } from 'ui/vis/editors/default/schemas'; import tableVisTemplate from './table_vis.html'; import { VisTypesRegistryProvider } from 'ui/registry/vis_types'; -import { LegacyResponseHandlerProvider as legacyResponseHandlerProvider } from 'ui/vis/response_handlers/legacy'; +import { legacyResponseHandlerProvider } from 'ui/vis/response_handlers/legacy'; import { VisFiltersProvider } from 'ui/vis/vis_filters'; // we need to load the css ourselves diff --git a/src/legacy/core_plugins/table_vis/public/table_vis_fn.js b/src/legacy/core_plugins/table_vis/public/table_vis_fn.js index 9498d8ce6549c..4176902c3db63 100644 --- a/src/legacy/core_plugins/table_vis/public/table_vis_fn.js +++ b/src/legacy/core_plugins/table_vis/public/table_vis_fn.js @@ -18,7 +18,7 @@ */ import { functionsRegistry } from 'plugins/interpreter/registries'; -import { LegacyResponseHandlerProvider as legacyResponseHandlerProvider } from 'ui/vis/response_handlers/legacy'; +import { legacyResponseHandlerProvider } from 'ui/vis/response_handlers/legacy'; import { i18n } from '@kbn/i18n'; export const kibanaTable = () => ({ diff --git a/src/legacy/core_plugins/table_vis/public/table_vis_fn.test.js b/src/legacy/core_plugins/table_vis/public/table_vis_fn.test.js index dc6da69800b29..1d2ff5ef72399 100644 --- a/src/legacy/core_plugins/table_vis/public/table_vis_fn.test.js +++ b/src/legacy/core_plugins/table_vis/public/table_vis_fn.test.js @@ -24,7 +24,7 @@ const mockResponseHandler = jest.fn().mockReturnValue(Promise.resolve({ tables: [{ columns: [], rows: [] }], })); jest.mock('ui/vis/response_handlers/legacy', () => ({ - LegacyResponseHandlerProvider: () => ({ handler: mockResponseHandler }), + legacyResponseHandlerProvider: () => ({ handler: mockResponseHandler }), })); describe('interpreter/functions#table', () => { diff --git a/src/legacy/core_plugins/tagcloud/public/tag_cloud_vis.js b/src/legacy/core_plugins/tagcloud/public/tag_cloud_vis.js index 18214965eab26..b3613d7b3d4e7 100644 --- a/src/legacy/core_plugins/tagcloud/public/tag_cloud_vis.js +++ b/src/legacy/core_plugins/tagcloud/public/tag_cloud_vis.js @@ -18,21 +18,22 @@ */ import './tag_cloud_vis_params'; +import { i18n } from '@kbn/i18n'; import { VisFactoryProvider } from 'ui/vis/vis_factory'; import { Schemas } from 'ui/vis/editors/default/schemas'; import { TagCloudVisualization } from './tag_cloud_visualization'; import { VisTypesRegistryProvider } from 'ui/registry/vis_types'; import { Status } from 'ui/vis/update_status'; -VisTypesRegistryProvider.register(function (Private, i18n) { +VisTypesRegistryProvider.register(function (Private) { const VisFactory = Private(VisFactoryProvider); return VisFactory.createBaseVisualization({ name: 'tagcloud', - title: i18n('tagCloud.vis.tagCloudTitle', { defaultMessage: 'Tag Cloud' }), + title: i18n.translate('tagCloud.vis.tagCloudTitle', { defaultMessage: 'Tag Cloud' }), icon: 'visTagCloud', - description: i18n('tagCloud.vis.tagCloudDescription', { + description: i18n.translate('tagCloud.vis.tagCloudDescription', { defaultMessage: 'A group of words, sized according to their importance' }), visConfig: { @@ -56,7 +57,7 @@ VisTypesRegistryProvider.register(function (Private, i18n) { { group: 'metrics', name: 'metric', - title: i18n('tagCloud.vis.schemas.metricTitle', { defaultMessage: 'Tag Size' }), + title: i18n.translate('tagCloud.vis.schemas.metricTitle', { defaultMessage: 'Tag Size' }), min: 1, max: 1, aggFilter: ['!std_dev', '!percentiles', '!percentile_ranks', '!derivative', '!geo_bounds', '!geo_centroid'], @@ -68,7 +69,7 @@ VisTypesRegistryProvider.register(function (Private, i18n) { group: 'buckets', name: 'segment', icon: 'fa fa-cloud', - title: i18n('tagCloud.vis.schemas.segmentTitle', { defaultMessage: 'Tags' }), + title: i18n.translate('tagCloud.vis.schemas.segmentTitle', { defaultMessage: 'Tags' }), min: 1, max: 1, aggFilter: ['terms', 'significant_terms'] diff --git a/src/legacy/core_plugins/tests_bundle/find_source_files.js b/src/legacy/core_plugins/tests_bundle/find_source_files.js index c867fb40b8193..bda1f226f8672 100644 --- a/src/legacy/core_plugins/tests_bundle/find_source_files.js +++ b/src/legacy/core_plugins/tests_bundle/find_source_files.js @@ -34,7 +34,8 @@ const findSourceFiles = async (patterns, cwd = fromRoot('.')) => { 'node_modules/**/*', 'bower_components/**/*', '**/_*.js', - '**/*.test.js' + '**/*.test.js', + '**/*.test.mocks.js', ], symlinks: findSourceFiles.symlinks, statCache: findSourceFiles.statCache, diff --git a/src/legacy/core_plugins/tests_bundle/tests_entry_template.js b/src/legacy/core_plugins/tests_bundle/tests_entry_template.js index 73dc777b317d9..f928e0403d912 100644 --- a/src/legacy/core_plugins/tests_bundle/tests_entry_template.js +++ b/src/legacy/core_plugins/tests_bundle/tests_entry_template.js @@ -35,7 +35,8 @@ import 'custom-event-polyfill'; import 'whatwg-fetch'; import 'abortcontroller-polyfill'; import 'childnode-remove-polyfill'; -import sinon from 'sinon'; +import fetchMock from 'fetch-mock/es5/client'; +import Symbol_observable from 'symbol-observable'; import { CoreSystem } from '__kibanaCore__'; @@ -59,22 +60,12 @@ const uiCapabilities = { }, }; -// Stub fetch for CoreSystem calls. -const fetchStub = sinon.stub(window, 'fetch'); -fetchStub.callsFake((url, options) => { - if (url !== '/api/capabilities') { - console.warn('Stubbed window.fetch does not support this request.'); - return Promise.resolve(new window.Response('Resource not found', { status: 404 })); - } - - return Promise.resolve( - new window.Response( - JSON.stringify({ capabilities: uiCapabilities })), - { - status: 200, - headers: { 'Content-Type': 'application/json' } - } - ); +// Mock fetch for CoreSystem calls. +fetchMock.config.fallbackToNetwork = true; +fetchMock.post(/\\/api\\/capabilities/, { + status: 200, + body: JSON.stringify({ capabilities: uiCapabilities }), + headers: { 'Content-Type': 'application/json' }, }); // render the core system in a child of the body as the default children of the body @@ -100,6 +91,7 @@ const coreSystem = new CoreSystem({ csp: { warnLegacyBrowsers: false, }, + capabilities: uiCapabilities, uiPlugins: [], vars: { kbnIndex: '.kibana', diff --git a/src/legacy/core_plugins/tile_map/common/ems_client.js b/src/legacy/core_plugins/tile_map/common/ems_client.js index e69c2a5cff9ca..accf9947442dd 100644 --- a/src/legacy/core_plugins/tile_map/common/ems_client.js +++ b/src/legacy/core_plugins/tile_map/common/ems_client.js @@ -99,7 +99,9 @@ export class EMSClient { constructor({ kbnVersion, manifestServiceUrl, htmlSanitizer, language, landingPageUrl }) { this._queryParams = { - my_app_version: kbnVersion + elastic_tile_service_tos: 'agree', + my_app_name: 'kibana', + my_app_version: kbnVersion, }; this._sanitizer = htmlSanitizer ? htmlSanitizer : x => x; @@ -127,7 +129,7 @@ export class EMSClient { /** * this internal method is overridden by the tests to simulate custom manifest. */ - async _getManifest(manifestUrl) { + async getManifest(manifestUrl) { let result; try { const url = extendUrl(manifestUrl, { query: this._queryParams }); @@ -187,7 +189,7 @@ export class EMSClient { _invalidateSettings() { this._getManifestWithParams = _.once( - async url => this._getManifest(this.extendUrlWithParams(url))); + async url => this.getManifest(this.extendUrlWithParams(url))); this._getCatalogueService = async serviceType => { const catalogueManifest = await this._getManifestWithParams(this._manifestServiceUrl); @@ -200,7 +202,7 @@ export class EMSClient { }; this._wrapServiceAttribute = async (manifestUrl, attr, WrapperClass) => { - const manifest = await this._getManifest(manifestUrl); + const manifest = await this.getManifest(manifestUrl); if (_.has(manifest, attr)) { return manifest[attr].map(config => { return new WrapperClass(config, this); diff --git a/src/legacy/core_plugins/tile_map/common/tms_service.js b/src/legacy/core_plugins/tile_map/common/tms_service.js index 8481f679a22e0..2c20619e540f8 100644 --- a/src/legacy/core_plugins/tile_map/common/tms_service.js +++ b/src/legacy/core_plugins/tile_map/common/tms_service.js @@ -17,33 +17,83 @@ * under the License. */ +import _ from 'lodash'; import { ORIGIN } from './origin'; export class TMSService { + _getTileJson = _.once( + async url => this._emsClient.getManifest(this._emsClient.extendUrlWithParams(url))); constructor(config, emsClient) { this._config = config; this._emsClient = emsClient; } - getUrlTemplate() { - return this._emsClient.extendUrlWithParams(this._config.url); + _getFormatsOfType(type) { + const formats = this._config.formats.filter(format => { + const language = this._emsClient.getLocale(); + return format.locale === language && format.format === type; + }); + return formats; + } + + _getDefaultStyleUrl() { + const defaultStyle = this._getFormatsOfType('raster')[0]; + if (defaultStyle && defaultStyle.hasOwnProperty('url')) { + return defaultStyle.url; + } + } + + async getUrlTemplate() { + const tileJson = await this._getTileJson(this._getDefaultStyleUrl()); + return this._emsClient.extendUrlWithParams(tileJson.tiles[0]); + } + + getDisplayName() { + const serviceName = this._emsClient.getValueInLanguage(this._config.name); + return serviceName; + } + + getAttributions() { + const attributions = this._config.attribution.map(attribution => { + const url = this._emsClient.getValueInLanguage(attribution.url); + const label = this._emsClient.getValueInLanguage(attribution.label); + return { + url: url, + label: label + }; + }); + return attributions; } getHTMLAttribution() { - return this._emsClient.sanitizeMarkdown(this._config.attribution); + const attributions = this._config.attribution.map(attribution => { + const url = this._emsClient.getValueInLanguage(attribution.url); + const label = this._emsClient.getValueInLanguage(attribution.label); + const html = url ? `${label}` : label; + return this._emsClient.sanitizeHtml(`${html}`); + }); + return `

${attributions.join(' | ')}

`;//!!!this is the current convention used in Kibana } getMarkdownAttribution() { - return this._config.attribution; + const attributions = this._config.attribution.map(attribution => { + const url = this._emsClient.getValueInLanguage(attribution.url); + const label = this._emsClient.getValueInLanguage(attribution.label); + const markdown = `[${label}](${url})`; + return markdown; + }); + return attributions.join('|'); } - getMinZoom() { - return this._config.minZoom; + async getMinZoom() { + const tileJson = await this._getTileJson(this._getDefaultStyleUrl()); + return tileJson.minzoom; } - getMaxZoom() { - return this._config.maxZoom; + async getMaxZoom() { + const tileJson = await this._getTileJson(this._getDefaultStyleUrl()); + return tileJson.maxzoom; } getId() { diff --git a/src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js b/src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js index 608ded804e5a7..8d28fea9bad0c 100644 --- a/src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js +++ b/src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js @@ -31,6 +31,9 @@ import heatmapRaw from './heatmap_raw.png'; import EMS_CATALOGUE from '../../../../ui/public/vis/__tests__/map/ems_mocks/sample_manifest.json'; import EMS_FILES from '../../../../ui/public/vis/__tests__/map/ems_mocks/sample_files.json'; import EMS_TILES from '../../../../ui/public/vis/__tests__/map/ems_mocks/sample_tiles.json'; +import EMS_STYLE_ROAD_MAP_BRIGHT from '../../../../ui/public/vis/__tests__/map/ems_mocks/sample_style_bright'; +import EMS_STYLE_ROAD_MAP_DESATURATED from '../../../../ui/public/vis/__tests__/map/ems_mocks/sample_style_desaturated'; +import EMS_STYLE_DARK_MAP from '../../../../ui/public/vis/__tests__/map/ems_mocks/sample_style_dark'; function mockRawData() { const stack = [dummyESResponse]; @@ -84,6 +87,14 @@ describe('CoordinateMapsVisualizationTest', function () { return EMS_TILES; } else if (url.startsWith('https://files.foobar')) { return EMS_FILES; + } else if (url.startsWith('https://raster-style.foobar')) { + if (url.includes('osm-bright-desaturated')) { + return EMS_STYLE_ROAD_MAP_DESATURATED; + } else if (url.includes('osm-bright')) { + return EMS_STYLE_ROAD_MAP_BRIGHT; + } else if (url.includes('dark-matter')) { + return EMS_STYLE_DARK_MAP; + } } }); })); diff --git a/src/legacy/core_plugins/tile_map/public/__tests__/scaledCircleMarkers.png b/src/legacy/core_plugins/tile_map/public/__tests__/scaledCircleMarkers.png index 803ea7ec75600..df659d1024f75 100644 Binary files a/src/legacy/core_plugins/tile_map/public/__tests__/scaledCircleMarkers.png and b/src/legacy/core_plugins/tile_map/public/__tests__/scaledCircleMarkers.png differ diff --git a/src/legacy/core_plugins/tile_map/public/base_maps_visualization.js b/src/legacy/core_plugins/tile_map/public/base_maps_visualization.js index 21c5539c4f2f9..a4c655951d1c9 100644 --- a/src/legacy/core_plugins/tile_map/public/base_maps_visualization.js +++ b/src/legacy/core_plugins/tile_map/public/base_maps_visualization.js @@ -18,12 +18,14 @@ */ import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; import { KibanaMap } from 'ui/vis/map/kibana_map'; import * as Rx from 'rxjs'; import { filter, first } from 'rxjs/operators'; import 'ui/vis/map/service_settings'; import { toastNotifications } from 'ui/notify'; import { uiModules } from 'ui/modules'; +import chrome from 'ui/chrome'; const WMS_MINZOOM = 0; const WMS_MAXZOOM = 22;//increase this to 22. Better for WMS @@ -35,7 +37,7 @@ const emsServiceSettings = new Promise((resolve) => { }); }); -export function BaseMapsVisualizationProvider(serviceSettings, i18n) { +export function BaseMapsVisualizationProvider(serviceSettings) { /** * Abstract base class for a visualization consisting of a map with a single baselayer. @@ -144,7 +146,7 @@ export function BaseMapsVisualizationProvider(serviceSettings, i18n) { async _updateBaseLayer() { - const DEFAULT_EMS_BASEMAP = 'road_map'; + const emsTileLayerId = chrome.getInjected('emsTileLayerId', true); if (!this._kibanaMap) { return; @@ -157,7 +159,7 @@ export function BaseMapsVisualizationProvider(serviceSettings, i18n) { const userConfiguredTmsLayer = tmsServices[0]; const initBasemapLayer = userConfiguredTmsLayer ? userConfiguredTmsLayer - : tmsServices.find(s => s.id === DEFAULT_EMS_BASEMAP); + : tmsServices.find(s => s.id === emsTileLayerId.bright); if (initBasemapLayer) { this._setTmsLayer(initBasemapLayer); } } catch (e) { toastNotifications.addWarning(e.message); @@ -197,19 +199,24 @@ export function BaseMapsVisualizationProvider(serviceSettings, i18n) { if (this._kibanaMap.getZoomLevel() > tmsLayer.maxZoom) { this._kibanaMap.setZoomLevel(tmsLayer.maxZoom); } - const url = await (await emsServiceSettings).getUrlTemplateForTMSLayer(tmsLayer); + let isDesaturated = this._getMapsParams().isDesaturated; + if (typeof isDesaturated !== 'boolean') { + isDesaturated = true; + } + const isDarkMode = chrome.getUiSettingsClient().get('theme:darkMode'); + const meta = await (await emsServiceSettings).getAttributesForTMSLayer(tmsLayer, isDesaturated, isDarkMode); const showZoomMessage = serviceSettings.shouldShowZoomMessage(tmsLayer); const options = _.cloneDeep(tmsLayer); delete options.id; - delete options.url; + delete options.subdomains; this._kibanaMap.setBaseLayer({ baseLayerType: 'tms', - options: { url, showZoomMessage, ...options } + options: { ...options, showZoomMessage, ...meta, } }); } async _updateData() { - throw new Error(i18n('tileMap.baseMapsVisualization.childShouldImplementMethodErrorMessage', { + throw new Error(i18n.translate('tileMap.baseMapsVisualization.childShouldImplementMethodErrorMessage', { defaultMessage: 'Child should implement this method to respond to data-update', })); } diff --git a/src/legacy/core_plugins/tile_map/public/editors/_tooltip_formatter.js b/src/legacy/core_plugins/tile_map/public/editors/_tooltip_formatter.js index 105bc510d8b2e..ba2293e72c814 100644 --- a/src/legacy/core_plugins/tile_map/public/editors/_tooltip_formatter.js +++ b/src/legacy/core_plugins/tile_map/public/editors/_tooltip_formatter.js @@ -18,11 +18,14 @@ */ import $ from 'jquery'; +import { i18n } from '@kbn/i18n'; -export function TileMapTooltipFormatterProvider($compile, $rootScope, i18n) { +import template from './_tooltip.html'; + +export function TileMapTooltipFormatterProvider($compile, $rootScope) { const $tooltipScope = $rootScope.$new(); - const $el = $('
').html(require('./_tooltip.html')); + const $el = $('
').html(template); $compile($el)($tooltipScope); return function tooltipFormatter(aggConfig, metricAgg, feature) { @@ -37,11 +40,11 @@ export function TileMapTooltipFormatterProvider($compile, $rootScope, i18n) { value: metricAgg.fieldFormatter()(feature.properties.value) }, { - label: i18n('tileMap.tooltipFormatter.latitudeLabel', { defaultMessage: 'Latitude' }), + label: i18n.translate('tileMap.tooltipFormatter.latitudeLabel', { defaultMessage: 'Latitude' }), value: feature.geometry.coordinates[1] }, { - label: i18n('tileMap.tooltipFormatter.longitudeLabel', { defaultMessage: 'Longitude' }), + label: i18n.translate('tileMap.tooltipFormatter.longitudeLabel', { defaultMessage: 'Longitude' }), value: feature.geometry.coordinates[0] } ]; diff --git a/src/legacy/core_plugins/tile_map/public/editors/wms_options.js b/src/legacy/core_plugins/tile_map/public/editors/wms_options.js index 47c379b255e10..e0497e85ab432 100644 --- a/src/legacy/core_plugins/tile_map/public/editors/wms_options.js +++ b/src/legacy/core_plugins/tile_map/public/editors/wms_options.js @@ -18,10 +18,11 @@ */ import { uiModules } from 'ui/modules'; +import { i18n } from '@kbn/i18n'; import wmsOptionsTemplate from './wms_options.html'; const module = uiModules.get('kibana'); -module.directive('wmsOptions', function (serviceSettings, i18n) { +module.directive('wmsOptions', function (serviceSettings) { return { restrict: 'E', template: wmsOptionsTemplate, @@ -31,7 +32,7 @@ module.directive('wmsOptions', function (serviceSettings, i18n) { collections: '=', }, link: function ($scope) { - $scope.wmsLinkText = i18n('tileMap.wmsOptions.wmsLinkText', { defaultMessage: 'here' }); + $scope.wmsLinkText = i18n.translate('tileMap.wmsOptions.wmsLinkText', { defaultMessage: 'here' }); new Promise((resolve, reject) => { diff --git a/src/legacy/core_plugins/tile_map/public/tile_map_vis.js b/src/legacy/core_plugins/tile_map/public/tile_map_vis.js index 59e9957549bcc..3baf68aa8746f 100644 --- a/src/legacy/core_plugins/tile_map/public/tile_map_vis.js +++ b/src/legacy/core_plugins/tile_map/public/tile_map_vis.js @@ -29,7 +29,7 @@ import { Status } from 'ui/vis/update_status'; import { truncatedColorMaps } from 'ui/vislib/components/color/truncated_colormaps'; import { convertToGeoJson } from 'ui/vis/map/convert_to_geojson'; -VisTypesRegistryProvider.register(function TileMapVisType(Private, getAppState, courier, config) { +VisTypesRegistryProvider.register(function TileMapVisType(Private, config) { const VisFactory = Private(VisFactoryProvider); const CoordinateMapsVisualization = Private(CoordinateMapsVisualizationProvider); diff --git a/src/legacy/core_plugins/timelion/public/app.js b/src/legacy/core_plugins/timelion/public/app.js index b2f973c7dd4f9..a658fcc1755ba 100644 --- a/src/legacy/core_plugins/timelion/public/app.js +++ b/src/legacy/core_plugins/timelion/public/app.js @@ -19,6 +19,8 @@ import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; + import { capabilities } from 'ui/capabilities'; import { DocTitleProvider } from 'ui/doc_title'; import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; @@ -41,6 +43,11 @@ import 'ui/listen'; import 'ui/kbn_top_nav'; import 'ui/saved_objects/ui/saved_object_save_as_checkbox'; +import rootTemplate from 'plugins/timelion/index.html'; +import saveTemplate from 'plugins/timelion/partials/save_sheet.html'; +import loadTemplate from 'plugins/timelion/partials/load_sheet.html'; +import sheetTemplate from 'plugins/timelion/partials/sheet_options.html'; + require('plugins/timelion/directives/cells/cells'); require('plugins/timelion/directives/fixed_element'); require('plugins/timelion/directives/fullscreen/fullscreen'); @@ -65,23 +72,23 @@ require('ui/routes').enable(); require('ui/routes') .when('/:id?', { - template: require('plugins/timelion/index.html'), + template: rootTemplate, reloadOnSearch: false, k7Breadcrumbs: ($injector, $route) => $injector.invoke( $route.current.params.id ? getSavedSheetBreadcrumbs : getCreateBreadcrumbs ), - badge: (i18n, uiCapabilities) => { + badge: uiCapabilities => { if (uiCapabilities.timelion.save) { return undefined; } return { - text: i18n('timelion.badge.readOnly.text', { + text: i18n.translate('timelion.badge.readOnly.text', { defaultMessage: 'Read only', }), - tooltip: i18n('timelion.badge.readOnly.tooltip', { + tooltip: i18n.translate('timelion.badge.readOnly.tooltip', { defaultMessage: 'Unable to save Timelion sheets', }), iconType: 'glasses' @@ -117,11 +124,8 @@ app.controller('timelion', function ( AppState, config, confirmModal, - courier, kbnUrl, - Notifier, - Private, - i18n, + Private ) { // Keeping this at app scope allows us to keep the current page when the user @@ -132,10 +136,6 @@ app.controller('timelion', function ( timefilter.enableAutoRefreshSelector(); timefilter.enableTimeRangeSelector(); - const notify = new Notifier({ - location - }); - const savedVisualizations = Private(SavedObjectRegistryProvider).byLoaderPropertiesName.visualizations; const timezone = Private(timezoneProvider)(); const docTitle = Private(DocTitleProvider); @@ -167,10 +167,10 @@ app.controller('timelion', function ( const newSheetAction = { key: 'new', - label: i18n('timelion.topNavMenu.newSheetButtonLabel', { + label: i18n.translate('timelion.topNavMenu.newSheetButtonLabel', { defaultMessage: 'New', }), - description: i18n('timelion.topNavMenu.newSheetButtonAriaLabel', { + description: i18n.translate('timelion.topNavMenu.newSheetButtonAriaLabel', { defaultMessage: 'New Sheet', }), run: function () { kbnUrl.change('/'); }, @@ -179,10 +179,10 @@ app.controller('timelion', function ( const addSheetAction = { key: 'add', - label: i18n('timelion.topNavMenu.addChartButtonLabel', { + label: i18n.translate('timelion.topNavMenu.addChartButtonLabel', { defaultMessage: 'Add', }), - description: i18n('timelion.topNavMenu.addChartButtonAriaLabel', { + description: i18n.translate('timelion.topNavMenu.addChartButtonAriaLabel', { defaultMessage: 'Add a chart', }), run: function () { $scope.newCell(); }, @@ -191,22 +191,22 @@ app.controller('timelion', function ( const saveSheetAction = { key: 'save', - label: i18n('timelion.topNavMenu.saveSheetButtonLabel', { + label: i18n.translate('timelion.topNavMenu.saveSheetButtonLabel', { defaultMessage: 'Save', }), - description: i18n('timelion.topNavMenu.saveSheetButtonAriaLabel', { + description: i18n.translate('timelion.topNavMenu.saveSheetButtonAriaLabel', { defaultMessage: 'Save Sheet', }), - template: require('plugins/timelion/partials/save_sheet.html'), + template: saveTemplate, testId: 'timelionSaveButton', }; const deleteSheetAction = { key: 'delete', - label: i18n('timelion.topNavMenu.deleteSheetButtonLabel', { + label: i18n.translate('timelion.topNavMenu.deleteSheetButtonLabel', { defaultMessage: 'Delete', }), - description: i18n('timelion.topNavMenu.deleteSheetButtonAriaLabel', { + description: i18n.translate('timelion.topNavMenu.deleteSheetButtonAriaLabel', { defaultMessage: 'Delete current sheet', }), disableButton: function () { @@ -216,7 +216,7 @@ app.controller('timelion', function ( const title = savedSheet.title; function doDelete() { savedSheet.delete().then(() => { - toastNotifications.addSuccess(i18n( + toastNotifications.addSuccess(i18n.translate( 'timelion.topNavMenu.delete.modal.successNotificationText', { defaultMessage: `Deleted '{title}'`, @@ -229,17 +229,17 @@ app.controller('timelion', function ( const confirmModalOptions = { onConfirm: doDelete, - confirmButtonText: i18n('timelion.topNavMenu.delete.modal.confirmButtonLabel', { + confirmButtonText: i18n.translate('timelion.topNavMenu.delete.modal.confirmButtonLabel', { defaultMessage: 'Delete', }), - title: i18n('timelion.topNavMenu.delete.modalTitle', { + title: i18n.translate('timelion.topNavMenu.delete.modalTitle', { defaultMessage: `Delete Timelion sheet '{title}'?`, values: { title } }), }; confirmModal( - i18n('timelion.topNavMenu.delete.modal.warningText', { + i18n.translate('timelion.topNavMenu.delete.modal.warningText', { defaultMessage: `You can't recover deleted sheets.`, }), confirmModalOptions @@ -250,34 +250,34 @@ app.controller('timelion', function ( const openSheetAction = { key: 'open', - label: i18n('timelion.topNavMenu.openSheetButtonLabel', { + label: i18n.translate('timelion.topNavMenu.openSheetButtonLabel', { defaultMessage: 'Open', }), - description: i18n('timelion.topNavMenu.openSheetButtonAriaLabel', { + description: i18n.translate('timelion.topNavMenu.openSheetButtonAriaLabel', { defaultMessage: 'Open Sheet', }), - template: require('plugins/timelion/partials/load_sheet.html'), + template: loadTemplate, testId: 'timelionOpenButton', }; const optionsAction = { key: 'options', - label: i18n('timelion.topNavMenu.optionsButtonLabel', { + label: i18n.translate('timelion.topNavMenu.optionsButtonLabel', { defaultMessage: 'Options', }), - description: i18n('timelion.topNavMenu.optionsButtonAriaLabel', { + description: i18n.translate('timelion.topNavMenu.optionsButtonAriaLabel', { defaultMessage: 'Options', }), - template: require('plugins/timelion/partials/sheet_options.html'), + template: sheetTemplate, testId: 'timelionOptionsButton', }; const helpAction = { key: 'help', - label: i18n('timelion.topNavMenu.helpButtonLabel', { + label: i18n.translate('timelion.topNavMenu.helpButtonLabel', { defaultMessage: 'Help', }), - description: i18n('timelion.topNavMenu.helpButtonAriaLabel', { + description: i18n.translate('timelion.topNavMenu.helpButtonAriaLabel', { defaultMessage: 'Help', }), template: '', @@ -380,8 +380,11 @@ app.controller('timelion', function ( const err = new Error(resp.message); err.stack = resp.stack; - notify.error(err); - + toastNotifications.addError(err, { + title: i18n.translate('timelion.searchErrorTitle', { + defaultMessage: 'Timelion request error', + }), + }); }); }; @@ -395,7 +398,7 @@ app.controller('timelion', function ( savedSheet.save().then(function (id) { if (id) { toastNotifications.addSuccess({ - title: i18n('timelion.saveSheet.successNotificationText', { + title: i18n.translate('timelion.saveSheet.successNotificationText', { defaultMessage: `Saved sheet '{title}'`, values: { title: savedSheet.title }, }), @@ -420,7 +423,7 @@ app.controller('timelion', function ( savedExpression.save().then(function (id) { if (id) { toastNotifications.addSuccess( - i18n('timelion.saveExpression.successNotificationText', { + i18n.translate('timelion.saveExpression.successNotificationText', { defaultMessage: `Saved expression '{title}'`, values: { title: savedExpression.title }, }), diff --git a/src/legacy/core_plugins/timelion/public/directives/__tests__/timelion_expression_input_helpers.js b/src/legacy/core_plugins/timelion/public/directives/__tests__/timelion_expression_input_helpers.js index 16f44e2835b21..31d1533e527d1 100644 --- a/src/legacy/core_plugins/timelion/public/directives/__tests__/timelion_expression_input_helpers.js +++ b/src/legacy/core_plugins/timelion/public/directives/__tests__/timelion_expression_input_helpers.js @@ -60,7 +60,7 @@ describe('Timelion expression suggestions', () => { }; const argValueSuggestions = ArgValueSuggestionsProvider(privateStub, indexPatternsStub); // eslint-disable-line new-cap beforeEach(function () { - Parser = PEG.buildParser(grammar); + Parser = PEG.generate(grammar); }); describe('parse exception', () => { diff --git a/src/legacy/core_plugins/timelion/public/directives/chart/chart.js b/src/legacy/core_plugins/timelion/public/directives/chart/chart.js index 104f0bd1aca30..7a898d7b3e161 100644 --- a/src/legacy/core_plugins/timelion/public/directives/chart/chart.js +++ b/src/legacy/core_plugins/timelion/public/directives/chart/chart.js @@ -18,9 +18,11 @@ */ import panelRegistryProvider from '../../lib/panel_registry'; +import { i18n } from '@kbn/i18n'; + require('ui/modules') .get('apps/timelion', []) - .directive('chart', function (Private, i18n) { + .directive('chart', function (Private) { return { restrict: 'A', scope: { @@ -47,7 +49,7 @@ require('ui/modules') if (!panelSchema) { $elem.text( - i18n('timelion.chart.seriesList.noSchemaWarning', { + i18n.translate('timelion.chart.seriesList.noSchemaWarning', { defaultMessage: 'No such panel type: {renderType}', values: { renderType: $scope.seriesList.render.type }, }) diff --git a/src/legacy/core_plugins/timelion/public/directives/fixed_element.js b/src/legacy/core_plugins/timelion/public/directives/fixed_element.js index 12b0ac68c1e6b..c88fa3d777648 100644 --- a/src/legacy/core_plugins/timelion/public/directives/fixed_element.js +++ b/src/legacy/core_plugins/timelion/public/directives/fixed_element.js @@ -23,7 +23,7 @@ const app = require('ui/modules').get('apps/timelion', []); app.directive('fixedElementRoot', function () { return { restrict: 'A', - link: function ($scope, $elem) { + link: function ($elem) { let fixedAt; $(window).bind('scroll', function () { const fixed = $('[fixed-element]', $elem); diff --git a/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js b/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js index fcc36092e8da8..498636df9250b 100644 --- a/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js +++ b/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js @@ -56,10 +56,10 @@ import { import { comboBoxKeyCodes } from '@elastic/eui'; import { ArgValueSuggestionsProvider } from './timelion_expression_suggestions/arg_value_suggestions'; -const Parser = PEG.buildParser(grammar); +const Parser = PEG.generate(grammar); const app = require('ui/modules').get('apps/timelion', []); -app.directive('timelionExpressionInput', function ($document, $http, $interval, $timeout, Private) { +app.directive('timelionExpressionInput', function ($http, $timeout, Private) { return { restrict: 'E', scope: { diff --git a/src/legacy/core_plugins/timelion/public/directives/timelion_help/timelion_help.js b/src/legacy/core_plugins/timelion/public/directives/timelion_help/timelion_help.js index 97ca1b2edb9fb..87a122abf2c3b 100644 --- a/src/legacy/core_plugins/timelion/public/directives/timelion_help/timelion_help.js +++ b/src/legacy/core_plugins/timelion/public/directives/timelion_help/timelion_help.js @@ -18,6 +18,7 @@ */ import template from './timelion_help.html'; +import { i18n } from '@kbn/i18n'; import { uiModules } from 'ui/modules'; import _ from 'lodash'; import moment from 'moment'; @@ -25,7 +26,7 @@ import '../../components/timelionhelp_tabs_directive'; const app = uiModules.get('apps/timelion', []); -app.directive('timelionHelp', function ($http, i18n) { +app.directive('timelionHelp', function ($http) { return { restrict: 'E', template, @@ -46,46 +47,46 @@ app.directive('timelionHelp', function ($http, i18n) { }; $scope.translations = { - nextButtonLabel: i18n('timelion.help.nextPageButtonLabel', { + nextButtonLabel: i18n.translate('timelion.help.nextPageButtonLabel', { defaultMessage: 'Next', }), - previousButtonLabel: i18n('timelion.help.previousPageButtonLabel', { + previousButtonLabel: i18n.translate('timelion.help.previousPageButtonLabel', { defaultMessage: 'Previous', }), - dontShowHelpButtonLabel: i18n('timelion.help.dontShowHelpButtonLabel', { + dontShowHelpButtonLabel: i18n.translate('timelion.help.dontShowHelpButtonLabel', { defaultMessage: `Don't show this again`, }), - strongNextText: i18n('timelion.help.welcome.content.strongNextText', { + strongNextText: i18n.translate('timelion.help.welcome.content.strongNextText', { defaultMessage: 'Next', }), - emphasizedEverythingText: i18n('timelion.help.welcome.content.emphasizedEverythingText', { + emphasizedEverythingText: i18n.translate('timelion.help.welcome.content.emphasizedEverythingText', { defaultMessage: 'everything', }), - notValidAdvancedSettingsPath: i18n('timelion.help.configuration.notValid.advancedSettingsPathText', { + notValidAdvancedSettingsPath: i18n.translate('timelion.help.configuration.notValid.advancedSettingsPathText', { defaultMessage: 'Management / Kibana / Advanced Settings' }), - validAdvancedSettingsPath: i18n('timelion.help.configuration.valid.advancedSettingsPathText', { + validAdvancedSettingsPath: i18n.translate('timelion.help.configuration.valid.advancedSettingsPathText', { defaultMessage: 'Management/Kibana/Advanced Settings', }), - esAsteriskQueryDescription: i18n('timelion.help.querying.esAsteriskQueryDescriptionText', { + esAsteriskQueryDescription: i18n.translate('timelion.help.querying.esAsteriskQueryDescriptionText', { defaultMessage: 'hey Elasticsearch, find everything in my default index', }), - esIndexQueryDescription: i18n('timelion.help.querying.esIndexQueryDescriptionText', { + esIndexQueryDescription: i18n.translate('timelion.help.querying.esIndexQueryDescriptionText', { defaultMessage: 'use * as the q (query) for the logstash-* index', }), - strongAddText: i18n('timelion.help.expressions.strongAddText', { + strongAddText: i18n.translate('timelion.help.expressions.strongAddText', { defaultMessage: 'Add', }), - twoExpressionsDescriptionTitle: i18n('timelion.help.expressions.examples.twoExpressionsDescriptionTitle', { + twoExpressionsDescriptionTitle: i18n.translate('timelion.help.expressions.examples.twoExpressionsDescriptionTitle', { defaultMessage: 'Double the fun.', }), - customStylingDescriptionTitle: i18n('timelion.help.expressions.examples.customStylingDescriptionTitle', { + customStylingDescriptionTitle: i18n.translate('timelion.help.expressions.examples.customStylingDescriptionTitle', { defaultMessage: 'Custom styling.', }), - namedArgumentsDescriptionTitle: i18n('timelion.help.expressions.examples.namedArgumentsDescriptionTitle', { + namedArgumentsDescriptionTitle: i18n.translate('timelion.help.expressions.examples.namedArgumentsDescriptionTitle', { defaultMessage: 'Named arguments.', }), - groupedExpressionsDescriptionTitle: i18n('timelion.help.expressions.examples.groupedExpressionsDescriptionTitle', { + groupedExpressionsDescriptionTitle: i18n.translate('timelion.help.expressions.examples.groupedExpressionsDescriptionTitle', { defaultMessage: 'Grouped expressions.', }), }; @@ -125,7 +126,7 @@ app.directive('timelionHelp', function ($http, i18n) { } catch (e) { if (_.get(resp, 'data.resp.message')) return _.get(resp, 'data.resp.message'); if (_.get(resp, 'data.resp.output.payload.message')) return _.get(resp, 'data.resp.output.payload.message'); - return i18n('timelion.help.unknownErrorMessage', { defaultMessage: 'Unknown error' }); + return i18n.translate('timelion.help.unknownErrorMessage', { defaultMessage: 'Unknown error' }); } }()); } diff --git a/src/legacy/core_plugins/timelion/public/directives/timelion_interval/timelion_interval.js b/src/legacy/core_plugins/timelion/public/directives/timelion_interval/timelion_interval.js index b47dafca5aa1e..7323874e62f95 100644 --- a/src/legacy/core_plugins/timelion/public/directives/timelion_interval/timelion_interval.js +++ b/src/legacy/core_plugins/timelion/public/directives/timelion_interval/timelion_interval.js @@ -23,7 +23,7 @@ import $ from 'jquery'; const app = require('ui/modules').get('apps/timelion', []); import template from './timelion_interval.html'; -app.directive('timelionInterval', function ($compile, $timeout) { +app.directive('timelionInterval', function ($timeout) { return { restrict: 'E', scope: { diff --git a/src/legacy/core_plugins/timelion/public/hacks/toggle_app_link_in_nav.ts b/src/legacy/core_plugins/timelion/public/hacks/toggle_app_link_in_nav.ts index 8aa8940c5bbd5..6480ea0a69e43 100644 --- a/src/legacy/core_plugins/timelion/public/hacks/toggle_app_link_in_nav.ts +++ b/src/legacy/core_plugins/timelion/public/hacks/toggle_app_link_in_nav.ts @@ -17,11 +17,9 @@ * under the License. */ -import { onStart } from 'ui/new_platform'; +import { npStart } from 'ui/new_platform'; -onStart(({ core }) => { - const timelionUiEnabled = core.injectedMetadata.getInjectedVar('timelionUiEnabled'); - if (timelionUiEnabled === false) { - core.chrome.navLinks.update('timelion', { hidden: true }); - } -}); +const timelionUiEnabled = npStart.core.injectedMetadata.getInjectedVar('timelionUiEnabled'); +if (timelionUiEnabled === false) { + npStart.core.chrome.navLinks.update('timelion', { hidden: true }); +} diff --git a/src/legacy/core_plugins/timelion/public/panels/panel.js b/src/legacy/core_plugins/timelion/public/panels/panel.js index 3efc88c7dc9d0..f45aeb08e31fe 100644 --- a/src/legacy/core_plugins/timelion/public/panels/panel.js +++ b/src/legacy/core_plugins/timelion/public/panels/panel.js @@ -17,7 +17,9 @@ * under the License. */ -export default function Panel(name, config, i18n) { +import { i18n } from '@kbn/i18n'; + +export default function Panel(name, config) { this.name = name; @@ -27,7 +29,7 @@ export default function Panel(name, config, i18n) { if (!config.render) { throw new Error ( - i18n('timelion.panels.noRenderFunctionErrorMessage', { + i18n.translate('timelion.panels.noRenderFunctionErrorMessage', { defaultMessage: 'Panel must have a rendering function' }) ); diff --git a/src/legacy/core_plugins/timelion/public/panels/timechart/timechart.js b/src/legacy/core_plugins/timelion/public/panels/timechart/timechart.js index 36bc20d8724ea..a3bee66f9d4ef 100644 --- a/src/legacy/core_plugins/timelion/public/panels/timechart/timechart.js +++ b/src/legacy/core_plugins/timelion/public/panels/timechart/timechart.js @@ -18,9 +18,10 @@ */ import Panel from '../panel'; +import { i18n } from '@kbn/i18n'; import panelRegistry from '../../lib/panel_registry'; -panelRegistry.register(function timeChartProvider(Private, i18n) { +panelRegistry.register(function timeChartProvider(Private) { // Schema is broken out so that it may be extended for use in other plugins // Its also easier to test. return new Panel('timechart', Private(require('./schema'))(), i18n); diff --git a/src/legacy/core_plugins/timelion/public/panels/timechart/xaxis_formatter.js b/src/legacy/core_plugins/timelion/public/panels/timechart/xaxis_formatter.js index 32a8ba53bad18..4e49c4eca6538 100644 --- a/src/legacy/core_plugins/timelion/public/panels/timechart/xaxis_formatter.js +++ b/src/legacy/core_plugins/timelion/public/panels/timechart/xaxis_formatter.js @@ -19,14 +19,16 @@ import moment from 'moment'; -export default function xaxisFormatterProvider(config, i18n) { +import { i18n } from '@kbn/i18n'; + +export default function xaxisFormatterProvider(config) { function getFormat(esInterval) { const parts = esInterval.match(/(\d+)(ms|s|m|h|d|w|M|y|)/); if (parts == null || parts[1] == null || parts[2] == null) { throw new Error ( - i18n('timelion.panels.timechart.unknownIntervalErrorMessage', { + i18n.translate('timelion.panels.timechart.unknownIntervalErrorMessage', { defaultMessage: 'Unknown interval', }) ); diff --git a/src/legacy/core_plugins/timelion/public/register_feature.js b/src/legacy/core_plugins/timelion/public/register_feature.js index afae454517ff5..415ed532f6a34 100644 --- a/src/legacy/core_plugins/timelion/public/register_feature.js +++ b/src/legacy/core_plugins/timelion/public/register_feature.js @@ -22,11 +22,13 @@ import { FeatureCatalogueCategory, } from 'ui/registry/feature_catalogue'; -FeatureCatalogueRegistryProvider.register(i18n => { +import { i18n } from '@kbn/i18n'; + +FeatureCatalogueRegistryProvider.register(() => { return { id: 'timelion', title: 'Timelion', - description: i18n('timelion.registerFeatureDescription', { + description: i18n.translate('timelion.registerFeatureDescription', { defaultMessage: 'Use an expression language to analyze time series data and visualize the results.', }), diff --git a/src/legacy/core_plugins/timelion/public/services/saved_sheets.js b/src/legacy/core_plugins/timelion/public/services/saved_sheets.js index ecb471401e611..d303069e74dea 100644 --- a/src/legacy/core_plugins/timelion/public/services/saved_sheets.js +++ b/src/legacy/core_plugins/timelion/public/services/saved_sheets.js @@ -32,7 +32,7 @@ savedObjectManagementRegistry.register({ }); // This is the only thing that gets injected into controllers -module.service('savedSheets', function (Private, Promise, SavedSheet, kbnIndex, kbnUrl, $http, chrome) { +module.service('savedSheets', function (Private, SavedSheet, kbnUrl, chrome) { const savedObjectClient = Private(SavedObjectsClientProvider); const savedSheetLoader = new SavedObjectLoader(SavedSheet, kbnUrl, chrome, savedObjectClient); savedSheetLoader.urlFor = function (id) { diff --git a/src/legacy/core_plugins/timelion/public/vis/index.js b/src/legacy/core_plugins/timelion/public/vis/index.js index 7ae3203e2b79a..4257d4334b602 100644 --- a/src/legacy/core_plugins/timelion/public/vis/index.js +++ b/src/legacy/core_plugins/timelion/public/vis/index.js @@ -18,6 +18,7 @@ */ import { VisFactoryProvider } from 'ui/vis/vis_factory'; +import { i18n } from '@kbn/i18n'; import { VisTypesRegistryProvider } from 'ui/registry/vis_types'; import { TimelionRequestHandlerProvider } from './timelion_request_handler'; import { DefaultEditorSize } from 'ui/vis/editor_size'; @@ -32,7 +33,7 @@ import editorConfigTemplate from './timelion_vis_params.html'; // register the provider with the visTypes registry so that other know it exists VisTypesRegistryProvider.register(TimelionVisProvider); -export default function TimelionVisProvider(Private, i18n) { +export default function TimelionVisProvider(Private) { const VisFactory = Private(VisFactoryProvider); const timelionRequestHandler = Private(TimelionRequestHandlerProvider); @@ -42,7 +43,7 @@ export default function TimelionVisProvider(Private, i18n) { name: 'timelion', title: 'Timelion', icon: 'visTimelion', - description: i18n('timelion.timelionDescription', { + description: i18n.translate('timelion.timelionDescription', { defaultMessage: 'Build time-series using functional expressions', }), visConfig: { diff --git a/src/legacy/core_plugins/timelion/public/vis/timelion_request_handler.js b/src/legacy/core_plugins/timelion/public/vis/timelion_request_handler.js index b57a654cc0263..21b6a243e77d9 100644 --- a/src/legacy/core_plugins/timelion/public/vis/timelion_request_handler.js +++ b/src/legacy/core_plugins/timelion/public/vis/timelion_request_handler.js @@ -20,14 +20,12 @@ import _ from 'lodash'; import { buildEsQuery, getEsQueryConfig } from '@kbn/es-query'; import { timezoneProvider } from 'ui/vis/lib/timezone'; +import { toastNotifications } from 'ui/notify'; +import { i18n } from '@kbn/i18n'; -const TimelionRequestHandlerProvider = function (Private, Notifier, $http, config) { +const TimelionRequestHandlerProvider = function (Private, $http, config) { const timezone = Private(timezoneProvider)(); - const notify = new Notifier({ - location: 'Timelion' - }); - return { name: 'timelion', handler: function ({ timeRange, filters, query, visParams }) { @@ -58,7 +56,11 @@ const TimelionRequestHandlerProvider = function (Private, Notifier, $http, confi .catch(function (resp) { const err = new Error(resp.message); err.stack = resp.stack; - notify.error(err); + toastNotifications.addError(err, { + title: i18n.translate('timelion.requestHandlerErrorTitle', { + defaultMessage: 'Timelion request error', + }), + }); reject(err); }); }); diff --git a/src/legacy/core_plugins/timelion/server/handlers/lib/parse_sheet.js b/src/legacy/core_plugins/timelion/server/handlers/lib/parse_sheet.js index 71510d5af625f..8aca7f516b7e0 100644 --- a/src/legacy/core_plugins/timelion/server/handlers/lib/parse_sheet.js +++ b/src/legacy/core_plugins/timelion/server/handlers/lib/parse_sheet.js @@ -23,7 +23,7 @@ import path from 'path'; import _ from 'lodash'; const grammar = fs.readFileSync(path.resolve(__dirname, '../../../public/chain.peg'), 'utf8'); import PEG from 'pegjs'; -const Parser = PEG.buildParser(grammar); +const Parser = PEG.generate(grammar); export default function parseSheet(sheet) { return _.map(sheet, function (plot) { diff --git a/src/legacy/core_plugins/ui_metric/server/usage/collector.ts b/src/legacy/core_plugins/ui_metric/server/usage/collector.ts index 2053c6cee1dd5..bbb7b1af8e7c7 100644 --- a/src/legacy/core_plugins/ui_metric/server/usage/collector.ts +++ b/src/legacy/core_plugins/ui_metric/server/usage/collector.ts @@ -52,6 +52,7 @@ export function registerUiMetricUsageCollector(server: any) { return uiMetricsByAppName; }, + isReady: () => true, }); server.usage.collectorSet.register(collector); diff --git a/src/legacy/core_plugins/vega/public/help_menus/vega_help_menu_directives.js b/src/legacy/core_plugins/vega/public/help_menus/vega_help_menu_directives.js index 46a9263691a3f..dae4a64fe8420 100644 --- a/src/legacy/core_plugins/vega/public/help_menus/vega_help_menu_directives.js +++ b/src/legacy/core_plugins/vega/public/help_menus/vega_help_menu_directives.js @@ -28,17 +28,9 @@ import { VegaHelpMenu } from './vega_help_menu'; import { VegaActionsMenu } from './vega_action_menu'; module.directive('vegaActionsMenu', function (reactDirective) { - return reactDirective( - wrapInI18nContext(VegaActionsMenu), - undefined, - { restrict: 'E' } - ); + return reactDirective(wrapInI18nContext(VegaActionsMenu)); }); module.directive('vegaHelpMenu', function (reactDirective) { - return reactDirective( - wrapInI18nContext(VegaHelpMenu), - undefined, - { restrict: 'E' } - ); + return reactDirective(wrapInI18nContext(VegaHelpMenu)); }); diff --git a/src/legacy/core_plugins/vega/public/vega_editor_controller.js b/src/legacy/core_plugins/vega/public/vega_editor_controller.js index 93a90fce189c4..451ff536a8ba0 100644 --- a/src/legacy/core_plugins/vega/public/vega_editor_controller.js +++ b/src/legacy/core_plugins/vega/public/vega_editor_controller.js @@ -19,16 +19,14 @@ import compactStringify from 'json-stringify-pretty-compact'; import hjson from 'hjson'; -import { Notifier } from 'ui/notify'; import { uiModules } from 'ui/modules'; import 'ui/accessibility/kbn_ui_ace_keyboard_mode'; +import { toastNotifications } from 'ui/notify'; +import { i18n } from '@kbn/i18n'; const module = uiModules.get('kibana/vega', ['kibana']); module.controller('VegaEditorController', ($scope /*, kbnUiAceKeyboardModeService*/) => { - - const notify = new Notifier({ location: 'Vega' }); - return new (class VegaEditorController { constructor() { $scope.aceLoaded = (editor) => { @@ -69,7 +67,11 @@ module.controller('VegaEditorController', ($scope /*, kbnUiAceKeyboardModeServic newSpec = stringify(spec, opts); } catch (err) { // This is a common case - user tries to format an invalid HJSON text - notify.error(err); + toastNotifications.addError(err, { + title: i18n.translate('vega.editor.formatError', { + defaultMessage: 'Error formatting spec', + }), + }); return; } diff --git a/src/legacy/core_plugins/vega/public/vega_editor_template.html b/src/legacy/core_plugins/vega/public/vega_editor_template.html index 498fa7c91ff53..4d5d6189f3302 100644 --- a/src/legacy/core_plugins/vega/public/vega_editor_template.html +++ b/src/legacy/core_plugins/vega/public/vega_editor_template.html @@ -19,13 +19,12 @@ >
- + + - - - + >
diff --git a/src/legacy/core_plugins/vega/public/vega_view/vega_map_view.js b/src/legacy/core_plugins/vega/public/vega_view/vega_map_view.js index 3effc883a0748..d05b4cf938cd7 100644 --- a/src/legacy/core_plugins/vega/public/vega_view/vega_map_view.js +++ b/src/legacy/core_plugins/vega/public/vega_view/vega_map_view.js @@ -22,6 +22,7 @@ import * as vega from 'vega-lib'; import { VegaBaseView } from './vega_base_view'; import { VegaMapLayer } from './vega_map_layer'; import { i18n } from '@kbn/i18n'; +import chrome from 'ui/chrome'; export class VegaMapView extends VegaBaseView { @@ -35,11 +36,13 @@ export class VegaMapView extends VegaBaseView { const tmsServices = await this._serviceSettings.getTMSServices(); // In some cases, Vega may be initialized twice, e.g. after awaiting... if (!this._$container) return; - const mapStyle = mapConfig.mapStyle === 'default' ? 'road_map' : mapConfig.mapStyle; + const emsTileLayerId = chrome.getInjected('emsTileLayerId', true); + const mapStyle = mapConfig.mapStyle === 'default' ? emsTileLayerId.bright : mapConfig.mapStyle; + const isDarkMode = chrome.getUiSettingsClient().get('theme:darkMode'); baseMapOpts = tmsServices.find((s) => s.id === mapStyle); baseMapOpts = { - url: await this._serviceSettings.getUrlTemplateForTMSLayer(baseMapOpts), - ...baseMapOpts + ...baseMapOpts, + ...await this._serviceSettings.getAttributesForTMSLayer(baseMapOpts, true, isDarkMode), }; if (!baseMapOpts) { this.onWarn(i18n.translate('vega.mapView.mapStyleNotFoundWarningMessage', { diff --git a/src/legacy/core_plugins/vega/public/vega_visualization.js b/src/legacy/core_plugins/vega/public/vega_visualization.js index a35da1655368e..be97789fe82be 100644 --- a/src/legacy/core_plugins/vega/public/vega_visualization.js +++ b/src/legacy/core_plugins/vega/public/vega_visualization.js @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { toastNotifications, Notifier } from 'ui/notify'; +import { toastNotifications } from 'ui/notify'; import { VegaView } from './vega_view/vega_view'; import { VegaMapView } from './vega_view/vega_map_view'; import { SavedObjectsClientProvider, findObjectByTitle } from 'ui/saved_objects'; @@ -27,7 +27,6 @@ import { SavedObjectsClientProvider, findObjectByTitle } from 'ui/saved_objects' export function VegaVisualizationProvider(Private, vegaConfig, serviceSettings, $rootScope) { const savedObjectsClient = Private(SavedObjectsClientProvider); - const notify = new Notifier({ location: 'Vega' }); return class VegaVisualization { constructor(el, vis) { @@ -83,7 +82,11 @@ export function VegaVisualizationProvider(Private, vegaConfig, serviceSettings, if (this._vegaView) { this._vegaView.onError(error); } else { - notify.error(error); + toastNotifications.addError(error, { + title: i18n.translate('vega.visualization.renderErrorTitle', { + defaultMessage: 'Vega error', + }), + }); } } } diff --git a/src/legacy/core_plugins/visualizations/index.ts b/src/legacy/core_plugins/visualizations/index.ts new file mode 100644 index 0000000000000..bb9ef1588bdc2 --- /dev/null +++ b/src/legacy/core_plugins/visualizations/index.ts @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { resolve } from 'path'; +import { Legacy } from '../../../../kibana'; + +// eslint-disable-next-line import/no-default-export +export default function VisualizationsPlugin(kibana: any) { + const config: Legacy.PluginSpecOptions = { + id: 'visualizations', + require: ['data'], + publicDir: resolve(__dirname, 'public'), + config: (Joi: any) => { + return Joi.object({ + enabled: Joi.boolean().default(true), + }).default(); + }, + init: (server: Legacy.Server) => ({}), + }; + + return new kibana.Plugin(config); +} diff --git a/src/legacy/core_plugins/visualizations/package.json b/src/legacy/core_plugins/visualizations/package.json new file mode 100644 index 0000000000000..5b436f0c2fef2 --- /dev/null +++ b/src/legacy/core_plugins/visualizations/package.json @@ -0,0 +1,4 @@ +{ + "name": "visualizations", + "version": "kibana" +} diff --git a/src/legacy/core_plugins/visualizations/public/filters/filters_service.ts b/src/legacy/core_plugins/visualizations/public/filters/filters_service.ts new file mode 100644 index 0000000000000..60c26d7cbdc1d --- /dev/null +++ b/src/legacy/core_plugins/visualizations/public/filters/filters_service.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// @ts-ignore +import { VisFiltersProvider, createFilter } from 'ui/vis/vis_filters'; + +/** + * Vis Filters Service + * + * @internal + */ +export class FiltersService { + public setup() { + return { + VisFiltersProvider, + createFilter, + }; + } + + public stop() { + // nothing to do here yet + } +} + +/** @public */ +export type FiltersSetup = ReturnType; diff --git a/src/legacy/core_plugins/visualizations/public/filters/index.ts b/src/legacy/core_plugins/visualizations/public/filters/index.ts new file mode 100644 index 0000000000000..591f7c9bc7715 --- /dev/null +++ b/src/legacy/core_plugins/visualizations/public/filters/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { FiltersService, FiltersSetup } from './filters_service'; diff --git a/src/legacy/core_plugins/visualizations/public/index.ts b/src/legacy/core_plugins/visualizations/public/index.ts new file mode 100644 index 0000000000000..202c2354f89eb --- /dev/null +++ b/src/legacy/core_plugins/visualizations/public/index.ts @@ -0,0 +1,68 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FiltersService, FiltersSetup } from './filters'; +import { TypesService, TypesSetup } from './types'; + +class VisualizationsPlugin { + private readonly filters: FiltersService; + private readonly types: TypesService; + + constructor() { + this.filters = new FiltersService(); + this.types = new TypesService(); + } + + public setup() { + return { + filters: this.filters.setup(), + types: this.types.setup(), + }; + } + + public stop() { + this.filters.stop(); + this.types.stop(); + } +} + +/** + * We export visualizations here so that users importing from 'plugins/visualizations' + * will automatically receive the response value of the `setup` contract, mimicking + * the data that will eventually be injected by the new platform. + */ +export const visualizations = new VisualizationsPlugin().setup(); + +/** @public */ +export interface VisualizationsSetup { + filters: FiltersSetup; + types: TypesSetup; +} + +/** @public types */ +export { + Vis, + VisParams, + VisProvider, + VisState, + VisualizationController, + VisType, + VisTypesRegistry, + Status, +} from './types'; diff --git a/src/legacy/core_plugins/visualizations/public/types/index.ts b/src/legacy/core_plugins/visualizations/public/types/index.ts new file mode 100644 index 0000000000000..a7830a8eb9704 --- /dev/null +++ b/src/legacy/core_plugins/visualizations/public/types/index.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { + TypesService, + // types + TypesSetup, + Vis, + VisParams, + VisProvider, + VisState, + VisualizationController, + VisType, + VisTypesRegistry, + Status, +} from './types_service'; diff --git a/src/legacy/core_plugins/visualizations/public/types/types_service.ts b/src/legacy/core_plugins/visualizations/public/types/types_service.ts new file mode 100644 index 0000000000000..82ab0ceb00baf --- /dev/null +++ b/src/legacy/core_plugins/visualizations/public/types/types_service.ts @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// @ts-ignore +import { defaultFeedbackMessage } from 'ui/vis/default_feedback_message'; +// @ts-ignore +import { VisProvider as Vis } from 'ui/vis/index.js'; +// @ts-ignore +import { VisFactoryProvider as VisFactory } from 'ui/vis/vis_factory'; +import { VisTypesRegistryProvider } from 'ui/registry/vis_types'; + +/** + * Vis Types Service + * + * @internal + */ +export class TypesService { + public setup() { + return { + Vis, + VisFactory, + VisTypesRegistryProvider, + defaultFeedbackMessage, // make default in base vis type, or move? + }; + } + + public stop() { + // nothing to do here yet + } +} + +/** @public */ +export type TypesSetup = ReturnType; + +/** @public types */ +import * as types from 'ui/vis/vis'; +export type Vis = types.Vis; +export type VisParams = types.VisParams; +export type VisProvider = types.VisProvider; +export type VisState = types.VisState; +export { VisualizationController, VisType } from 'ui/vis/vis_types/vis_type'; +export { VisTypesRegistry } from 'ui/registry/vis_types'; +export { Status } from 'ui/vis/update_status'; diff --git a/src/legacy/plugin_discovery/types.ts b/src/legacy/plugin_discovery/types.ts index 632a295036d8a..d7cc1aff7fa1e 100644 --- a/src/legacy/plugin_discovery/types.ts +++ b/src/legacy/plugin_discovery/types.ts @@ -64,6 +64,8 @@ export interface LegacyPluginOptions { home: string[]; mappings: any; savedObjectSchemas: SavedObjectsSchemaDefinition; + embeddableActions?: string[]; + embeddableFactories?: string[]; }>; uiCapabilities?: Capabilities; publicDir: any; diff --git a/src/legacy/server/capabilities/capabilities_mixin.test.ts b/src/legacy/server/capabilities/capabilities_mixin.test.ts index 7665c28797884..9b6827e1bb380 100644 --- a/src/legacy/server/capabilities/capabilities_mixin.test.ts +++ b/src/legacy/server/capabilities/capabilities_mixin.test.ts @@ -85,4 +85,15 @@ describe('capabilitiesMixin', () => { expect(mockRegisterCapabilitiesRoute.mock.calls[0][2]).toEqual([mockModifier1, mockModifier2]); }); + + it('exposes request#getCapabilities for retrieving legacy capabilities', async () => { + const kbnServer = getKbnServer(); + jest.spyOn(server, 'decorate'); + await capabilitiesMixin(kbnServer, server); + expect(server.decorate).toHaveBeenCalledWith( + 'request', + 'getCapabilities', + expect.any(Function) + ); + }); }); diff --git a/src/legacy/server/capabilities/capabilities_mixin.ts b/src/legacy/server/capabilities/capabilities_mixin.ts index d99568c27cba5..b41dfe42c40b2 100644 --- a/src/legacy/server/capabilities/capabilities_mixin.ts +++ b/src/legacy/server/capabilities/capabilities_mixin.ts @@ -23,6 +23,7 @@ import { Capabilities } from '../../../core/public'; import KbnServer from '../kbn_server'; import { registerCapabilitiesRoute } from './capabilities_route'; import { mergeCapabilities } from './merge_capabilities'; +import { resolveCapabilities } from './resolve_capabilities'; export type CapabilitiesModifier = ( request: Request, @@ -48,6 +49,19 @@ export async function capabilitiesMixin(kbnServer: KbnServer, server: Server) { )) ); + server.decorate('request', 'getCapabilities', function() { + // Get legacy nav links + const navLinks = server.getUiNavLinks().reduce( + (acc, spec) => ({ + ...acc, + [spec._id]: true, + }), + {} as Record + ); + + return resolveCapabilities(this, modifiers, defaultCapabilities, { navLinks }); + }); + registerCapabilitiesRoute(server, defaultCapabilities, modifiers); }); } diff --git a/src/legacy/server/capabilities/capabilities_route.ts b/src/legacy/server/capabilities/capabilities_route.ts index fe72bdf03310b..5564fbb295a62 100644 --- a/src/legacy/server/capabilities/capabilities_route.ts +++ b/src/legacy/server/capabilities/capabilities_route.ts @@ -20,9 +20,9 @@ import Joi from 'joi'; import { Server } from 'hapi'; -import { CapabilitiesModifier } from '.'; import { Capabilities } from '../../../core/public'; -import { mergeCapabilities } from './merge_capabilities'; +import { CapabilitiesModifier } from './capabilities_mixin'; +import { resolveCapabilities } from './resolve_capabilities'; export const registerCapabilitiesRoute = ( server: Server, @@ -40,15 +40,14 @@ export const registerCapabilitiesRoute = ( }, }, async handler(request) { - let { capabilities } = request.payload as { capabilities: Capabilities }; - capabilities = mergeCapabilities({ ...defaultCapabilities }, capabilities); - - for (const provider of modifiers) { - capabilities = await provider(request, capabilities); - } - + const { capabilities } = request.payload as { capabilities: Capabilities }; return { - capabilities, + capabilities: await resolveCapabilities( + request, + modifiers, + defaultCapabilities, + capabilities + ), }; }, }); diff --git a/src/legacy/server/capabilities/merge_capabilities.ts b/src/legacy/server/capabilities/merge_capabilities.ts index 1c99647030f88..e0f2d22ab34ad 100644 --- a/src/legacy/server/capabilities/merge_capabilities.ts +++ b/src/legacy/server/capabilities/merge_capabilities.ts @@ -19,10 +19,10 @@ import { Capabilities } from '../../../core/public'; -export const mergeCapabilities = (...sources: Capabilities[]): Capabilities => +export const mergeCapabilities = (...sources: Array>): Capabilities => sources.reduce( - (capabilities, source) => { - Object.entries(source).forEach(([key, value]) => { + (capabilities: Capabilities, source) => { + Object.entries(source).forEach(([key, value = {}]) => { capabilities[key] = { ...value, ...capabilities[key], diff --git a/src/legacy/server/capabilities/resolve_capabilities.ts b/src/legacy/server/capabilities/resolve_capabilities.ts new file mode 100644 index 0000000000000..0df4932099b54 --- /dev/null +++ b/src/legacy/server/capabilities/resolve_capabilities.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Request } from 'hapi'; + +import { Capabilities } from '../../../core/public'; +import { mergeCapabilities } from './merge_capabilities'; +import { CapabilitiesModifier } from './capabilities_mixin'; + +export const resolveCapabilities = ( + request: Request, + modifiers: CapabilitiesModifier[], + ...capabilities: Array> +) => + modifiers.reduce( + async (resolvedCaps, modifier) => modifier(request, await resolvedCaps), + Promise.resolve(mergeCapabilities(...capabilities)) + ); diff --git a/src/legacy/server/config/complete.js b/src/legacy/server/config/complete.js index fad8bcbe657cd..4100a6b5a9bdf 100644 --- a/src/legacy/server/config/complete.js +++ b/src/legacy/server/config/complete.js @@ -21,6 +21,8 @@ import { difference, get, set } from 'lodash'; import { transformDeprecations } from './transform_deprecations'; import { unset, formatListAsProse, getFlattenedObject } from '../../utils'; import { getTransform } from '../../deprecation'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { hasConfigPathIntersection } from '../../../core/server/config/'; const getFlattenedKeys = object => Object.keys(getFlattenedObject(object)); @@ -68,7 +70,7 @@ async function getUnusedConfigKeys( return difference(inputKeys, appliedKeys).filter( unusedConfigKey => !coreHandledConfigPaths.some(usedInCoreConfigKey => - unusedConfigKey.startsWith(usedInCoreConfigKey) + hasConfigPathIntersection(unusedConfigKey, usedInCoreConfigKey) ) ); } diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index 23d1e2067f00e..2bf31ee143b73 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -175,6 +175,10 @@ export default () => Joi.object({ pollInterval: Joi.number().default(1500), }).default(), + stats: Joi.object({ + maximumWaitTimeForAllCollectorsInS: Joi.number().default(60) + }).default(), + optimize: Joi.object({ enabled: Joi.boolean().default(true), bundleFilter: Joi.string().default('!tests'), @@ -205,6 +209,7 @@ export default () => Joi.object({ }).default(), map: Joi.object({ includeElasticMapsService: Joi.boolean().default(true), + proxyElasticMapsServiceInMaps: Joi.boolean().default(false), tilemap: Joi.object({ url: Joi.string(), options: Joi.object({ @@ -244,8 +249,17 @@ export default () => Joi.object({ })) })).default([]) }).default(), - manifestServiceUrl: Joi.string().default('https://catalogue.maps.elastic.co/v7.0/manifest'), - emsLandingPageUrl: Joi.string().default('https://maps.elastic.co/v7.0'), + manifestServiceUrl: Joi.string().default('https://catalogue.maps.elastic.co/v7.2/manifest'), + emsLandingPageUrl: Joi.string().default('https://maps.elastic.co/v7.2'), + emsTileLayerId: Joi.object({ + bright: Joi.string().default('road_map'), + desaturated: Joi.string().default('road_map_desaturated'), + dark: Joi.string().default('dark_map'), + }).default({ + bright: 'road_map', + desaturated: 'road_map_desaturated', + dark: 'dark_map', + }), }).default(), i18n: Joi.object({ diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index c104ef3361bd0..c86b967019b0a 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -21,12 +21,10 @@ import { ResponseObject, Server } from 'hapi'; import { ElasticsearchServiceSetup, - HttpServiceSetup, - HttpServiceStart, ConfigService, LoggerFactory, - PluginsServiceSetup, - PluginsServiceStart, + InternalCoreSetup, + InternalCoreStart, } from '../../core/server'; import { ApmOssPlugin } from '../core_plugins/apm_oss'; import { CallClusterWithRequest, ElasticsearchPlugin } from '../core_plugins/elasticsearch'; @@ -39,6 +37,7 @@ import { SavedObjectsSchema, SavedObjectsManagement, } from './saved_objects'; +import { Capabilities } from '../../core/public'; export interface KibanaConfig { get(key: string): T; @@ -71,12 +70,15 @@ declare module 'hapi' { scopedTutorialContextFactory: (...args: any[]) => any ) => void; savedObjectsManagement(): SavedObjectsManagement; + getInjectedUiAppVars: (pluginName: string) => { [key: string]: any }; + getUiNavLinks(): Array<{ _id: string }>; } interface Request { getSavedObjectsClient(): SavedObjectsClient; getBasePath(): string; getUiSettingsService(): any; + getCapabilities(): Promise; } interface ResponseToolkit { @@ -93,17 +95,12 @@ export default class KbnServer { logger: LoggerFactory; }; setup: { - core: { - elasticsearch: ElasticsearchServiceSetup; - http: HttpServiceSetup; - }; - plugins: PluginsServiceSetup; + core: InternalCoreSetup; + plugins: Record; }; start: { - core: { - http: HttpServiceStart; - }; - plugins: PluginsServiceStart; + core: InternalCoreStart; + plugins: Record; }; stop: null; params: { diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js index 478d1c0ff7238..78a8829dbb08a 100644 --- a/src/legacy/server/kbn_server.js +++ b/src/legacy/server/kbn_server.js @@ -36,7 +36,7 @@ import configCompleteMixin from './config/complete'; import optimizeMixin from '../../optimize'; import * as Plugins from './plugins'; import { indexPatternsMixin } from './index_patterns'; -import { savedObjectsMixin } from './saved_objects'; +import { savedObjectsMixin } from './saved_objects/saved_objects_mixin'; import { sampleDataMixin } from './sample_data'; import { capabilitiesMixin } from './capabilities'; import { urlShorteningMixin } from './url_shortening'; @@ -60,19 +60,8 @@ export default class KbnServer { coreContext: { logger, }, - setup: { - core: { - elasticsearch: setupDeps.elasticsearch, - http: setupDeps.http, - }, - plugins: setupDeps.plugins, - }, - start: { - core: { - http: startDeps.http, - }, - plugins: startDeps.plugins, - }, + setup: setupDeps, + start: startDeps, stop: null, params: { serverOptions, diff --git a/src/legacy/server/sample_data/data_sets/ecommerce/saved_objects.js b/src/legacy/server/sample_data/data_sets/ecommerce/saved_objects.js index e2be84b14e7a5..6d5bef9f191e1 100644 --- a/src/legacy/server/sample_data/data_sets/ecommerce/saved_objects.js +++ b/src/legacy/server/sample_data/data_sets/ecommerce/saved_objects.js @@ -109,7 +109,7 @@ export const getSavedObjects = () => [ "title": i18n.translate('server.sampleData.ecommerceSpec.promotionTrackingTitle', { defaultMessage: '[eCommerce] Promotion Tracking', }), - "visState": "{\"title\":\"[eCommerce] Promotion Tracking\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"timeseries\",\"series\":[{\"id\":\"ea20ae70-b88d-11e8-a451-f37365e9f268\",\"color\":\"rgba(240,138,217,1)\",\"split_mode\":\"everything\",\"metrics\":[{\"id\":\"ea20ae71-b88d-11e8-a451-f37365e9f268\",\"type\":\"sum\",\"field\":\"taxful_total_price\"}],\"separate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":\"0.7\",\"stacked\":\"none\",\"filter\":\"products.product_name:*trouser*\",\"label\":\"Revenue Trousers\",\"value_template\":\"${{value}}\"},{\"id\":\"062d77b0-b88e-11e8-a451-f37365e9f268\",\"color\":\"rgba(191,240,129,1)\",\"split_mode\":\"everything\",\"metrics\":[{\"id\":\"062d77b1-b88e-11e8-a451-f37365e9f268\",\"type\":\"sum\",\"field\":\"taxful_total_price\"}],\"separate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":\"0.7\",\"stacked\":\"none\",\"filter\":\"products.product_name:*watch*\",\"label\":\"Revenue Watches\",\"value_template\":\"${{value}}\"},{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"rgba(23,233,230,1)\",\"split_mode\":\"everything\",\"metrics\":[{\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"sum\",\"field\":\"taxful_total_price\"}],\"separate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":\"0.7\",\"stacked\":\"none\",\"filter\":\"products.product_name:*bag*\",\"label\":\"Revenue Bags\",\"value_template\":\"${{value}}\"},{\"id\":\"faa2c170-b88d-11e8-a451-f37365e9f268\",\"color\":\"rgba(235,186,180,1)\",\"split_mode\":\"everything\",\"metrics\":[{\"id\":\"faa2c171-b88d-11e8-a451-f37365e9f268\",\"type\":\"sum\",\"field\":\"taxful_total_price\"}],\"separate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":\"0.7\",\"stacked\":\"none\",\"filter\":\"products.product_name:*cocktail dress*\",\"label\":\"Revenue Cocktail Dresses\",\"value_template\":\"${{value}}\"}],\"time_field\":\"order_date\",\"index_pattern\":\"kibana_sample_data_ecommerce\",\"interval\":\">=12h\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"axis_scale\":\"normal\",\"show_legend\":1,\"show_grid\":1,\"legend_position\":\"bottom\",\"annotations\":[{\"fields\":\"taxful_total_price\",\"template\":\"Ring the bell! ${{taxful_total_price}}\",\"index_pattern\":\"kibana_sample_data_ecommerce\",\"query_string\":\"taxful_total_price:>250\",\"id\":\"c8c30be0-b88f-11e8-a451-f37365e9f268\",\"color\":\"rgba(25,77,51,1)\",\"time_field\":\"order_date\",\"icon\":\"fa-bell\",\"ignore_global_filters\":1,\"ignore_panel_filters\":1}]},\"aggs\":[]}", + "visState": "{\"title\":\"[eCommerce] Promotion Tracking\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"timeseries\",\"series\":[{\"id\":\"ea20ae70-b88d-11e8-a451-f37365e9f268\",\"color\":\"rgba(240,138,217,1)\",\"split_mode\":\"everything\",\"metrics\":[{\"id\":\"ea20ae71-b88d-11e8-a451-f37365e9f268\",\"type\":\"sum\",\"field\":\"taxful_total_price\"}],\"separate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":\"0.7\",\"stacked\":\"none\",\"filter\":{\"query\":\"products.product_name:*trouser*\", \"language\":\"kuery\"},\"label\":\"Revenue Trousers\",\"value_template\":\"${{value}}\"},{\"id\":\"062d77b0-b88e-11e8-a451-f37365e9f268\",\"color\":\"rgba(191,240,129,1)\",\"split_mode\":\"everything\",\"metrics\":[{\"id\":\"062d77b1-b88e-11e8-a451-f37365e9f268\",\"type\":\"sum\",\"field\":\"taxful_total_price\"}],\"separate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":\"0.7\",\"stacked\":\"none\",\"filter\":{\"query\":\"products.product_name:*watch*\", \"language\":\"kuery\"},\"label\":\"Revenue Watches\",\"value_template\":\"${{value}}\"},{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"rgba(23,233,230,1)\",\"split_mode\":\"everything\",\"metrics\":[{\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"sum\",\"field\":\"taxful_total_price\"}],\"separate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":\"0.7\",\"stacked\":\"none\",\"filter\":{\"query\":\"products.product_name:*bag*\", \"language\":\"kuery\"},\"label\":\"Revenue Bags\",\"value_template\":\"${{value}}\"},{\"id\":\"faa2c170-b88d-11e8-a451-f37365e9f268\",\"color\":\"rgba(235,186,180,1)\",\"split_mode\":\"everything\",\"metrics\":[{\"id\":\"faa2c171-b88d-11e8-a451-f37365e9f268\",\"type\":\"sum\",\"field\":\"taxful_total_price\"}],\"separate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":\"0.7\",\"stacked\":\"none\",\"filter\":{\"query\":\"products.product_name:*cocktail dress*\", \"language\":\"kuery\"},\"label\":\"Revenue Cocktail Dresses\",\"value_template\":\"${{value}}\"}],\"time_field\":\"order_date\",\"index_pattern\":\"kibana_sample_data_ecommerce\",\"interval\":\">=12h\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"axis_scale\":\"normal\",\"show_legend\":1,\"show_grid\":1,\"legend_position\":\"bottom\",\"annotations\":[{\"fields\":\"taxful_total_price\",\"template\":\"Ring the bell! ${{taxful_total_price}}\",\"index_pattern\":\"kibana_sample_data_ecommerce\",\"query_string\":{\"query\":\"taxful_total_price > 250\", \"language\":\"kuery\"},\"id\":\"c8c30be0-b88f-11e8-a451-f37365e9f268\",\"color\":\"rgba(25,77,51,1)\",\"time_field\":\"order_date\",\"icon\":\"fa-bell\",\"ignore_global_filters\":1,\"ignore_panel_filters\":1}]},\"aggs\":[]}", "uiStateJSON": "{}", "description": "", "version": 1, @@ -147,7 +147,7 @@ export const getSavedObjects = () => [ "title": i18n.translate('server.sampleData.ecommerceSpec.soldProductsPerDayTitle', { defaultMessage: '[eCommerce] Sold Products per Day', }), - "visState": "{\"title\":\"[eCommerce] Sold Products per Day\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"gauge\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"everything\",\"metrics\":[{\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"count\"}],\"separate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"label\":\"Trxns / day\"}],\"time_field\":\"order_date\",\"index_pattern\":\"kibana_sample_data_ecommerce\",\"interval\":\"1d\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"axis_scale\":\"normal\",\"show_legend\":1,\"show_grid\":1,\"gauge_color_rules\":[{\"value\":150,\"id\":\"6da070c0-b891-11e8-b645-195edeb9de84\",\"gauge\":\"rgba(104,188,0,1)\",\"operator\":\"gte\"},{\"value\":150,\"id\":\"9b0cdbc0-b891-11e8-b645-195edeb9de84\",\"gauge\":\"rgba(244,78,59,1)\",\"operator\":\"lt\"}],\"gauge_width\":\"15\",\"gauge_inner_width\":10,\"gauge_style\":\"half\",\"filter\":\"\",\"gauge_max\":\"300\"},\"aggs\":[]}", + "visState": "{\"title\":\"[eCommerce] Sold Products per Day\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"gauge\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"everything\",\"metrics\":[{\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"count\"}],\"separate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"label\":\"Trxns / day\"}],\"time_field\":\"order_date\",\"index_pattern\":\"kibana_sample_data_ecommerce\",\"interval\":\"1d\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"axis_scale\":\"normal\",\"show_legend\":1,\"show_grid\":1,\"gauge_color_rules\":[{\"value\":150,\"id\":\"6da070c0-b891-11e8-b645-195edeb9de84\",\"gauge\":\"rgba(104,188,0,1)\",\"operator\":\"gte\"},{\"value\":150,\"id\":\"9b0cdbc0-b891-11e8-b645-195edeb9de84\",\"gauge\":\"rgba(244,78,59,1)\",\"operator\":\"lt\"}],\"gauge_width\":\"15\",\"gauge_inner_width\":10,\"gauge_style\":\"half\",\"filter\":{\"query\":\"\",\"language\":\"kuery\"},\"gauge_max\":\"300\"},\"aggs\":[]}", "uiStateJSON": "{}", "description": "", "version": 1, @@ -166,7 +166,7 @@ export const getSavedObjects = () => [ "title": i18n.translate('server.sampleData.ecommerceSpec.averageSalesPriceTitle', { defaultMessage: '[eCommerce] Average Sales Price', }), - "visState": "{\"title\":\"[eCommerce] Average Sales Price\",\"type\":\"gauge\",\"params\":{\"type\":\"gauge\",\"addTooltip\":true,\"addLegend\":true,\"isDisplayWarning\":false,\"gauge\":{\"verticalSplit\":false,\"extendRange\":true,\"percentageMode\":false,\"gaugeType\":\"Circle\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"colorSchema\":\"Green to Red\",\"gaugeColorMode\":\"Labels\",\"colorsRange\":[{\"from\":0,\"to\":50},{\"from\":50,\"to\":75},{\"from\":75,\"to\":100}],\"invertColors\":true,\"labels\":{\"show\":true,\"color\":\"black\"},\"scale\":{\"show\":false,\"labels\":false,\"color\":\"#333\"},\"type\":\"meter\",\"style\":{\"bgWidth\":0.9,\"width\":0.9,\"mask\":false,\"bgMask\":false,\"maskBars\":50,\"bgFill\":\"#eee\",\"bgColor\":false,\"subText\":\"per order\",\"fontSize\":60,\"labelColor\":true},\"minAngle\":0,\"maxAngle\":6.283185307179586}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"avg\",\"schema\":\"metric\",\"params\":{\"field\":\"taxful_total_price\",\"customLabel\":\"average spend\"}}]}", + "visState": "{\"title\":\"[eCommerce] Average Sales Price\",\"type\":\"gauge\",\"params\":{\"type\":\"gauge\",\"addTooltip\":true,\"addLegend\":true,\"isDisplayWarning\":false,\"gauge\":{\"alignment\":\"automatic\",\"extendRange\":true,\"percentageMode\":false,\"gaugeType\":\"Circle\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"colorSchema\":\"Green to Red\",\"gaugeColorMode\":\"Labels\",\"colorsRange\":[{\"from\":0,\"to\":50},{\"from\":50,\"to\":75},{\"from\":75,\"to\":100}],\"invertColors\":true,\"labels\":{\"show\":true,\"color\":\"black\"},\"scale\":{\"show\":false,\"labels\":false,\"color\":\"#333\"},\"type\":\"meter\",\"style\":{\"bgWidth\":0.9,\"width\":0.9,\"mask\":false,\"bgMask\":false,\"maskBars\":50,\"bgFill\":\"#eee\",\"bgColor\":false,\"subText\":\"per order\",\"fontSize\":60,\"labelColor\":true},\"minAngle\":0,\"maxAngle\":6.283185307179586}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"avg\",\"schema\":\"metric\",\"params\":{\"field\":\"taxful_total_price\",\"customLabel\":\"average spend\"}}]}", "uiStateJSON": "{\"vis\":{\"defaultColors\":{\"0 - 50\":\"rgb(165,0,38)\",\"50 - 75\":\"rgb(255,255,190)\",\"75 - 100\":\"rgb(0,104,55)\"}}}", "description": "", "version": 1, @@ -185,7 +185,7 @@ export const getSavedObjects = () => [ "title": i18n.translate('server.sampleData.ecommerceSpec.averageSoldQuantityTitle', { defaultMessage: '[eCommerce] Average Sold Quantity', }), - "visState": "{\"title\":\"[eCommerce] Average Sold Quantity\",\"type\":\"gauge\",\"params\":{\"type\":\"gauge\",\"addTooltip\":true,\"addLegend\":true,\"isDisplayWarning\":false,\"gauge\":{\"verticalSplit\":false,\"extendRange\":true,\"percentageMode\":false,\"gaugeType\":\"Circle\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"colorSchema\":\"Green to Red\",\"gaugeColorMode\":\"Labels\",\"colorsRange\":[{\"from\":0,\"to\":2},{\"from\":2,\"to\":3},{\"from\":3,\"to\":4}],\"invertColors\":true,\"labels\":{\"show\":true,\"color\":\"black\"},\"scale\":{\"show\":false,\"labels\":false,\"color\":\"#333\"},\"type\":\"meter\",\"style\":{\"bgWidth\":0.9,\"width\":0.9,\"mask\":false,\"bgMask\":false,\"maskBars\":50,\"bgFill\":\"#eee\",\"bgColor\":false,\"subText\":\"per order\",\"fontSize\":60,\"labelColor\":true},\"minAngle\":0,\"maxAngle\":6.283185307179586}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"avg\",\"schema\":\"metric\",\"params\":{\"field\":\"total_quantity\",\"customLabel\":\"average items\"}}]}", + "visState": "{\"title\":\"[eCommerce] Average Sold Quantity\",\"type\":\"gauge\",\"params\":{\"type\":\"gauge\",\"addTooltip\":true,\"addLegend\":true,\"isDisplayWarning\":false,\"gauge\":{\"alignment\":\"automatic\",\"extendRange\":true,\"percentageMode\":false,\"gaugeType\":\"Circle\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"colorSchema\":\"Green to Red\",\"gaugeColorMode\":\"Labels\",\"colorsRange\":[{\"from\":0,\"to\":2},{\"from\":2,\"to\":3},{\"from\":3,\"to\":4}],\"invertColors\":true,\"labels\":{\"show\":true,\"color\":\"black\"},\"scale\":{\"show\":false,\"labels\":false,\"color\":\"#333\"},\"type\":\"meter\",\"style\":{\"bgWidth\":0.9,\"width\":0.9,\"mask\":false,\"bgMask\":false,\"maskBars\":50,\"bgFill\":\"#eee\",\"bgColor\":false,\"subText\":\"per order\",\"fontSize\":60,\"labelColor\":true},\"minAngle\":0,\"maxAngle\":6.283185307179586}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"avg\",\"schema\":\"metric\",\"params\":{\"field\":\"total_quantity\",\"customLabel\":\"average items\"}}]}", "uiStateJSON": "{\"vis\":{\"defaultColors\":{\"0 - 2\":\"rgb(165,0,38)\",\"2 - 3\":\"rgb(255,255,190)\",\"3 - 4\":\"rgb(0,104,55)\"}}}", "description": "", "version": 1, diff --git a/src/legacy/server/sample_data/data_sets/flights/flights.json.gz b/src/legacy/server/sample_data/data_sets/flights/flights.json.gz index caaa7eb2d0452..1f6e10099416a 100644 Binary files a/src/legacy/server/sample_data/data_sets/flights/flights.json.gz and b/src/legacy/server/sample_data/data_sets/flights/flights.json.gz differ diff --git a/src/legacy/server/sample_data/data_sets/flights/saved_objects.js b/src/legacy/server/sample_data/data_sets/flights/saved_objects.js index 1e8a355d331df..fdc03dd36e183 100644 --- a/src/legacy/server/sample_data/data_sets/flights/saved_objects.js +++ b/src/legacy/server/sample_data/data_sets/flights/saved_objects.js @@ -142,7 +142,7 @@ export const getSavedObjects = () => [ "title": i18n.translate('server.sampleData.flightsSpec.delaysAndCancellationsTitle', { defaultMessage: '[Flights] Delays & Cancellations', }), - "visState": "{\"title\":\"[Flights] Delays & Cancellations\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"timeseries\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"rgba(0,156,224,1)\",\"split_mode\":\"everything\",\"metrics\":[{\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"filter_ratio\",\"numerator\":\"FlightDelay:true\"}],\"separate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"percent\",\"chart_type\":\"line\",\"line_width\":\"2\",\"point_size\":\"0\",\"fill\":0.5,\"stacked\":\"none\",\"label\":\"Percent Delays\"}],\"time_field\":\"timestamp\",\"index_pattern\":\"kibana_sample_data_flights\",\"interval\":\">=1h\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1,\"annotations\":[{\"fields\":\"FlightDelay,Cancelled,Carrier\",\"template\":\"{{Carrier}}: Flight Delayed and Cancelled!\",\"index_pattern\":\"kibana_sample_data_flights\",\"query_string\":\"FlightDelay:true AND Cancelled:true\",\"id\":\"53b7dff0-4c89-11e8-a66a-6989ad5a0a39\",\"color\":\"rgba(0,98,177,1)\",\"time_field\":\"timestamp\",\"icon\":\"fa-exclamation-triangle\",\"ignore_global_filters\":1,\"ignore_panel_filters\":1}],\"legend_position\":\"bottom\"},\"aggs\":[]}", + "visState": "{\"title\":\"[Flights] Delays & Cancellations\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"timeseries\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"rgba(0,156,224,1)\",\"split_mode\":\"everything\",\"metrics\":[{\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"filter_ratio\",\"numerator\":\"FlightDelay:true\"}],\"separate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"percent\",\"chart_type\":\"line\",\"line_width\":\"2\",\"point_size\":\"0\",\"fill\":0.5,\"stacked\":\"none\",\"label\":\"Percent Delays\"}],\"time_field\":\"timestamp\",\"index_pattern\":\"kibana_sample_data_flights\",\"interval\":\">=1h\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1,\"annotations\":[{\"fields\":\"FlightDelay,Cancelled,Carrier\",\"template\":\"{{Carrier}}: Flight Delayed and Cancelled!\",\"index_pattern\":\"kibana_sample_data_flights\",\"query_string\":{\"query\":\"FlightDelay:true AND Cancelled:true\", \"language\":\"kuery\"},\"id\":\"53b7dff0-4c89-11e8-a66a-6989ad5a0a39\",\"color\":\"rgba(0,98,177,1)\",\"time_field\":\"timestamp\",\"icon\":\"fa-exclamation-triangle\",\"ignore_global_filters\":1,\"ignore_panel_filters\":1}],\"legend_position\":\"bottom\"},\"aggs\":[]}", "uiStateJSON": "{}", "description": "", "version": 1, @@ -294,7 +294,7 @@ export const getSavedObjects = () => [ "title": i18n.translate('server.sampleData.flightsSpec.totalFlightDelaysTitle', { defaultMessage: '[Flights] Total Flight Delays', }), - "visState": "{\"title\":\"[Flights] Total Flight Delays\",\"type\":\"gauge\",\"params\":{\"type\":\"gauge\",\"addTooltip\":true,\"addLegend\":true,\"isDisplayWarning\":false,\"gauge\":{\"verticalSplit\":false,\"extendRange\":true,\"percentageMode\":false,\"gaugeType\":\"Arc\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"colorSchema\":\"Blues\",\"gaugeColorMode\":\"Labels\",\"colorsRange\":[{\"from\":0,\"to\":75},{\"from\":75,\"to\":150},{\"from\":150,\"to\":225},{\"from\":225,\"to\":300}],\"invertColors\":true,\"labels\":{\"show\":false,\"color\":\"black\"},\"scale\":{\"show\":false,\"labels\":false,\"color\":\"#333\"},\"type\":\"meter\",\"style\":{\"bgWidth\":0.9,\"width\":0.9,\"mask\":false,\"bgMask\":false,\"maskBars\":50,\"bgFill\":\"#eee\",\"bgColor\":false,\"subText\":\"\",\"fontSize\":60,\"labelColor\":true}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{\"customLabel\":\"Total Delays\"}}]}", + "visState": "{\"title\":\"[Flights] Total Flight Delays\",\"type\":\"gauge\",\"params\":{\"type\":\"gauge\",\"addTooltip\":true,\"addLegend\":true,\"isDisplayWarning\":false,\"gauge\":{\"alignment\":\"automatic\",\"extendRange\":true,\"percentageMode\":false,\"gaugeType\":\"Arc\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"colorSchema\":\"Blues\",\"gaugeColorMode\":\"Labels\",\"colorsRange\":[{\"from\":0,\"to\":75},{\"from\":75,\"to\":150},{\"from\":150,\"to\":225},{\"from\":225,\"to\":300}],\"invertColors\":true,\"labels\":{\"show\":false,\"color\":\"black\"},\"scale\":{\"show\":false,\"labels\":false,\"color\":\"#333\"},\"type\":\"meter\",\"style\":{\"bgWidth\":0.9,\"width\":0.9,\"mask\":false,\"bgMask\":false,\"maskBars\":50,\"bgFill\":\"#eee\",\"bgColor\":false,\"subText\":\"\",\"fontSize\":60,\"labelColor\":true}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{\"customLabel\":\"Total Delays\"}}]}", "uiStateJSON": "{\"vis\":{\"defaultColors\":{\"0 - 75\":\"rgb(8,48,107)\",\"75 - 150\":\"rgb(55,135,192)\",\"150 - 225\":\"rgb(171,208,230)\",\"225 - 300\":\"rgb(247,251,255)\"}}}", "description": "", "version": 1, @@ -313,7 +313,7 @@ export const getSavedObjects = () => [ "title": i18n.translate('server.sampleData.flightsSpec.totalFlightCancellationsTitle', { defaultMessage: '[Flights] Total Flight Cancellations', }), - "visState": "{\"title\":\"[Flights] Total Flight Cancellations\",\"type\":\"gauge\",\"params\":{\"type\":\"gauge\",\"addTooltip\":true,\"addLegend\":true,\"isDisplayWarning\":false,\"gauge\":{\"verticalSplit\":false,\"extendRange\":true,\"percentageMode\":false,\"gaugeType\":\"Arc\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"colorSchema\":\"Blues\",\"gaugeColorMode\":\"Labels\",\"colorsRange\":[{\"from\":0,\"to\":75},{\"from\":75,\"to\":150},{\"from\":150,\"to\":225},{\"from\":225,\"to\":300}],\"invertColors\":true,\"labels\":{\"show\":false,\"color\":\"black\"},\"scale\":{\"show\":false,\"labels\":false,\"color\":\"#333\"},\"type\":\"meter\",\"style\":{\"bgWidth\":0.9,\"width\":0.9,\"mask\":false,\"bgMask\":false,\"maskBars\":50,\"bgFill\":\"#eee\",\"bgColor\":false,\"subText\":\"\",\"fontSize\":60,\"labelColor\":true}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{\"customLabel\":\"Total Cancellations\"}}]}", + "visState": "{\"title\":\"[Flights] Total Flight Cancellations\",\"type\":\"gauge\",\"params\":{\"type\":\"gauge\",\"addTooltip\":true,\"addLegend\":true,\"isDisplayWarning\":false,\"gauge\":{\"alignment\":\"automatic\",\"extendRange\":true,\"percentageMode\":false,\"gaugeType\":\"Arc\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"colorSchema\":\"Blues\",\"gaugeColorMode\":\"Labels\",\"colorsRange\":[{\"from\":0,\"to\":75},{\"from\":75,\"to\":150},{\"from\":150,\"to\":225},{\"from\":225,\"to\":300}],\"invertColors\":true,\"labels\":{\"show\":false,\"color\":\"black\"},\"scale\":{\"show\":false,\"labels\":false,\"color\":\"#333\"},\"type\":\"meter\",\"style\":{\"bgWidth\":0.9,\"width\":0.9,\"mask\":false,\"bgMask\":false,\"maskBars\":50,\"bgFill\":\"#eee\",\"bgColor\":false,\"subText\":\"\",\"fontSize\":60,\"labelColor\":true}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{\"customLabel\":\"Total Cancellations\"}}]}", "uiStateJSON": "{\"vis\":{\"defaultColors\":{\"0 - 75\":\"rgb(8,48,107)\",\"75 - 150\":\"rgb(55,135,192)\",\"150 - 225\":\"rgb(171,208,230)\",\"225 - 300\":\"rgb(247,251,255)\"}}}", "description": "", "version": 1, diff --git a/src/legacy/server/sample_data/data_sets/logs/logs.json.gz b/src/legacy/server/sample_data/data_sets/logs/logs.json.gz index 3b17db6168c99..7b88d366e1651 100644 Binary files a/src/legacy/server/sample_data/data_sets/logs/logs.json.gz and b/src/legacy/server/sample_data/data_sets/logs/logs.json.gz differ diff --git a/src/legacy/server/sample_data/data_sets/logs/saved_objects.js b/src/legacy/server/sample_data/data_sets/logs/saved_objects.js index 974392d1e08bb..0812ce8e9f711 100644 --- a/src/legacy/server/sample_data/data_sets/logs/saved_objects.js +++ b/src/legacy/server/sample_data/data_sets/logs/saved_objects.js @@ -109,7 +109,7 @@ export const getSavedObjects = () => [ "title": i18n.translate('server.sampleData.logsSpec.goalsTitle', { defaultMessage: '[Logs] Goals', }), - "visState": "{\"title\":\"[Logs] Goals\",\"type\":\"gauge\",\"params\":{\"type\":\"gauge\",\"addTooltip\":true,\"addLegend\":false,\"gauge\":{\"verticalSplit\":false,\"extendRange\":true,\"percentageMode\":false,\"gaugeType\":\"Arc\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"colorSchema\":\"Green to Red\",\"gaugeColorMode\":\"Labels\",\"colorsRange\":[{\"from\":0,\"to\":500},{\"from\":500,\"to\":1000},{\"from\":1000,\"to\":1500}],\"invertColors\":true,\"labels\":{\"show\":false,\"color\":\"black\"},\"scale\":{\"show\":true,\"labels\":false,\"color\":\"#333\"},\"type\":\"meter\",\"style\":{\"bgWidth\":0.9,\"width\":0.9,\"mask\":false,\"bgMask\":false,\"maskBars\":50,\"bgFill\":\"#eee\",\"bgColor\":false,\"subText\":\"visitors\",\"fontSize\":60,\"labelColor\":true}},\"isDisplayWarning\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"cardinality\",\"schema\":\"metric\",\"params\":{\"field\":\"clientip\",\"customLabel\":\"Unique Visitors\"}}]}", + "visState": "{\"title\":\"[Logs] Goals\",\"type\":\"gauge\",\"params\":{\"type\":\"gauge\",\"addTooltip\":true,\"addLegend\":false,\"gauge\":{\"alignment\":\"automatic\",\"extendRange\":true,\"percentageMode\":false,\"gaugeType\":\"Arc\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"colorSchema\":\"Green to Red\",\"gaugeColorMode\":\"Labels\",\"colorsRange\":[{\"from\":0,\"to\":500},{\"from\":500,\"to\":1000},{\"from\":1000,\"to\":1500}],\"invertColors\":true,\"labels\":{\"show\":false,\"color\":\"black\"},\"scale\":{\"show\":true,\"labels\":false,\"color\":\"#333\"},\"type\":\"meter\",\"style\":{\"bgWidth\":0.9,\"width\":0.9,\"mask\":false,\"bgMask\":false,\"maskBars\":50,\"bgFill\":\"#eee\",\"bgColor\":false,\"subText\":\"visitors\",\"fontSize\":60,\"labelColor\":true}},\"isDisplayWarning\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"cardinality\",\"schema\":\"metric\",\"params\":{\"field\":\"clientip\",\"customLabel\":\"Unique Visitors\"}}]}", "uiStateJSON": "{\"vis\":{\"defaultColors\":{\"0 - 500\":\"rgb(165,0,38)\",\"500 - 1000\":\"rgb(255,255,190)\",\"1000 - 1500\":\"rgb(0,104,55)\"},\"colors\":{\"75 - 100\":\"#629E51\",\"50 - 75\":\"#EAB839\",\"0 - 50\":\"#E24D42\",\"0 - 100\":\"#E24D42\",\"200 - 300\":\"#7EB26D\",\"500 - 1000\":\"#E5AC0E\",\"0 - 500\":\"#E24D42\",\"1000 - 1500\":\"#7EB26D\"},\"legendOpen\":true}}", "description": "", "version": 1, @@ -166,7 +166,7 @@ export const getSavedObjects = () => [ "title": i18n.translate('server.sampleData.logsSpec.responseCodesOverTimeTitle', { defaultMessage: '[Logs] Response Codes Over Time + Annotations', }), - "visState": "{\"title\":\"[Logs] Response Codes Over Time + Annotations\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"timeseries\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"rgba(115,216,255,1)\",\"split_mode\":\"terms\",\"metrics\":[{\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"cardinality\",\"field\":\"ip\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"percent\",\"chart_type\":\"line\",\"line_width\":\"2\",\"point_size\":\"0\",\"fill\":\"0.5\",\"stacked\":\"percent\",\"terms_field\":\"response.keyword\",\"terms_order_by\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"label\":\"Response Code Count\",\"split_color_mode\":\"gradient\"}],\"time_field\":\"timestamp\",\"index_pattern\":\"kibana_sample_data_logs\",\"interval\":\">=4h\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1,\"annotations\":[{\"fields\":\"geo.src, host\",\"template\":\"Security Error from {{geo.src}} on {{host}}\",\"index_pattern\":\"kibana_sample_data_logs\",\"query_string\":\"tags:error AND tags:security\",\"id\":\"bd7548a0-2223-11e8-832f-d5027f3c8a47\",\"color\":\"rgba(211,49,21,1)\",\"time_field\":\"timestamp\",\"icon\":\"fa-asterisk\",\"ignore_global_filters\":1,\"ignore_panel_filters\":1}],\"legend_position\":\"bottom\",\"axis_scale\":\"normal\",\"drop_last_bucket\":0},\"aggs\":[]}", + "visState": "{\"title\":\"[Logs] Response Codes Over Time + Annotations\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"timeseries\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"rgba(115,216,255,1)\",\"split_mode\":\"terms\",\"metrics\":[{\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"cardinality\",\"field\":\"ip\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"percent\",\"chart_type\":\"line\",\"line_width\":\"2\",\"point_size\":\"0\",\"fill\":\"0.5\",\"stacked\":\"percent\",\"terms_field\":\"response.keyword\",\"terms_order_by\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"label\":\"Response Code Count\",\"split_color_mode\":\"gradient\"}],\"time_field\":\"timestamp\",\"index_pattern\":\"kibana_sample_data_logs\",\"interval\":\">=4h\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1,\"annotations\":[{\"fields\":\"geo.src, host\",\"template\":\"Security Error from {{geo.src}} on {{host}}\",\"index_pattern\":\"kibana_sample_data_logs\",\"query_string\":{\"query\":\"tags:error AND tags:security\", \"language\":\"kuery\"},\"id\":\"bd7548a0-2223-11e8-832f-d5027f3c8a47\",\"color\":\"rgba(211,49,21,1)\",\"time_field\":\"timestamp\",\"icon\":\"fa-asterisk\",\"ignore_global_filters\":1,\"ignore_panel_filters\":1}],\"legend_position\":\"bottom\",\"axis_scale\":\"normal\",\"drop_last_bucket\":0},\"aggs\":[]}", "uiStateJSON": "{}", "description": "", "version": 1, diff --git a/src/legacy/server/sample_data/routes/lib/load_data.test.js b/src/legacy/server/sample_data/routes/lib/load_data.test.js index caf0c089a9990..81bf03c82c320 100644 --- a/src/legacy/server/sample_data/routes/lib/load_data.test.js +++ b/src/legacy/server/sample_data/routes/lib/load_data.test.js @@ -35,8 +35,8 @@ test('load log data', async () => { myDocsCount += docs.length; }; const count = await loadData('./src/legacy/server/sample_data/data_sets/logs/logs.json.gz', bulkInsertMock); - expect(myDocsCount).toBe(14005); - expect(count).toBe(14005); + expect(myDocsCount).toBe(14074); + expect(count).toBe(14074); }); test('load ecommerce data', async () => { diff --git a/src/legacy/server/sample_data/usage/collector.ts b/src/legacy/server/sample_data/usage/collector.ts index 4ed7487807eec..8561a6c3f1007 100644 --- a/src/legacy/server/sample_data/usage/collector.ts +++ b/src/legacy/server/sample_data/usage/collector.ts @@ -36,6 +36,7 @@ export function makeSampleDataUsageCollector(server: KbnServer) { server.usage.collectorSet.makeUsageCollector({ type: 'sample-data', fetch: fetchProvider(index), + isReady: () => true, }) ); } diff --git a/src/legacy/server/saved_objects/export/get_sorted_objects_for_export.test.ts b/src/legacy/server/saved_objects/export/get_sorted_objects_for_export.test.ts index 4122fa63585e0..7ac3ebd412c0a 100644 --- a/src/legacy/server/saved_objects/export/get_sorted_objects_for_export.test.ts +++ b/src/legacy/server/saved_objects/export/get_sorted_objects_for_export.test.ts @@ -18,18 +18,10 @@ */ import { getSortedObjectsForExport } from './get_sorted_objects_for_export'; +import { SavedObjectsClientMock } from '../service/saved_objects_client.mock'; describe('getSortedObjectsForExport()', () => { - const savedObjectsClient = { - errors: {} as any, - find: jest.fn(), - bulkGet: jest.fn(), - create: jest.fn(), - bulkCreate: jest.fn(), - delete: jest.fn(), - get: jest.fn(), - update: jest.fn(), - }; + const savedObjectsClient = SavedObjectsClientMock.create(); afterEach(() => { savedObjectsClient.find.mockReset(); @@ -48,8 +40,10 @@ describe('getSortedObjectsForExport()', () => { { id: '2', type: 'search', + attributes: {}, references: [ { + name: 'name', type: 'index-pattern', id: '1', }, @@ -58,9 +52,12 @@ describe('getSortedObjectsForExport()', () => { { id: '1', type: 'index-pattern', + attributes: {}, references: [], }, ], + per_page: 1, + page: 0, }); const response = await getSortedObjectsForExport({ savedObjectsClient, @@ -70,15 +67,18 @@ describe('getSortedObjectsForExport()', () => { expect(response).toMatchInlineSnapshot(` Array [ Object { + "attributes": Object {}, "id": "1", "references": Array [], "type": "index-pattern", }, Object { + "attributes": Object {}, "id": "2", "references": Array [ Object { "id": "1", + "name": "name", "type": "index-pattern", }, ], @@ -118,9 +118,11 @@ Array [ { id: '2', type: 'search', + attributes: {}, references: [ { type: 'index-pattern', + name: 'name', id: '1', }, ], @@ -128,9 +130,12 @@ Array [ { id: '1', type: 'index-pattern', + attributes: {}, references: [], }, ], + per_page: 1, + page: 0, }); await expect( getSortedObjectsForExport({ @@ -147,16 +152,19 @@ Array [ { id: '2', type: 'search', + attributes: {}, references: [ { - type: 'index-pattern', id: '1', + name: 'name', + type: 'index-pattern', }, ], }, { id: '1', type: 'index-pattern', + attributes: {}, references: [], }, ], @@ -179,15 +187,18 @@ Array [ expect(response).toMatchInlineSnapshot(` Array [ Object { + "attributes": Object {}, "id": "1", "references": Array [], "type": "index-pattern", }, Object { + "attributes": Object {}, "id": "2", "references": Array [ Object { "id": "1", + "name": "name", "type": "index-pattern", }, ], @@ -227,9 +238,11 @@ Array [ { id: '2', type: 'search', + attributes: {}, references: [ { type: 'index-pattern', + name: 'name', id: '1', }, ], @@ -241,6 +254,7 @@ Array [ { id: '1', type: 'index-pattern', + attributes: {}, references: [], }, ], @@ -260,15 +274,18 @@ Array [ expect(response).toMatchInlineSnapshot(` Array [ Object { + "attributes": Object {}, "id": "1", "references": Array [], "type": "index-pattern", }, Object { + "attributes": Object {}, "id": "2", "references": Array [ Object { "id": "1", + "name": "name", "type": "index-pattern", }, ], diff --git a/src/legacy/server/saved_objects/export/get_sorted_objects_for_export.ts b/src/legacy/server/saved_objects/export/get_sorted_objects_for_export.ts index dc48e35dc5898..9782de4b553f8 100644 --- a/src/legacy/server/saved_objects/export/get_sorted_objects_for_export.ts +++ b/src/legacy/server/saved_objects/export/get_sorted_objects_for_export.ts @@ -18,7 +18,7 @@ */ import Boom from 'boom'; -import { SavedObjectsClient } from '../service/saved_objects_client'; +import { SavedObjectsClientContract } from '../'; import { injectNestedDependencies } from './inject_nested_depdendencies'; import { sortObjects } from './sort_objects'; @@ -30,7 +30,7 @@ interface ObjectToExport { interface ExportObjectsOptions { types?: string[]; objects?: ObjectToExport[]; - savedObjectsClient: SavedObjectsClient; + savedObjectsClient: SavedObjectsClientContract; exportSizeLimit: number; includeReferencesDeep?: boolean; } @@ -44,7 +44,7 @@ async function fetchObjectsToExport({ objects?: ObjectToExport[]; types?: string[]; exportSizeLimit: number; - savedObjectsClient: SavedObjectsClient; + savedObjectsClient: SavedObjectsClientContract; }) { if (objects) { if (objects.length > exportSizeLimit) { diff --git a/src/legacy/server/saved_objects/export/inject_nested_depdendencies.ts b/src/legacy/server/saved_objects/export/inject_nested_depdendencies.ts index 8529dcae4f0a6..ee9ce781ef9a5 100644 --- a/src/legacy/server/saved_objects/export/inject_nested_depdendencies.ts +++ b/src/legacy/server/saved_objects/export/inject_nested_depdendencies.ts @@ -18,7 +18,7 @@ */ import Boom from 'boom'; -import { SavedObject, SavedObjectsClient } from '../service/saved_objects_client'; +import { SavedObject, SavedObjectsClientContract } from '../service/saved_objects_client'; export function getObjectReferencesToFetch(savedObjectsMap: Map) { const objectsToFetch = new Map(); @@ -34,7 +34,7 @@ export function getObjectReferencesToFetch(savedObjectsMap: Map(); for (const savedObject of savedObjects) { diff --git a/src/legacy/server/saved_objects/import/import_saved_objects.ts b/src/legacy/server/saved_objects/import/import_saved_objects.ts index 03be32392b803..10c1350c4c579 100644 --- a/src/legacy/server/saved_objects/import/import_saved_objects.ts +++ b/src/legacy/server/saved_objects/import/import_saved_objects.ts @@ -18,17 +18,17 @@ */ import { Readable } from 'stream'; -import { SavedObjectsClient } from '../service'; import { collectSavedObjects } from './collect_saved_objects'; import { extractErrors } from './extract_errors'; import { ImportError } from './types'; import { validateReferences } from './validate_references'; +import { SavedObjectsClientContract } from '../'; interface ImportSavedObjectsOptions { readStream: Readable; objectLimit: number; overwrite: boolean; - savedObjectsClient: SavedObjectsClient; + savedObjectsClient: SavedObjectsClientContract; supportedTypes: string[]; } diff --git a/src/legacy/server/saved_objects/import/resolve_import_errors.ts b/src/legacy/server/saved_objects/import/resolve_import_errors.ts index 77fe856ece9ff..5cd4d2fca740c 100644 --- a/src/legacy/server/saved_objects/import/resolve_import_errors.ts +++ b/src/legacy/server/saved_objects/import/resolve_import_errors.ts @@ -18,7 +18,7 @@ */ import { Readable } from 'stream'; -import { SavedObjectsClient } from '../service'; +import { SavedObjectsClientContract } from '../'; import { collectSavedObjects } from './collect_saved_objects'; import { createObjectsFilter } from './create_objects_filter'; import { extractErrors } from './extract_errors'; @@ -29,7 +29,7 @@ import { validateReferences } from './validate_references'; interface ResolveImportErrorsOptions { readStream: Readable; objectLimit: number; - savedObjectsClient: SavedObjectsClient; + savedObjectsClient: SavedObjectsClientContract; retries: Retry[]; supportedTypes: string[]; } diff --git a/src/legacy/server/saved_objects/import/validate_references.ts b/src/legacy/server/saved_objects/import/validate_references.ts index e44cacd992489..2e3c1ef5293b3 100644 --- a/src/legacy/server/saved_objects/import/validate_references.ts +++ b/src/legacy/server/saved_objects/import/validate_references.ts @@ -18,7 +18,7 @@ */ import Boom from 'boom'; -import { SavedObject, SavedObjectsClient } from '../service'; +import { SavedObject, SavedObjectsClientContract } from '../'; import { ImportError } from './types'; const REF_TYPES_TO_VLIDATE = ['index-pattern', 'search']; @@ -29,7 +29,7 @@ function filterReferencesToValidate({ type }: { type: string }) { export async function getNonExistingReferenceAsKeys( savedObjects: SavedObject[], - savedObjectsClient: SavedObjectsClient + savedObjectsClient: SavedObjectsClientContract ) { const collector = new Map(); // Collect all references within objects @@ -77,7 +77,7 @@ export async function getNonExistingReferenceAsKeys( export async function validateReferences( savedObjects: SavedObject[], - savedObjectsClient: SavedObjectsClient + savedObjectsClient: SavedObjectsClientContract ) { const errorMap: { [key: string]: ImportError } = {}; const nonExistingReferenceKeys = await getNonExistingReferenceAsKeys( diff --git a/src/legacy/server/saved_objects/index.d.ts b/src/legacy/server/saved_objects/index.d.ts deleted file mode 100644 index e49dc27b9598a..0000000000000 --- a/src/legacy/server/saved_objects/index.d.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { - MigrationVersion, - SavedObject, - SavedObjectAttributes, - SavedObjectsClient, - SavedObjectsClientWrapperFactory, - SavedObjectReference, - SavedObjectsService, -} from './service'; - -export { SavedObjectsSchema } from './schema'; - -export { SavedObjectsManagement } from './management'; diff --git a/src/legacy/server/saved_objects/index.js b/src/legacy/server/saved_objects/index.js deleted file mode 100644 index e128322850a6e..0000000000000 --- a/src/legacy/server/saved_objects/index.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { savedObjectsMixin } from './saved_objects_mixin'; -export { SavedObjectsClient } from './service'; diff --git a/src/legacy/server/saved_objects/index.ts b/src/legacy/server/saved_objects/index.ts new file mode 100644 index 0000000000000..e6e9e2d266000 --- /dev/null +++ b/src/legacy/server/saved_objects/index.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './service'; + +export { SavedObjectsSchema } from './schema'; + +export { SavedObjectsManagement } from './management'; diff --git a/src/legacy/server/saved_objects/migrations/core/build_index_map.ts b/src/legacy/server/saved_objects/migrations/core/build_index_map.ts new file mode 100644 index 0000000000000..5c0f08bf4046b --- /dev/null +++ b/src/legacy/server/saved_objects/migrations/core/build_index_map.ts @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { MappingProperties } from 'src/legacy/server/mappings'; +import { SavedObjectsSchemaDefinition } from '../../schema'; + +/* + * This file contains logic to convert savedObjectSchemas into a dictonary of indexes and documents + */ +export function createIndexMap( + defaultIndex: string, + savedObjectSchemas: SavedObjectsSchemaDefinition, + indexMap: MappingProperties +) { + const map: { [index: string]: MappingProperties } = {}; + Object.keys(indexMap).forEach(type => { + const indexPattern = (savedObjectSchemas[type] || {}).indexPattern || defaultIndex; + if (!map.hasOwnProperty(indexPattern as string)) { + map[indexPattern] = {}; + } + map[indexPattern][type] = indexMap[type]; + }); + return map; +} diff --git a/src/legacy/server/saved_objects/migrations/core/document_migrator.ts b/src/legacy/server/saved_objects/migrations/core/document_migrator.ts index 636b35e390ef0..bcff2988f4afe 100644 --- a/src/legacy/server/saved_objects/migrations/core/document_migrator.ts +++ b/src/legacy/server/saved_objects/migrations/core/document_migrator.ts @@ -64,7 +64,8 @@ import Boom from 'boom'; import _ from 'lodash'; import cloneDeep from 'lodash.clonedeep'; import Semver from 'semver'; -import { MigrationVersion, RawSavedObjectDoc } from '../../serialization'; +import { RawSavedObjectDoc } from '../../serialization'; +import { MigrationVersion } from '../../'; import { LogFn, Logger, MigrationLogger } from './migration_logger'; export type TransformFn = (doc: RawSavedObjectDoc, log?: Logger) => RawSavedObjectDoc; diff --git a/src/legacy/server/saved_objects/migrations/core/elastic_index.ts b/src/legacy/server/saved_objects/migrations/core/elastic_index.ts index 48d44492eb275..1e55bd3d01688 100644 --- a/src/legacy/server/saved_objects/migrations/core/elastic_index.ts +++ b/src/legacy/server/saved_objects/migrations/core/elastic_index.ts @@ -24,12 +24,9 @@ import _ from 'lodash'; import { IndexMapping } from '../../../mappings'; -import { MigrationVersion } from '../../serialization'; +import { MigrationVersion } from '../../'; import { AliasAction, CallCluster, NotFound, RawDoc, ShardsInfo } from './call_cluster'; -// @ts-ignore untyped dependency -import { getTypes } from '../../../mappings'; - const settings = { number_of_shards: 1, auto_expand_replicas: '0-1' }; export interface FullIndexInfo { diff --git a/src/legacy/server/saved_objects/migrations/core/index_migrator.ts b/src/legacy/server/saved_objects/migrations/core/index_migrator.ts index b8cfad96ae76a..7fc2bcfb72602 100644 --- a/src/legacy/server/saved_objects/migrations/core/index_migrator.ts +++ b/src/legacy/server/saved_objects/migrations/core/index_migrator.ts @@ -53,7 +53,7 @@ export class IndexMigrator { pollInterval: context.pollInterval, async isMigrated() { - return requiresMigration(context); + return !(await requiresMigration(context)); }, async runMigration() { diff --git a/src/legacy/server/saved_objects/migrations/kibana/kibana_migrator.test.ts b/src/legacy/server/saved_objects/migrations/kibana/kibana_migrator.test.ts index 4cfe64059f716..7237d62dca6e2 100644 --- a/src/legacy/server/saved_objects/migrations/kibana/kibana_migrator.test.ts +++ b/src/legacy/server/saved_objects/migrations/kibana/kibana_migrator.test.ts @@ -61,14 +61,12 @@ describe('KibanaMigrator', () => { const { kbnServer } = mockKbnServer(); kbnServer.server.plugins.elasticsearch = undefined; const result = await new KibanaMigrator({ kbnServer }).awaitMigration(); - expect(result).toEqual({ status: 'skipped' }); + expect(result).toEqual([{ status: 'skipped' }, { status: 'skipped' }]); }); it('waits for kbnServer.ready and elasticsearch.ready before attempting migrations', async () => { const { kbnServer } = mockKbnServer(); - const clusterStub = jest.fn(() => { - throw new Error('Doh!'); - }); + const clusterStub = jest.fn(() => ({ status: 404 })); const waitUntilReady = jest.fn(async () => undefined); kbnServer.server.plugins.elasticsearch = { @@ -83,7 +81,8 @@ describe('KibanaMigrator', () => { }, }; - await expect(new KibanaMigrator({ kbnServer }).awaitMigration()).rejects.toThrow(/Doh!/); + const migrationResults = await new KibanaMigrator({ kbnServer }).awaitMigration(); + expect(migrationResults.length).toEqual(2); }); }); }); @@ -94,11 +93,37 @@ function mockKbnServer({ configValues }: { configValues?: any } = {}) { version: '8.2.3', ready: jest.fn(async () => undefined), uiExports: { + savedObjectsManagement: {}, savedObjectValidations: {}, savedObjectMigrations: {}, - savedObjectMappings: [], - savedObjectSchemas: {}, - savedObjectsManagement: {}, + savedObjectMappings: [ + { + pluginId: 'testtype', + properties: { + testtype: { + properties: { + name: { type: 'keyword' }, + }, + }, + }, + }, + { + pluginId: 'testtype2', + properties: { + testtype2: { + properties: { + name: { type: 'keyword' }, + }, + }, + }, + }, + ], + savedObjectSchemas: { + testtype2: { + isNamespaceAgnostic: false, + indexPattern: 'other-index', + }, + }, }, server: { config: () => ({ diff --git a/src/legacy/server/saved_objects/migrations/kibana/kibana_migrator.ts b/src/legacy/server/saved_objects/migrations/kibana/kibana_migrator.ts index 8eef9aed37c01..69322ef0a8b23 100644 --- a/src/legacy/server/saved_objects/migrations/kibana/kibana_migrator.ts +++ b/src/legacy/server/saved_objects/migrations/kibana/kibana_migrator.ts @@ -30,7 +30,7 @@ import { RawSavedObjectDoc, SavedObjectsSerializer } from '../../serialization'; import { docValidator } from '../../validation'; import { buildActiveMappings, CallCluster, IndexMigrator, LogFn } from '../core'; import { DocumentMigrator, VersionedTransformer } from '../core/document_migrator'; - +import { createIndexMap } from '../core/build_index_map'; export interface KbnServer { server: Server; version: string; @@ -86,27 +86,39 @@ export class KibanaMigrator { ['warning', 'migration'], 'The elasticsearch plugin is disabled. Skipping migrations.' ); - return { status: 'skipped' }; + return Object.keys(this.mappingProperties).map(() => ({ status: 'skipped' })); } // Wait until elasticsearch is green... await server.plugins.elasticsearch.waitUntilReady(); const config = server.config(); - const migrator = new IndexMigrator({ - batchSize: config.get('migrations.batchSize'), - callCluster: server.plugins.elasticsearch!.getCluster('admin').callWithInternalUser, - documentMigrator: this.documentMigrator, - index: config.get('kibana.index'), - log: this.log, - mappingProperties: this.mappingProperties, - pollInterval: config.get('migrations.pollInterval'), - scrollDuration: config.get('migrations.scrollDuration'), - serializer: this.serializer, - obsoleteIndexTemplatePattern: 'kibana_index_template*', + const indexMap = createIndexMap( + config.get('kibana.index'), + this.kbnServer.uiExports.savedObjectSchemas, + this.mappingProperties + ); + + const migrators = Object.keys(indexMap).map(index => { + return new IndexMigrator({ + batchSize: config.get('migrations.batchSize'), + callCluster: server.plugins.elasticsearch!.getCluster('admin').callWithInternalUser, + documentMigrator: this.documentMigrator, + index, + log: this.log, + mappingProperties: indexMap[index], + pollInterval: config.get('migrations.pollInterval'), + scrollDuration: config.get('migrations.scrollDuration'), + serializer: this.serializer, + obsoleteIndexTemplatePattern: 'kibana_index_template*', + }); }); - return migrator.migrate(); + if (migrators.length === 0) { + throw new Error(`Migrations failed to run, no mappings found or Kibana is not "ready".`); + } + + return Promise.all(migrators.map(migrator => migrator.migrate())); }); private kbnServer: KbnServer; @@ -124,11 +136,15 @@ export class KibanaMigrator { */ constructor({ kbnServer }: { kbnServer: KbnServer }) { this.kbnServer = kbnServer; + this.serializer = new SavedObjectsSerializer( new SavedObjectsSchema(kbnServer.uiExports.savedObjectSchemas) ); + this.mappingProperties = mergeProperties(kbnServer.uiExports.savedObjectMappings || []); + this.log = (meta: string[], message: string) => kbnServer.server.log(meta, message); + this.documentMigrator = new DocumentMigrator({ kibanaVersion: kbnServer.version, migrations: kbnServer.uiExports.savedObjectMigrations || {}, @@ -138,7 +154,7 @@ export class KibanaMigrator { } /** - * Gets the index mappings defined by Kibana's enabled plugins. + * Gets all the index mappings defined by Kibana's enabled plugins. * * @returns * @memberof KibanaMigrator diff --git a/src/legacy/server/saved_objects/routes/bulk_create.test.ts b/src/legacy/server/saved_objects/routes/bulk_create.test.ts index 5c69c490084b3..f981b0a62f605 100644 --- a/src/legacy/server/saved_objects/routes/bulk_create.test.ts +++ b/src/legacy/server/saved_objects/routes/bulk_create.test.ts @@ -20,22 +20,14 @@ import Hapi from 'hapi'; import { createMockServer } from './_mock_server'; import { createBulkCreateRoute } from './bulk_create'; +import { SavedObjectsClientMock } from '../service/saved_objects_client.mock'; describe('POST /api/saved_objects/_bulk_create', () => { let server: Hapi.Server; - const savedObjectsClient = { - errors: {} as any, - bulkCreate: jest.fn(), - bulkGet: jest.fn(), - create: jest.fn(), - delete: jest.fn(), - find: jest.fn(), - get: jest.fn(), - update: jest.fn(), - }; + const savedObjectsClient = SavedObjectsClientMock.create(); beforeEach(() => { - savedObjectsClient.bulkCreate.mockImplementation(() => Promise.resolve('')); + savedObjectsClient.bulkCreate.mockImplementation(() => Promise.resolve('' as any)); server = createMockServer(); const prereqs = { @@ -75,7 +67,8 @@ describe('POST /api/saved_objects/_bulk_create', () => { id: 'abc123', type: 'index-pattern', title: 'logstash-*', - version: 2, + attributes: {}, + version: '2', references: [], }, ], diff --git a/src/legacy/server/saved_objects/routes/bulk_create.ts b/src/legacy/server/saved_objects/routes/bulk_create.ts index ffc6831bc3e5f..35b90ecbcd853 100644 --- a/src/legacy/server/saved_objects/routes/bulk_create.ts +++ b/src/legacy/server/saved_objects/routes/bulk_create.ts @@ -19,7 +19,7 @@ import Hapi from 'hapi'; import Joi from 'joi'; -import { SavedObjectAttributes, SavedObjectsClient } from '../'; +import { SavedObjectAttributes, SavedObjectsClientContract } from '../'; import { Prerequisites, SavedObjectReference, WithoutQueryAndParams } from './types'; interface SavedObject { @@ -33,7 +33,7 @@ interface SavedObject { interface BulkCreateRequest extends WithoutQueryAndParams { pre: { - savedObjectsClient: SavedObjectsClient; + savedObjectsClient: SavedObjectsClientContract; }; query: { overwrite: boolean; diff --git a/src/legacy/server/saved_objects/routes/export.test.ts b/src/legacy/server/saved_objects/routes/export.test.ts index 07abd92784624..8096bce269bf2 100644 --- a/src/legacy/server/saved_objects/routes/export.test.ts +++ b/src/legacy/server/saved_objects/routes/export.test.ts @@ -17,6 +17,10 @@ * under the License. */ +jest.mock('../export', () => ({ + getSortedObjectsForExport: jest.fn(), +})); + import Hapi from 'hapi'; import * as exportMock from '../export'; import { createMockServer } from './_mock_server'; @@ -24,10 +28,6 @@ import { createExportRoute } from './export'; const getSortedObjectsForExport = exportMock.getSortedObjectsForExport as jest.Mock; -jest.mock('../export', () => ({ - getSortedObjectsForExport: jest.fn(), -})); - describe('POST /api/saved_objects/_export', () => { let server: Hapi.Server; const savedObjectsClient = { diff --git a/src/legacy/server/saved_objects/routes/types.ts b/src/legacy/server/saved_objects/routes/types.ts index 8a63a5ff32d9a..f658a61117890 100644 --- a/src/legacy/server/saved_objects/routes/types.ts +++ b/src/legacy/server/saved_objects/routes/types.ts @@ -18,7 +18,7 @@ */ import Hapi from 'hapi'; -import { SavedObjectsClient } from '../'; +import { SavedObjectsClientContract } from '../'; export interface SavedObjectReference { name: string; @@ -29,7 +29,7 @@ export interface SavedObjectReference { export interface Prerequisites { getSavedObjectsClient: { assign: string; - method: (req: Hapi.Request) => SavedObjectsClient; + method: (req: Hapi.Request) => SavedObjectsClientContract; }; } diff --git a/src/legacy/server/saved_objects/saved_objects_mixin.test.js b/src/legacy/server/saved_objects/saved_objects_mixin.test.js index bca02bb3c9766..3bf1aa8139cc2 100644 --- a/src/legacy/server/saved_objects/saved_objects_mixin.test.js +++ b/src/legacy/server/saved_objects/saved_objects_mixin.test.js @@ -67,6 +67,9 @@ describe('Saved Objects Mixin', () => { hiddentype: { hidden: true, }, + doc1: { + indexPattern: 'other-index', + }, }, savedObjectMappings: [ { @@ -79,6 +82,21 @@ describe('Saved Objects Mixin', () => { }, }, }, + { + pluginId: 'testtype2', + properties: { + doc1: { + properties: { + name: { type: 'keyword' }, + }, + }, + doc2: { + properties: { + name: { type: 'keyword' }, + }, + }, + }, + }, { pluginId: 'secretPlugin', properties: { @@ -198,7 +216,7 @@ describe('Saved Objects Mixin', () => { it('should return all but hidden types', () => { expect(service).toBeDefined(); - expect(service.types).toEqual(['config', 'testtype']); + expect(service.types).toEqual(['config', 'testtype', 'doc1', 'doc2']); }); const mockCallEs = jest.fn(); @@ -212,7 +230,7 @@ describe('Saved Objects Mixin', () => { it('should create a repository without hidden types', () => { const repository = service.getSavedObjectsRepository(mockCallEs); expect(repository).toBeDefined(); - expect(repository._allowedTypes).toEqual(['config', 'testtype']); + expect(repository._allowedTypes).toEqual(['config', 'testtype', 'doc1', 'doc2']); }); it('should create a repository with a unique list of allowed types', () => { @@ -221,7 +239,7 @@ describe('Saved Objects Mixin', () => { 'config', 'config', ]); - expect(repository._allowedTypes).toEqual(['config', 'testtype']); + expect(repository._allowedTypes).toEqual(['config', 'testtype', 'doc1', 'doc2']); }); it('should create a repository with extraTypes minus duplicate', () => { @@ -229,7 +247,13 @@ describe('Saved Objects Mixin', () => { 'hiddentype', 'hiddentype', ]); - expect(repository._allowedTypes).toEqual(['config', 'testtype', 'hiddentype']); + expect(repository._allowedTypes).toEqual([ + 'config', + 'testtype', + 'doc1', + 'doc2', + 'hiddentype', + ]); }); it('should not allow a repository without a callCluster function', () => { diff --git a/src/legacy/server/saved_objects/schema/schema.mock.ts b/src/legacy/server/saved_objects/schema/schema.mock.ts index 844893156deef..7fec7d54294d6 100644 --- a/src/legacy/server/saved_objects/schema/schema.mock.ts +++ b/src/legacy/server/saved_objects/schema/schema.mock.ts @@ -22,6 +22,7 @@ import { SavedObjectsSchema } from './schema'; type Schema = PublicMethodsOf; const createSchemaMock = () => { const mocked: jest.Mocked = { + getIndexForType: jest.fn().mockReturnValue('.kibana-test'), isHiddenType: jest.fn().mockReturnValue(false), isNamespaceAgnostic: jest.fn((type: string) => type === 'global'), }; diff --git a/src/legacy/server/saved_objects/schema/schema.ts b/src/legacy/server/saved_objects/schema/schema.ts index ab74c64e5ba67..6756feeb15a0f 100644 --- a/src/legacy/server/saved_objects/schema/schema.ts +++ b/src/legacy/server/saved_objects/schema/schema.ts @@ -16,10 +16,10 @@ * specific language governing permissions and limitations * under the License. */ - interface SavedObjectsSchemaTypeDefinition { isNamespaceAgnostic: boolean; hidden?: boolean; + indexPattern?: string; } export interface SavedObjectsSchemaDefinition { @@ -40,6 +40,14 @@ export class SavedObjectsSchema { return false; } + public getIndexForType(type: string): string | undefined { + if (this.definition != null && this.definition.hasOwnProperty(type)) { + return this.definition[type].indexPattern; + } else { + return undefined; + } + } + public isNamespaceAgnostic(type: string) { // if no plugins have registered a uiExports.savedObjectSchemas, // this.schema will be undefined, and no types are namespace agnostic diff --git a/src/legacy/server/saved_objects/serialization/index.ts b/src/legacy/server/saved_objects/serialization/index.ts index 997c5d38b69db..72071ae8866fa 100644 --- a/src/legacy/server/saved_objects/serialization/index.ts +++ b/src/legacy/server/saved_objects/serialization/index.ts @@ -27,6 +27,7 @@ import uuid from 'uuid'; import { SavedObjectsSchema } from '../schema'; import { decodeVersion, encodeVersion } from '../version'; +import { MigrationVersion, SavedObjectReference } from '../service/saved_objects_client'; /** * A raw document as represented directly in the saved object index. @@ -39,23 +40,6 @@ export interface RawDoc { _primary_term?: number; } -/** - * A dictionary of saved object type -> version used to determine - * what migrations need to be applied to a saved object. - */ -export interface MigrationVersion { - [type: string]: string; -} - -/** - * A reference object to anohter saved object. - */ -export interface SavedObjectReference { - name: string; - type: string; - id: string; -} - /** * A saved object type definition that allows for miscellaneous, unknown * properties, as current discussions around security, ACLs, etc indicate @@ -64,12 +48,12 @@ export interface SavedObjectReference { */ interface SavedObjectDoc { attributes: object; - id: string; + id?: string; // NOTE: SavedObjectDoc is used for uncreated objects where `id` is optional type: string; namespace?: string; migrationVersion?: MigrationVersion; version?: string; - updated_at?: Date; + updated_at?: string; [rootProp: string]: any; } diff --git a/src/legacy/server/saved_objects/service/index.d.ts b/src/legacy/server/saved_objects/service/index.d.ts deleted file mode 100644 index cfc190bb199aa..0000000000000 --- a/src/legacy/server/saved_objects/service/index.d.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { ScopedSavedObjectsClientProvider } from './lib'; -import { SavedObjectsClient } from './saved_objects_client'; - -export interface SavedObjectsService { - // ATTENTION: these types are incomplete - addScopedSavedObjectsClientWrapperFactory: ScopedSavedObjectsClientProvider< - Request - >['addClientWrapperFactory']; - getScopedSavedObjectsClient: ScopedSavedObjectsClientProvider['getClient']; - SavedObjectsClient: typeof SavedObjectsClient; - types: string[]; - getSavedObjectsRepository(...rest: any[]): any; -} - -export { SavedObjectsClientWrapperFactory } from './lib'; -export { - FindOptions, - GetResponse, - UpdateResponse, - CreateResponse, - MigrationVersion, - SavedObject, - SavedObjectAttributes, - SavedObjectsClient, - SavedObjectReference, -} from './saved_objects_client'; diff --git a/src/legacy/server/saved_objects/service/index.js b/src/legacy/server/saved_objects/service/index.js deleted file mode 100644 index 4624197e323e3..0000000000000 --- a/src/legacy/server/saved_objects/service/index.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { SavedObjectsClient } from './saved_objects_client'; -export { SavedObjectsRepository, ScopedSavedObjectsClientProvider } from './lib'; diff --git a/src/legacy/server/saved_objects/service/index.ts b/src/legacy/server/saved_objects/service/index.ts new file mode 100644 index 0000000000000..c4e0d66eb95b8 --- /dev/null +++ b/src/legacy/server/saved_objects/service/index.ts @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ScopedSavedObjectsClientProvider } from './lib'; +import { SavedObjectsClient } from './saved_objects_client'; + +export interface SavedObjectsService { + // ATTENTION: these types are incomplete + addScopedSavedObjectsClientWrapperFactory: ScopedSavedObjectsClientProvider< + Request + >['addClientWrapperFactory']; + getScopedSavedObjectsClient: ScopedSavedObjectsClientProvider['getClient']; + SavedObjectsClient: typeof SavedObjectsClient; + types: string[]; + getSavedObjectsRepository(...rest: any[]): any; +} + +export { + SavedObjectsRepository, + ScopedSavedObjectsClientProvider, + SavedObjectsClientWrapperFactory, +} from './lib'; + +export * from './saved_objects_client'; diff --git a/src/legacy/server/saved_objects/service/lib/decorate_es_error.js b/src/legacy/server/saved_objects/service/lib/decorate_es_error.js deleted file mode 100644 index c99ca7c794584..0000000000000 --- a/src/legacy/server/saved_objects/service/lib/decorate_es_error.js +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import elasticsearch from 'elasticsearch'; -import { get } from 'lodash'; - -const { - ConnectionFault, - ServiceUnavailable, - NoConnections, - RequestTimeout, - Conflict, - 401: NotAuthorized, - 403: Forbidden, - 413: RequestEntityTooLarge, - NotFound, - BadRequest, -} = elasticsearch.errors; - -import { - decorateBadRequestError, - decorateNotAuthorizedError, - decorateForbiddenError, - decorateRequestEntityTooLargeError, - createGenericNotFoundError, - decorateConflictError, - decorateEsUnavailableError, - decorateGeneralError, -} from './errors'; - -export function decorateEsError(error) { - if (!(error instanceof Error)) { - throw new Error('Expected an instance of Error'); - } - - const { reason } = get(error, 'body.error', {}); - if ( - error instanceof ConnectionFault || - error instanceof ServiceUnavailable || - error instanceof NoConnections || - error instanceof RequestTimeout - ) { - return decorateEsUnavailableError(error, reason); - } - - if (error instanceof Conflict) { - return decorateConflictError(error, reason); - } - - if (error instanceof NotAuthorized) { - return decorateNotAuthorizedError(error, reason); - } - - if (error instanceof Forbidden) { - return decorateForbiddenError(error, reason); - } - - if (error instanceof RequestEntityTooLarge) { - return decorateRequestEntityTooLargeError(error, reason); - } - - if (error instanceof NotFound) { - return createGenericNotFoundError(); - } - - if (error instanceof BadRequest) { - return decorateBadRequestError(error, reason); - } - - return decorateGeneralError(error, reason); -} diff --git a/src/legacy/server/saved_objects/service/lib/decorate_es_error.test.js b/src/legacy/server/saved_objects/service/lib/decorate_es_error.test.js deleted file mode 100644 index 8d070e1713202..0000000000000 --- a/src/legacy/server/saved_objects/service/lib/decorate_es_error.test.js +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { errors as esErrors } from 'elasticsearch'; - -import { decorateEsError } from './decorate_es_error'; -import { - isEsUnavailableError, - isConflictError, - isNotAuthorizedError, - isForbiddenError, - isRequestEntityTooLargeError, - isNotFoundError, - isBadRequestError, -} from './errors'; - -describe('savedObjectsClient/decorateEsError', () => { - it('always returns the same error it receives', () => { - const error = new Error(); - expect(decorateEsError(error)).toBe(error); - }); - - it('makes es.ConnectionFault a SavedObjectsClient/EsUnavailable error', () => { - const error = new esErrors.ConnectionFault(); - expect(isEsUnavailableError(error)).toBe(false); - expect(decorateEsError(error)).toBe(error); - expect(isEsUnavailableError(error)).toBe(true); - }); - - it('makes es.ServiceUnavailable a SavedObjectsClient/EsUnavailable error', () => { - const error = new esErrors.ServiceUnavailable(); - expect(isEsUnavailableError(error)).toBe(false); - expect(decorateEsError(error)).toBe(error); - expect(isEsUnavailableError(error)).toBe(true); - }); - - it('makes es.NoConnections a SavedObjectsClient/EsUnavailable error', () => { - const error = new esErrors.NoConnections(); - expect(isEsUnavailableError(error)).toBe(false); - expect(decorateEsError(error)).toBe(error); - expect(isEsUnavailableError(error)).toBe(true); - }); - - it('makes es.RequestTimeout a SavedObjectsClient/EsUnavailable error', () => { - const error = new esErrors.RequestTimeout(); - expect(isEsUnavailableError(error)).toBe(false); - expect(decorateEsError(error)).toBe(error); - expect(isEsUnavailableError(error)).toBe(true); - }); - - it('makes es.Conflict a SavedObjectsClient/Conflict error', () => { - const error = new esErrors.Conflict(); - expect(isConflictError(error)).toBe(false); - expect(decorateEsError(error)).toBe(error); - expect(isConflictError(error)).toBe(true); - }); - - it('makes es.AuthenticationException a SavedObjectsClient/NotAuthorized error', () => { - const error = new esErrors.AuthenticationException(); - expect(isNotAuthorizedError(error)).toBe(false); - expect(decorateEsError(error)).toBe(error); - expect(isNotAuthorizedError(error)).toBe(true); - }); - - it('makes es.Forbidden a SavedObjectsClient/Forbidden error', () => { - const error = new esErrors.Forbidden(); - expect(isForbiddenError(error)).toBe(false); - expect(decorateEsError(error)).toBe(error); - expect(isForbiddenError(error)).toBe(true); - }); - - it('makes es.RequestEntityTooLarge a SavedObjectsClient/RequestEntityTooLarge error', () => { - const error = new esErrors.RequestEntityTooLarge(); - expect(isRequestEntityTooLargeError(error)).toBe(false); - expect(decorateEsError(error)).toBe(error); - expect(isRequestEntityTooLargeError(error)).toBe(true); - }); - - it('discards es.NotFound errors and returns a generic NotFound error', () => { - const error = new esErrors.NotFound(); - expect(isNotFoundError(error)).toBe(false); - const genericError = decorateEsError(error); - expect(genericError).not.toBe(error); - expect(isNotFoundError(error)).toBe(false); - expect(isNotFoundError(genericError)).toBe(true); - }); - - it('makes es.BadRequest a SavedObjectsClient/BadRequest error', () => { - const error = new esErrors.BadRequest(); - expect(isBadRequestError(error)).toBe(false); - expect(decorateEsError(error)).toBe(error); - expect(isBadRequestError(error)).toBe(true); - }); - - it('returns other errors as Boom errors', () => { - const error = new Error(); - expect(error).not.toHaveProperty('isBoom'); - expect(decorateEsError(error)).toBe(error); - expect(error).toHaveProperty('isBoom'); - }); -}); diff --git a/src/legacy/server/saved_objects/service/lib/decorate_es_error.test.ts b/src/legacy/server/saved_objects/service/lib/decorate_es_error.test.ts new file mode 100644 index 0000000000000..272a26327b808 --- /dev/null +++ b/src/legacy/server/saved_objects/service/lib/decorate_es_error.test.ts @@ -0,0 +1,117 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { errors as esErrors } from 'elasticsearch'; + +import { decorateEsError } from './decorate_es_error'; +import { + isBadRequestError, + isConflictError, + isEsUnavailableError, + isForbiddenError, + isNotAuthorizedError, + isNotFoundError, + isRequestEntityTooLargeError, +} from './errors'; + +describe('savedObjectsClient/decorateEsError', () => { + it('always returns the same error it receives', () => { + const error = new Error(); + expect(decorateEsError(error)).toBe(error); + }); + + it('makes es.ConnectionFault a SavedObjectsClient/EsUnavailable error', () => { + const error = new esErrors.ConnectionFault(); + expect(isEsUnavailableError(error)).toBe(false); + expect(decorateEsError(error)).toBe(error); + expect(isEsUnavailableError(error)).toBe(true); + }); + + it('makes es.ServiceUnavailable a SavedObjectsClient/EsUnavailable error', () => { + const error = new esErrors.ServiceUnavailable(); + expect(isEsUnavailableError(error)).toBe(false); + expect(decorateEsError(error)).toBe(error); + expect(isEsUnavailableError(error)).toBe(true); + }); + + it('makes es.NoConnections a SavedObjectsClient/EsUnavailable error', () => { + const error = new esErrors.NoConnections(); + expect(isEsUnavailableError(error)).toBe(false); + expect(decorateEsError(error)).toBe(error); + expect(isEsUnavailableError(error)).toBe(true); + }); + + it('makes es.RequestTimeout a SavedObjectsClient/EsUnavailable error', () => { + const error = new esErrors.RequestTimeout(); + expect(isEsUnavailableError(error)).toBe(false); + expect(decorateEsError(error)).toBe(error); + expect(isEsUnavailableError(error)).toBe(true); + }); + + it('makes es.Conflict a SavedObjectsClient/Conflict error', () => { + const error = new esErrors.Conflict(); + expect(isConflictError(error)).toBe(false); + expect(decorateEsError(error)).toBe(error); + expect(isConflictError(error)).toBe(true); + }); + + it('makes es.AuthenticationException a SavedObjectsClient/NotAuthorized error', () => { + const error = new esErrors.AuthenticationException(); + expect(isNotAuthorizedError(error)).toBe(false); + expect(decorateEsError(error)).toBe(error); + expect(isNotAuthorizedError(error)).toBe(true); + }); + + it('makes es.Forbidden a SavedObjectsClient/Forbidden error', () => { + const error = new esErrors.Forbidden(); + expect(isForbiddenError(error)).toBe(false); + expect(decorateEsError(error)).toBe(error); + expect(isForbiddenError(error)).toBe(true); + }); + + it('makes es.RequestEntityTooLarge a SavedObjectsClient/RequestEntityTooLarge error', () => { + const error = new esErrors.RequestEntityTooLarge(); + expect(isRequestEntityTooLargeError(error)).toBe(false); + expect(decorateEsError(error)).toBe(error); + expect(isRequestEntityTooLargeError(error)).toBe(true); + }); + + it('discards es.NotFound errors and returns a generic NotFound error', () => { + const error = new esErrors.NotFound(); + expect(isNotFoundError(error)).toBe(false); + const genericError = decorateEsError(error); + expect(genericError).not.toBe(error); + expect(isNotFoundError(error)).toBe(false); + expect(isNotFoundError(genericError)).toBe(true); + }); + + it('makes es.BadRequest a SavedObjectsClient/BadRequest error', () => { + const error = new esErrors.BadRequest(); + expect(isBadRequestError(error)).toBe(false); + expect(decorateEsError(error)).toBe(error); + expect(isBadRequestError(error)).toBe(true); + }); + + it('returns other errors as Boom errors', () => { + const error = new Error(); + expect(error).not.toHaveProperty('isBoom'); + expect(decorateEsError(error)).toBe(error); + expect(error).toHaveProperty('isBoom'); + }); +}); diff --git a/src/legacy/server/saved_objects/service/lib/decorate_es_error.ts b/src/legacy/server/saved_objects/service/lib/decorate_es_error.ts new file mode 100644 index 0000000000000..becb41b78dad4 --- /dev/null +++ b/src/legacy/server/saved_objects/service/lib/decorate_es_error.ts @@ -0,0 +1,90 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import elasticsearch from 'elasticsearch'; +import { get } from 'lodash'; + +const { + ConnectionFault, + ServiceUnavailable, + NoConnections, + RequestTimeout, + Conflict, + // @ts-ignore + 401: NotAuthorized, + // @ts-ignore + 403: Forbidden, + // @ts-ignore + 413: RequestEntityTooLarge, + NotFound, + BadRequest, +} = elasticsearch.errors; + +import { + createGenericNotFoundError, + decorateBadRequestError, + decorateConflictError, + decorateEsUnavailableError, + decorateForbiddenError, + decorateGeneralError, + decorateNotAuthorizedError, + decorateRequestEntityTooLargeError, +} from './errors'; + +export function decorateEsError(error: Error) { + if (!(error instanceof Error)) { + throw new Error('Expected an instance of Error'); + } + + const { reason } = get(error, 'body.error', { reason: undefined }); + if ( + error instanceof ConnectionFault || + error instanceof ServiceUnavailable || + error instanceof NoConnections || + error instanceof RequestTimeout + ) { + return decorateEsUnavailableError(error, reason); + } + + if (error instanceof Conflict) { + return decorateConflictError(error, reason); + } + + if (error instanceof NotAuthorized) { + return decorateNotAuthorizedError(error, reason); + } + + if (error instanceof Forbidden) { + return decorateForbiddenError(error, reason); + } + + if (error instanceof RequestEntityTooLarge) { + return decorateRequestEntityTooLargeError(error, reason); + } + + if (error instanceof NotFound) { + return createGenericNotFoundError(); + } + + if (error instanceof BadRequest) { + return decorateBadRequestError(error, reason); + } + + return decorateGeneralError(error, reason); +} diff --git a/src/legacy/server/saved_objects/service/lib/errors.d.ts b/src/legacy/server/saved_objects/service/lib/errors.d.ts deleted file mode 100644 index cfeed417b98da..0000000000000 --- a/src/legacy/server/saved_objects/service/lib/errors.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export function isBadRequestError(maybeError: any): boolean; -export function isNotAuthorizedError(maybeError: any): boolean; -export function isForbiddenError(maybeError: any): boolean; -export function isRequestEntityTooLargeError(maybeError: any): boolean; -export function isNotFoundError(maybeError: any): boolean; -export function isConflictError(maybeError: any): boolean; -export function isEsUnavailableError(maybeError: any): boolean; -export function isEsAutoCreateIndexError(maybeError: any): boolean; - -export function createInvalidVersionError(version: any): Error; -export function isInvalidVersionError(maybeError: Error): boolean; diff --git a/src/legacy/server/saved_objects/service/lib/errors.js b/src/legacy/server/saved_objects/service/lib/errors.js deleted file mode 100644 index 63fed3f092bcc..0000000000000 --- a/src/legacy/server/saved_objects/service/lib/errors.js +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import Boom from 'boom'; - -const code = Symbol('SavedObjectsClientErrorCode'); - -function decorate(error, errorCode, statusCode, message) { - if (isSavedObjectsClientError(error)) { - return error; - } - - const boom = Boom.boomify(error, { - statusCode, - message, - override: false, - }); - - boom[code] = errorCode; - - return boom; -} - -export function isSavedObjectsClientError(error) { - return error && !!error[code]; -} - -// 400 - badRequest -const CODE_BAD_REQUEST = 'SavedObjectsClient/badRequest'; -export function decorateBadRequestError(error, reason) { - return decorate(error, CODE_BAD_REQUEST, 400, reason); -} -export function createBadRequestError(reason) { - return decorateBadRequestError(new Error('Bad Request'), reason); -} -export function createUnsupportedTypeError(type) { - return createBadRequestError(`Unsupported saved object type: '${type}'`); -} -export function isBadRequestError(error) { - return error && error[code] === CODE_BAD_REQUEST; -} - -// 400 - invalid version -const CODE_INVALID_VERSION = 'SavedObjectsClient/invalidVersion'; -export function createInvalidVersionError(versionInput) { - return decorate(Boom.badRequest(`Invalid version [${versionInput}]`), CODE_INVALID_VERSION, 400); -} -export function isInvalidVersionError(error) { - return error && error[code] === CODE_INVALID_VERSION; -} - -// 401 - Not Authorized -const CODE_NOT_AUTHORIZED = 'SavedObjectsClient/notAuthorized'; -export function decorateNotAuthorizedError(error, reason) { - return decorate(error, CODE_NOT_AUTHORIZED, 401, reason); -} -export function isNotAuthorizedError(error) { - return error && error[code] === CODE_NOT_AUTHORIZED; -} - -// 403 - Forbidden -const CODE_FORBIDDEN = 'SavedObjectsClient/forbidden'; -export function decorateForbiddenError(error, reason) { - return decorate(error, CODE_FORBIDDEN, 403, reason); -} -export function isForbiddenError(error) { - return error && error[code] === CODE_FORBIDDEN; -} - -// 413 - Request Entity Too Large -const CODE_REQUEST_ENTITY_TOO_LARGE = 'SavedObjectsClient/requestEntityTooLarge'; -export function decorateRequestEntityTooLargeError(error, reason) { - return decorate(error, CODE_REQUEST_ENTITY_TOO_LARGE, 413, reason); -} -export function isRequestEntityTooLargeError(error) { - return error && error[code] === CODE_REQUEST_ENTITY_TOO_LARGE; -} - -// 404 - Not Found -const CODE_NOT_FOUND = 'SavedObjectsClient/notFound'; -export function createGenericNotFoundError(type = null, id = null) { - if (type && id) { - return decorate(Boom.notFound(`Saved object [${type}/${id}] not found`), CODE_NOT_FOUND, 404); - } - return decorate(Boom.notFound(), CODE_NOT_FOUND, 404); -} -export function isNotFoundError(error) { - return error && error[code] === CODE_NOT_FOUND; -} - -// 409 - Conflict -const CODE_CONFLICT = 'SavedObjectsClient/conflict'; -export function decorateConflictError(error, reason) { - return decorate(error, CODE_CONFLICT, 409, reason); -} -export function isConflictError(error) { - return error && error[code] === CODE_CONFLICT; -} - -// 503 - Es Unavailable -const CODE_ES_UNAVAILABLE = 'SavedObjectsClient/esUnavailable'; -export function decorateEsUnavailableError(error, reason) { - return decorate(error, CODE_ES_UNAVAILABLE, 503, reason); -} -export function isEsUnavailableError(error) { - return error && error[code] === CODE_ES_UNAVAILABLE; -} - -// 503 - Unable to automatically create index because of action.auto_create_index setting -const CODE_ES_AUTO_CREATE_INDEX_ERROR = 'SavedObjectsClient/autoCreateIndex'; -export function createEsAutoCreateIndexError() { - const error = Boom.serverUnavailable('Automatic index creation failed'); - error.output.payload.code = 'ES_AUTO_CREATE_INDEX_ERROR'; - - return decorate(error, CODE_ES_AUTO_CREATE_INDEX_ERROR, 503); -} -export function isEsAutoCreateIndexError(error) { - return error && error[code] === CODE_ES_AUTO_CREATE_INDEX_ERROR; -} - -// 500 - General Error -const CODE_GENERAL_ERROR = 'SavedObjectsClient/generalError'; -export function decorateGeneralError(error, reason) { - return decorate(error, CODE_GENERAL_ERROR, 500, reason); -} diff --git a/src/legacy/server/saved_objects/service/lib/errors.test.js b/src/legacy/server/saved_objects/service/lib/errors.test.js deleted file mode 100644 index b9b437f345d7d..0000000000000 --- a/src/legacy/server/saved_objects/service/lib/errors.test.js +++ /dev/null @@ -1,387 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import Boom from 'boom'; - -import { - createBadRequestError, - createUnsupportedTypeError, - decorateBadRequestError, - isBadRequestError, - decorateNotAuthorizedError, - isNotAuthorizedError, - decorateForbiddenError, - isForbiddenError, - createGenericNotFoundError, - isNotFoundError, - decorateConflictError, - isConflictError, - decorateEsUnavailableError, - isEsUnavailableError, - decorateGeneralError, - isEsAutoCreateIndexError, - createEsAutoCreateIndexError, -} from './errors'; - -describe('savedObjectsClient/errorTypes', () => { - describe('BadRequest error', () => { - describe('createUnsupportedTypeError', () => { - const errorObj = createUnsupportedTypeError('someType'); - - it('should have the unsupported type message', () => { - expect(errorObj).toHaveProperty( - 'message', - "Unsupported saved object type: 'someType': Bad Request" - ); - }); - - it('has boom properties', () => { - expect(errorObj.output.payload).toMatchObject({ - statusCode: 400, - message: "Unsupported saved object type: 'someType': Bad Request", - error: 'Bad Request', - }); - }); - - it("should be identified by 'isBadRequestError' method", () => { - expect(isBadRequestError(errorObj)).toBeTruthy(); - }); - }); - - describe('createBadRequestError', () => { - const errorObj = createBadRequestError('test reason message'); - it('should create an appropriately structured error object', () => { - expect(errorObj.message).toEqual('test reason message: Bad Request'); - }); - - it("should be identified by 'isBadRequestError' method", () => { - expect(isBadRequestError(errorObj)).toBeTruthy(); - }); - - it('has boom properties', () => { - expect(errorObj.output.payload).toMatchObject({ - statusCode: 400, - message: 'test reason message: Bad Request', - error: 'Bad Request', - }); - }); - }); - - describe('decorateBadRequestError', () => { - it('returns original object', () => { - const error = new Error(); - expect(decorateBadRequestError(error)).toBe(error); - }); - - it('makes the error identifiable as a BadRequest error', () => { - const error = new Error(); - expect(isBadRequestError(error)).toBe(false); - decorateBadRequestError(error); - expect(isBadRequestError(error)).toBe(true); - }); - - it('adds boom properties', () => { - const error = decorateBadRequestError(new Error()); - expect(typeof error.output).toBe('object'); - expect(error.output.statusCode).toBe(400); - }); - - it('preserves boom properties of input', () => { - const error = Boom.notFound(); - decorateBadRequestError(error); - expect(error.output.statusCode).toBe(404); - }); - - describe('error.output', () => { - it('defaults to message of error', () => { - const error = decorateBadRequestError(new Error('foobar')); - expect(error.output.payload).toHaveProperty('message', 'foobar'); - }); - it('prefixes message with passed reason', () => { - const error = decorateBadRequestError(new Error('foobar'), 'biz'); - expect(error.output.payload).toHaveProperty('message', 'biz: foobar'); - }); - it('sets statusCode to 400', () => { - const error = decorateBadRequestError(new Error('foo')); - expect(error.output).toHaveProperty('statusCode', 400); - }); - }); - }); - }); - describe('NotAuthorized error', () => { - describe('decorateNotAuthorizedError', () => { - it('returns original object', () => { - const error = new Error(); - expect(decorateNotAuthorizedError(error)).toBe(error); - }); - - it('makes the error identifiable as a NotAuthorized error', () => { - const error = new Error(); - expect(isNotAuthorizedError(error)).toBe(false); - decorateNotAuthorizedError(error); - expect(isNotAuthorizedError(error)).toBe(true); - }); - - it('adds boom properties', () => { - const error = decorateNotAuthorizedError(new Error()); - expect(typeof error.output).toBe('object'); - expect(error.output.statusCode).toBe(401); - }); - - it('preserves boom properties of input', () => { - const error = Boom.notFound(); - decorateNotAuthorizedError(error); - expect(error.output.statusCode).toBe(404); - }); - - describe('error.output', () => { - it('defaults to message of error', () => { - const error = decorateNotAuthorizedError(new Error('foobar')); - expect(error.output.payload).toHaveProperty('message', 'foobar'); - }); - it('prefixes message with passed reason', () => { - const error = decorateNotAuthorizedError(new Error('foobar'), 'biz'); - expect(error.output.payload).toHaveProperty('message', 'biz: foobar'); - }); - it('sets statusCode to 401', () => { - const error = decorateNotAuthorizedError(new Error('foo')); - expect(error.output).toHaveProperty('statusCode', 401); - }); - }); - }); - }); - describe('Forbidden error', () => { - describe('decorateForbiddenError', () => { - it('returns original object', () => { - const error = new Error(); - expect(decorateForbiddenError(error)).toBe(error); - }); - - it('makes the error identifiable as a Forbidden error', () => { - const error = new Error(); - expect(isForbiddenError(error)).toBe(false); - decorateForbiddenError(error); - expect(isForbiddenError(error)).toBe(true); - }); - - it('adds boom properties', () => { - const error = decorateForbiddenError(new Error()); - expect(typeof error.output).toBe('object'); - expect(error.output.statusCode).toBe(403); - }); - - it('preserves boom properties of input', () => { - const error = Boom.notFound(); - decorateForbiddenError(error); - expect(error.output.statusCode).toBe(404); - }); - - describe('error.output', () => { - it('defaults to message of error', () => { - const error = decorateForbiddenError(new Error('foobar')); - expect(error.output.payload).toHaveProperty('message', 'foobar'); - }); - it('prefixes message with passed reason', () => { - const error = decorateForbiddenError(new Error('foobar'), 'biz'); - expect(error.output.payload).toHaveProperty('message', 'biz: foobar'); - }); - it('sets statusCode to 403', () => { - const error = decorateForbiddenError(new Error('foo')); - expect(error.output).toHaveProperty('statusCode', 403); - }); - }); - }); - }); - describe('NotFound error', () => { - describe('createGenericNotFoundError', () => { - it('makes an error identifiable as a NotFound error', () => { - const error = createGenericNotFoundError(); - expect(isNotFoundError(error)).toBe(true); - }); - - it('is a boom error, has boom properties', () => { - const error = createGenericNotFoundError(); - expect(error).toHaveProperty('isBoom'); - expect(typeof error.output).toBe('object'); - expect(error.output.statusCode).toBe(404); - }); - - describe('error.output', () => { - it('Uses "Not Found" message', () => { - const error = createGenericNotFoundError(); - expect(error.output.payload).toHaveProperty('message', 'Not Found'); - }); - it('sets statusCode to 404', () => { - const error = createGenericNotFoundError(); - expect(error.output).toHaveProperty('statusCode', 404); - }); - }); - }); - }); - describe('Conflict error', () => { - describe('decorateConflictError', () => { - it('returns original object', () => { - const error = new Error(); - expect(decorateConflictError(error)).toBe(error); - }); - - it('makes the error identifiable as a Conflict error', () => { - const error = new Error(); - expect(isConflictError(error)).toBe(false); - decorateConflictError(error); - expect(isConflictError(error)).toBe(true); - }); - - it('adds boom properties', () => { - const error = decorateConflictError(new Error()); - expect(typeof error.output).toBe('object'); - expect(error.output.statusCode).toBe(409); - }); - - it('preserves boom properties of input', () => { - const error = Boom.notFound(); - decorateConflictError(error); - expect(error.output.statusCode).toBe(404); - }); - - describe('error.output', () => { - it('defaults to message of error', () => { - const error = decorateConflictError(new Error('foobar')); - expect(error.output.payload).toHaveProperty('message', 'foobar'); - }); - it('prefixes message with passed reason', () => { - const error = decorateConflictError(new Error('foobar'), 'biz'); - expect(error.output.payload).toHaveProperty('message', 'biz: foobar'); - }); - it('sets statusCode to 409', () => { - const error = decorateConflictError(new Error('foo')); - expect(error.output).toHaveProperty('statusCode', 409); - }); - }); - }); - }); - describe('EsUnavailable error', () => { - describe('decorateEsUnavailableError', () => { - it('returns original object', () => { - const error = new Error(); - expect(decorateEsUnavailableError(error)).toBe(error); - }); - - it('makes the error identifiable as a EsUnavailable error', () => { - const error = new Error(); - expect(isEsUnavailableError(error)).toBe(false); - decorateEsUnavailableError(error); - expect(isEsUnavailableError(error)).toBe(true); - }); - - it('adds boom properties', () => { - const error = decorateEsUnavailableError(new Error()); - expect(typeof error.output).toBe('object'); - expect(error.output.statusCode).toBe(503); - }); - - it('preserves boom properties of input', () => { - const error = Boom.notFound(); - decorateEsUnavailableError(error); - expect(error.output.statusCode).toBe(404); - }); - - describe('error.output', () => { - it('defaults to message of error', () => { - const error = decorateEsUnavailableError(new Error('foobar')); - expect(error.output.payload).toHaveProperty('message', 'foobar'); - }); - it('prefixes message with passed reason', () => { - const error = decorateEsUnavailableError(new Error('foobar'), 'biz'); - expect(error.output.payload).toHaveProperty('message', 'biz: foobar'); - }); - it('sets statusCode to 503', () => { - const error = decorateEsUnavailableError(new Error('foo')); - expect(error.output).toHaveProperty('statusCode', 503); - }); - }); - }); - }); - describe('General error', () => { - describe('decorateGeneralError', () => { - it('returns original object', () => { - const error = new Error(); - expect(decorateGeneralError(error)).toBe(error); - }); - - it('adds boom properties', () => { - const error = decorateGeneralError(new Error()); - expect(typeof error.output).toBe('object'); - expect(error.output.statusCode).toBe(500); - }); - - it('preserves boom properties of input', () => { - const error = Boom.notFound(); - decorateGeneralError(error); - expect(error.output.statusCode).toBe(404); - }); - - describe('error.output', () => { - it('ignores error message', () => { - const error = decorateGeneralError(new Error('foobar')); - expect(error.output.payload.message).toMatch(/internal server error/i); - }); - it('sets statusCode to 500', () => { - const error = decorateGeneralError(new Error('foo')); - expect(error.output).toHaveProperty('statusCode', 500); - }); - }); - }); - }); - - describe('EsAutoCreateIndex error', () => { - describe('createEsAutoCreateIndexError', () => { - it('does not take an error argument', () => { - const error = new Error(); - expect(createEsAutoCreateIndexError(error)).not.toBe(error); - }); - - it('returns a new Error', () => { - expect(createEsAutoCreateIndexError()).toBeInstanceOf(Error); - }); - - it('makes errors identifiable as EsAutoCreateIndex errors', () => { - expect(isEsAutoCreateIndexError(createEsAutoCreateIndexError())).toBe(true); - }); - - it('returns a boom error', () => { - const error = createEsAutoCreateIndexError(); - expect(error).toHaveProperty('isBoom'); - expect(typeof error.output).toBe('object'); - expect(error.output.statusCode).toBe(503); - }); - - describe('error.output', () => { - it('uses "Automatic index creation failed" message', () => { - const error = createEsAutoCreateIndexError(); - expect(error.output.payload).toHaveProperty('message', 'Automatic index creation failed'); - }); - it('sets statusCode to 503', () => { - const error = createEsAutoCreateIndexError(); - expect(error.output).toHaveProperty('statusCode', 503); - }); - }); - }); - }); -}); diff --git a/src/legacy/server/saved_objects/service/lib/errors.test.ts b/src/legacy/server/saved_objects/service/lib/errors.test.ts new file mode 100644 index 0000000000000..facbacae84d07 --- /dev/null +++ b/src/legacy/server/saved_objects/service/lib/errors.test.ts @@ -0,0 +1,388 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Boom from 'boom'; + +import { + createBadRequestError, + createEsAutoCreateIndexError, + createGenericNotFoundError, + createUnsupportedTypeError, + decorateBadRequestError, + decorateConflictError, + decorateEsUnavailableError, + decorateForbiddenError, + decorateGeneralError, + decorateNotAuthorizedError, + isBadRequestError, + isConflictError, + isEsAutoCreateIndexError, + isEsUnavailableError, + isForbiddenError, + isNotAuthorizedError, + isNotFoundError, +} from './errors'; + +describe('savedObjectsClient/errorTypes', () => { + describe('BadRequest error', () => { + describe('createUnsupportedTypeError', () => { + const errorObj = createUnsupportedTypeError('someType'); + + it('should have the unsupported type message', () => { + expect(errorObj).toHaveProperty( + 'message', + "Unsupported saved object type: 'someType': Bad Request" + ); + }); + + it('has boom properties', () => { + expect(errorObj.output.payload).toMatchObject({ + statusCode: 400, + message: "Unsupported saved object type: 'someType': Bad Request", + error: 'Bad Request', + }); + }); + + it("should be identified by 'isBadRequestError' method", () => { + expect(isBadRequestError(errorObj)).toBeTruthy(); + }); + }); + + describe('createBadRequestError', () => { + const errorObj = createBadRequestError('test reason message'); + it('should create an appropriately structured error object', () => { + expect(errorObj.message).toEqual('test reason message: Bad Request'); + }); + + it("should be identified by 'isBadRequestError' method", () => { + expect(isBadRequestError(errorObj)).toBeTruthy(); + }); + + it('has boom properties', () => { + expect(errorObj.output.payload).toMatchObject({ + statusCode: 400, + message: 'test reason message: Bad Request', + error: 'Bad Request', + }); + }); + }); + + describe('decorateBadRequestError', () => { + it('returns original object', () => { + const error = new Error(); + expect(decorateBadRequestError(error)).toBe(error); + }); + + it('makes the error identifiable as a BadRequest error', () => { + const error = new Error(); + expect(isBadRequestError(error)).toBe(false); + decorateBadRequestError(error); + expect(isBadRequestError(error)).toBe(true); + }); + + it('adds boom properties', () => { + const error = decorateBadRequestError(new Error()); + expect(typeof error.output).toBe('object'); + expect(error.output.statusCode).toBe(400); + }); + + it('preserves boom properties of input', () => { + const error = Boom.notFound(); + decorateBadRequestError(error); + expect(error.output.statusCode).toBe(404); + }); + + describe('error.output', () => { + it('defaults to message of error', () => { + const error = decorateBadRequestError(new Error('foobar')); + expect(error.output.payload).toHaveProperty('message', 'foobar'); + }); + it('prefixes message with passed reason', () => { + const error = decorateBadRequestError(new Error('foobar'), 'biz'); + expect(error.output.payload).toHaveProperty('message', 'biz: foobar'); + }); + it('sets statusCode to 400', () => { + const error = decorateBadRequestError(new Error('foo')); + expect(error.output).toHaveProperty('statusCode', 400); + }); + }); + }); + }); + describe('NotAuthorized error', () => { + describe('decorateNotAuthorizedError', () => { + it('returns original object', () => { + const error = new Error(); + expect(decorateNotAuthorizedError(error)).toBe(error); + }); + + it('makes the error identifiable as a NotAuthorized error', () => { + const error = new Error(); + expect(isNotAuthorizedError(error)).toBe(false); + decorateNotAuthorizedError(error); + expect(isNotAuthorizedError(error)).toBe(true); + }); + + it('adds boom properties', () => { + const error = decorateNotAuthorizedError(new Error()); + expect(typeof error.output).toBe('object'); + expect(error.output.statusCode).toBe(401); + }); + + it('preserves boom properties of input', () => { + const error = Boom.notFound(); + decorateNotAuthorizedError(error); + expect(error.output.statusCode).toBe(404); + }); + + describe('error.output', () => { + it('defaults to message of error', () => { + const error = decorateNotAuthorizedError(new Error('foobar')); + expect(error.output.payload).toHaveProperty('message', 'foobar'); + }); + it('prefixes message with passed reason', () => { + const error = decorateNotAuthorizedError(new Error('foobar'), 'biz'); + expect(error.output.payload).toHaveProperty('message', 'biz: foobar'); + }); + it('sets statusCode to 401', () => { + const error = decorateNotAuthorizedError(new Error('foo')); + expect(error.output).toHaveProperty('statusCode', 401); + }); + }); + }); + }); + describe('Forbidden error', () => { + describe('decorateForbiddenError', () => { + it('returns original object', () => { + const error = new Error(); + expect(decorateForbiddenError(error)).toBe(error); + }); + + it('makes the error identifiable as a Forbidden error', () => { + const error = new Error(); + expect(isForbiddenError(error)).toBe(false); + decorateForbiddenError(error); + expect(isForbiddenError(error)).toBe(true); + }); + + it('adds boom properties', () => { + const error = decorateForbiddenError(new Error()); + expect(typeof error.output).toBe('object'); + expect(error.output.statusCode).toBe(403); + }); + + it('preserves boom properties of input', () => { + const error = Boom.notFound(); + decorateForbiddenError(error); + expect(error.output.statusCode).toBe(404); + }); + + describe('error.output', () => { + it('defaults to message of error', () => { + const error = decorateForbiddenError(new Error('foobar')); + expect(error.output.payload).toHaveProperty('message', 'foobar'); + }); + it('prefixes message with passed reason', () => { + const error = decorateForbiddenError(new Error('foobar'), 'biz'); + expect(error.output.payload).toHaveProperty('message', 'biz: foobar'); + }); + it('sets statusCode to 403', () => { + const error = decorateForbiddenError(new Error('foo')); + expect(error.output).toHaveProperty('statusCode', 403); + }); + }); + }); + }); + describe('NotFound error', () => { + describe('createGenericNotFoundError', () => { + it('makes an error identifiable as a NotFound error', () => { + const error = createGenericNotFoundError(); + expect(isNotFoundError(error)).toBe(true); + }); + + it('is a boom error, has boom properties', () => { + const error = createGenericNotFoundError(); + expect(error).toHaveProperty('isBoom'); + expect(typeof error.output).toBe('object'); + expect(error.output.statusCode).toBe(404); + }); + + describe('error.output', () => { + it('Uses "Not Found" message', () => { + const error = createGenericNotFoundError(); + expect(error.output.payload).toHaveProperty('message', 'Not Found'); + }); + it('sets statusCode to 404', () => { + const error = createGenericNotFoundError(); + expect(error.output).toHaveProperty('statusCode', 404); + }); + }); + }); + }); + describe('Conflict error', () => { + describe('decorateConflictError', () => { + it('returns original object', () => { + const error = new Error(); + expect(decorateConflictError(error)).toBe(error); + }); + + it('makes the error identifiable as a Conflict error', () => { + const error = new Error(); + expect(isConflictError(error)).toBe(false); + decorateConflictError(error); + expect(isConflictError(error)).toBe(true); + }); + + it('adds boom properties', () => { + const error = decorateConflictError(new Error()); + expect(typeof error.output).toBe('object'); + expect(error.output.statusCode).toBe(409); + }); + + it('preserves boom properties of input', () => { + const error = Boom.notFound(); + decorateConflictError(error); + expect(error.output.statusCode).toBe(404); + }); + + describe('error.output', () => { + it('defaults to message of error', () => { + const error = decorateConflictError(new Error('foobar')); + expect(error.output.payload).toHaveProperty('message', 'foobar'); + }); + it('prefixes message with passed reason', () => { + const error = decorateConflictError(new Error('foobar'), 'biz'); + expect(error.output.payload).toHaveProperty('message', 'biz: foobar'); + }); + it('sets statusCode to 409', () => { + const error = decorateConflictError(new Error('foo')); + expect(error.output).toHaveProperty('statusCode', 409); + }); + }); + }); + }); + describe('EsUnavailable error', () => { + describe('decorateEsUnavailableError', () => { + it('returns original object', () => { + const error = new Error(); + expect(decorateEsUnavailableError(error)).toBe(error); + }); + + it('makes the error identifiable as a EsUnavailable error', () => { + const error = new Error(); + expect(isEsUnavailableError(error)).toBe(false); + decorateEsUnavailableError(error); + expect(isEsUnavailableError(error)).toBe(true); + }); + + it('adds boom properties', () => { + const error = decorateEsUnavailableError(new Error()); + expect(typeof error.output).toBe('object'); + expect(error.output.statusCode).toBe(503); + }); + + it('preserves boom properties of input', () => { + const error = Boom.notFound(); + decorateEsUnavailableError(error); + expect(error.output.statusCode).toBe(404); + }); + + describe('error.output', () => { + it('defaults to message of error', () => { + const error = decorateEsUnavailableError(new Error('foobar')); + expect(error.output.payload).toHaveProperty('message', 'foobar'); + }); + it('prefixes message with passed reason', () => { + const error = decorateEsUnavailableError(new Error('foobar'), 'biz'); + expect(error.output.payload).toHaveProperty('message', 'biz: foobar'); + }); + it('sets statusCode to 503', () => { + const error = decorateEsUnavailableError(new Error('foo')); + expect(error.output).toHaveProperty('statusCode', 503); + }); + }); + }); + }); + describe('General error', () => { + describe('decorateGeneralError', () => { + it('returns original object', () => { + const error = new Error(); + expect(decorateGeneralError(error)).toBe(error); + }); + + it('adds boom properties', () => { + const error = decorateGeneralError(new Error()); + expect(typeof error.output).toBe('object'); + expect(error.output.statusCode).toBe(500); + }); + + it('preserves boom properties of input', () => { + const error = Boom.notFound(); + decorateGeneralError(error); + expect(error.output.statusCode).toBe(404); + }); + + describe('error.output', () => { + it('ignores error message', () => { + const error = decorateGeneralError(new Error('foobar')); + expect(error.output.payload.message).toMatch(/internal server error/i); + }); + it('sets statusCode to 500', () => { + const error = decorateGeneralError(new Error('foo')); + expect(error.output).toHaveProperty('statusCode', 500); + }); + }); + }); + }); + + describe('EsAutoCreateIndex error', () => { + describe('createEsAutoCreateIndexError', () => { + it('does not take an error argument', () => { + const error = new Error(); + // @ts-ignore + expect(createEsAutoCreateIndexError(error)).not.toBe(error); + }); + + it('returns a new Error', () => { + expect(createEsAutoCreateIndexError()).toBeInstanceOf(Error); + }); + + it('makes errors identifiable as EsAutoCreateIndex errors', () => { + expect(isEsAutoCreateIndexError(createEsAutoCreateIndexError())).toBe(true); + }); + + it('returns a boom error', () => { + const error = createEsAutoCreateIndexError(); + expect(error).toHaveProperty('isBoom'); + expect(typeof error.output).toBe('object'); + expect(error.output.statusCode).toBe(503); + }); + + describe('error.output', () => { + it('uses "Automatic index creation failed" message', () => { + const error = createEsAutoCreateIndexError(); + expect(error.output.payload).toHaveProperty('message', 'Automatic index creation failed'); + }); + it('sets statusCode to 503', () => { + const error = createEsAutoCreateIndexError(); + expect(error.output).toHaveProperty('statusCode', 503); + }); + }); + }); + }); +}); diff --git a/src/legacy/server/saved_objects/service/lib/errors.ts b/src/legacy/server/saved_objects/service/lib/errors.ts new file mode 100644 index 0000000000000..e1df155022b11 --- /dev/null +++ b/src/legacy/server/saved_objects/service/lib/errors.ts @@ -0,0 +1,151 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Boom from 'boom'; + +const code = Symbol('SavedObjectsClientErrorCode'); + +interface DecoratedError extends Boom { + [code]?: string; +} + +function decorate( + error: Error | DecoratedError, + errorCode: string, + statusCode: number, + message?: string +): DecoratedError { + if (isSavedObjectsClientError(error)) { + return error; + } + + const boom = Boom.boomify(error, { + statusCode, + message, + override: false, + }) as DecoratedError; + + boom[code] = errorCode; + + return boom; +} + +export function isSavedObjectsClientError(error: any): error is DecoratedError { + return Boolean(error && error[code]); +} + +// 400 - badRequest +const CODE_BAD_REQUEST = 'SavedObjectsClient/badRequest'; +export function decorateBadRequestError(error: Error, reason?: string) { + return decorate(error, CODE_BAD_REQUEST, 400, reason); +} +export function createBadRequestError(reason?: string) { + return decorateBadRequestError(new Error('Bad Request'), reason); +} +export function createUnsupportedTypeError(type: string) { + return createBadRequestError(`Unsupported saved object type: '${type}'`); +} +export function isBadRequestError(error: Error | DecoratedError) { + return isSavedObjectsClientError(error) && error[code] === CODE_BAD_REQUEST; +} + +// 400 - invalid version +const CODE_INVALID_VERSION = 'SavedObjectsClient/invalidVersion'; +export function createInvalidVersionError(versionInput?: string) { + return decorate(Boom.badRequest(`Invalid version [${versionInput}]`), CODE_INVALID_VERSION, 400); +} +export function isInvalidVersionError(error: Error | DecoratedError) { + return isSavedObjectsClientError(error) && error[code] === CODE_INVALID_VERSION; +} + +// 401 - Not Authorized +const CODE_NOT_AUTHORIZED = 'SavedObjectsClient/notAuthorized'; +export function decorateNotAuthorizedError(error: Error, reason?: string) { + return decorate(error, CODE_NOT_AUTHORIZED, 401, reason); +} +export function isNotAuthorizedError(error: Error | DecoratedError) { + return isSavedObjectsClientError(error) && error[code] === CODE_NOT_AUTHORIZED; +} + +// 403 - Forbidden +const CODE_FORBIDDEN = 'SavedObjectsClient/forbidden'; +export function decorateForbiddenError(error: Error, reason?: string) { + return decorate(error, CODE_FORBIDDEN, 403, reason); +} +export function isForbiddenError(error: Error | DecoratedError) { + return isSavedObjectsClientError(error) && error[code] === CODE_FORBIDDEN; +} + +// 413 - Request Entity Too Large +const CODE_REQUEST_ENTITY_TOO_LARGE = 'SavedObjectsClient/requestEntityTooLarge'; +export function decorateRequestEntityTooLargeError(error: Error, reason?: string) { + return decorate(error, CODE_REQUEST_ENTITY_TOO_LARGE, 413, reason); +} +export function isRequestEntityTooLargeError(error: Error | DecoratedError) { + return isSavedObjectsClientError(error) && error[code] === CODE_REQUEST_ENTITY_TOO_LARGE; +} + +// 404 - Not Found +const CODE_NOT_FOUND = 'SavedObjectsClient/notFound'; +export function createGenericNotFoundError(type: string | null = null, id: string | null = null) { + if (type && id) { + return decorate(Boom.notFound(`Saved object [${type}/${id}] not found`), CODE_NOT_FOUND, 404); + } + return decorate(Boom.notFound(), CODE_NOT_FOUND, 404); +} +export function isNotFoundError(error: Error | DecoratedError) { + return isSavedObjectsClientError(error) && error[code] === CODE_NOT_FOUND; +} + +// 409 - Conflict +const CODE_CONFLICT = 'SavedObjectsClient/conflict'; +export function decorateConflictError(error: Error, reason?: string) { + return decorate(error, CODE_CONFLICT, 409, reason); +} +export function isConflictError(error: Error | DecoratedError) { + return isSavedObjectsClientError(error) && error[code] === CODE_CONFLICT; +} + +// 503 - Es Unavailable +const CODE_ES_UNAVAILABLE = 'SavedObjectsClient/esUnavailable'; +export function decorateEsUnavailableError(error: Error, reason?: string) { + return decorate(error, CODE_ES_UNAVAILABLE, 503, reason); +} +export function isEsUnavailableError(error: Error | DecoratedError) { + return isSavedObjectsClientError(error) && error[code] === CODE_ES_UNAVAILABLE; +} + +// 503 - Unable to automatically create index because of action.auto_create_index setting +const CODE_ES_AUTO_CREATE_INDEX_ERROR = 'SavedObjectsClient/autoCreateIndex'; +export function createEsAutoCreateIndexError() { + const error = Boom.serverUnavailable('Automatic index creation failed'); + error.output.payload.attributes = error.output.payload.attributes || {}; + error.output.payload.attributes.code = 'ES_AUTO_CREATE_INDEX_ERROR'; + + return decorate(error, CODE_ES_AUTO_CREATE_INDEX_ERROR, 503); +} +export function isEsAutoCreateIndexError(error: Error | DecoratedError) { + return isSavedObjectsClientError(error) && error[code] === CODE_ES_AUTO_CREATE_INDEX_ERROR; +} + +// 500 - General Error +const CODE_GENERAL_ERROR = 'SavedObjectsClient/generalError'; +export function decorateGeneralError(error: Error, reason?: string) { + return decorate(error, CODE_GENERAL_ERROR, 500, reason); +} diff --git a/src/legacy/server/saved_objects/service/lib/included_fields.js b/src/legacy/server/saved_objects/service/lib/included_fields.js deleted file mode 100644 index ce972d89afeae..0000000000000 --- a/src/legacy/server/saved_objects/service/lib/included_fields.js +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * Provides an array of paths for ES source filtering - * - * @param {string} type - * @param {string|array} fields - * @returns {array} - */ -export function includedFields(type, fields) { - if (!fields || fields.length === 0) return; - - // convert to an array - const sourceFields = typeof fields === 'string' ? [fields] : fields; - const sourceType = type || '*'; - - return sourceFields - .map(f => `${sourceType}.${f}`) - .concat('namespace') - .concat('type') - .concat('references') - .concat('migrationVersion') - .concat('updated_at') - .concat(fields); // v5 compatibility -} diff --git a/src/legacy/server/saved_objects/service/lib/included_fields.test.js b/src/legacy/server/saved_objects/service/lib/included_fields.test.js deleted file mode 100644 index d0b01638aff1a..0000000000000 --- a/src/legacy/server/saved_objects/service/lib/included_fields.test.js +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { includedFields } from './included_fields'; - -describe('includedFields', () => { - it('returns undefined if fields are not provided', () => { - expect(includedFields()).toBe(undefined); - }); - - it('includes type', () => { - const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(7); - expect(fields).toContain('type'); - }); - - it('includes namespace', () => { - const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(7); - expect(fields).toContain('namespace'); - }); - - it('includes references', () => { - const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(7); - expect(fields).toContain('references'); - }); - - it('includes migrationVersion', () => { - const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(7); - expect(fields).toContain('migrationVersion'); - }); - - it('includes updated_at', () => { - const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(7); - expect(fields).toContain('updated_at'); - }); - - it('accepts field as string', () => { - const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(7); - expect(fields).toContain('config.foo'); - }); - - it('accepts fields as an array', () => { - const fields = includedFields('config', ['foo', 'bar']); - - expect(fields).toHaveLength(9); - expect(fields).toContain('config.foo'); - expect(fields).toContain('config.bar'); - }); - - it('uses wildcard when type is not provided', () => { - const fields = includedFields(undefined, 'foo'); - expect(fields).toHaveLength(7); - expect(fields).toContain('*.foo'); - }); - - describe('v5 compatibility', () => { - it('includes legacy field path', () => { - const fields = includedFields('config', ['foo', 'bar']); - - expect(fields).toHaveLength(9); - expect(fields).toContain('foo'); - expect(fields).toContain('bar'); - }); - }); -}); diff --git a/src/legacy/server/saved_objects/service/lib/included_fields.test.ts b/src/legacy/server/saved_objects/service/lib/included_fields.test.ts new file mode 100644 index 0000000000000..40d6552c2ad5f --- /dev/null +++ b/src/legacy/server/saved_objects/service/lib/included_fields.test.ts @@ -0,0 +1,121 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { includedFields } from './included_fields'; + +describe('includedFields', () => { + it('returns undefined if fields are not provided', () => { + expect(includedFields()).toBe(undefined); + }); + + it('accepts type string', () => { + const fields = includedFields('config', 'foo'); + expect(fields).toHaveLength(7); + expect(fields).toContain('type'); + }); + + it('accepts type as string array', () => { + const fields = includedFields(['config', 'secret'], 'foo'); + expect(fields).toMatchInlineSnapshot(` +Array [ + "config.foo", + "secret.foo", + "namespace", + "type", + "references", + "migrationVersion", + "updated_at", + "foo", +] +`); + }); + + it('accepts field as string', () => { + const fields = includedFields('config', 'foo'); + expect(fields).toHaveLength(7); + expect(fields).toContain('config.foo'); + }); + + it('accepts fields as an array', () => { + const fields = includedFields('config', ['foo', 'bar']); + + expect(fields).toHaveLength(9); + expect(fields).toContain('config.foo'); + expect(fields).toContain('config.bar'); + }); + + it('accepts type as string array and fields as string array', () => { + const fields = includedFields(['config', 'secret'], ['foo', 'bar']); + expect(fields).toMatchInlineSnapshot(` +Array [ + "config.foo", + "config.bar", + "secret.foo", + "secret.bar", + "namespace", + "type", + "references", + "migrationVersion", + "updated_at", + "foo", + "bar", +] +`); + }); + + it('includes namespace', () => { + const fields = includedFields('config', 'foo'); + expect(fields).toHaveLength(7); + expect(fields).toContain('namespace'); + }); + + it('includes references', () => { + const fields = includedFields('config', 'foo'); + expect(fields).toHaveLength(7); + expect(fields).toContain('references'); + }); + + it('includes migrationVersion', () => { + const fields = includedFields('config', 'foo'); + expect(fields).toHaveLength(7); + expect(fields).toContain('migrationVersion'); + }); + + it('includes updated_at', () => { + const fields = includedFields('config', 'foo'); + expect(fields).toHaveLength(7); + expect(fields).toContain('updated_at'); + }); + + it('uses wildcard when type is not provided', () => { + const fields = includedFields(undefined, 'foo'); + expect(fields).toHaveLength(7); + expect(fields).toContain('*.foo'); + }); + + describe('v5 compatibility', () => { + it('includes legacy field path', () => { + const fields = includedFields('config', ['foo', 'bar']); + + expect(fields).toHaveLength(9); + expect(fields).toContain('foo'); + expect(fields).toContain('bar'); + }); + }); +}); diff --git a/src/legacy/server/saved_objects/service/lib/included_fields.ts b/src/legacy/server/saved_objects/service/lib/included_fields.ts new file mode 100644 index 0000000000000..f372db5a1a635 --- /dev/null +++ b/src/legacy/server/saved_objects/service/lib/included_fields.ts @@ -0,0 +1,45 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +function toArray(value: string | string[]): string[] { + return typeof value === 'string' ? [value] : value; +} +/** + * Provides an array of paths for ES source filtering + */ +export function includedFields(type: string | string[] = '*', fields?: string[] | string) { + if (!fields || fields.length === 0) { + return; + } + + // convert to an array + const sourceFields = toArray(fields); + const sourceType = toArray(type); + + return sourceType + .reduce((acc: string[], t) => { + return [...acc, ...sourceFields.map(f => `${t}.${f}`)]; + }, []) + .concat('namespace') + .concat('type') + .concat('references') + .concat('migrationVersion') + .concat('updated_at') + .concat(fields); // v5 compatibility +} diff --git a/src/legacy/server/saved_objects/service/lib/index.d.ts b/src/legacy/server/saved_objects/service/lib/index.d.ts deleted file mode 100644 index 486d3b1b46bb1..0000000000000 --- a/src/legacy/server/saved_objects/service/lib/index.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import errors from './errors'; - -export { errors }; - -export { SavedObjectsRepository, SavedObjectsRepositoryOptions } from './repository'; - -export { - SavedObjectsClientWrapperFactory, - SavedObjectsClientWrapperOptions, - ScopedSavedObjectsClientProvider, -} from './scoped_client_provider'; diff --git a/src/legacy/server/saved_objects/service/lib/index.js b/src/legacy/server/saved_objects/service/lib/index.js deleted file mode 100644 index 5851fc8568e4c..0000000000000 --- a/src/legacy/server/saved_objects/service/lib/index.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { SavedObjectsRepository } from './repository'; -export { ScopedSavedObjectsClientProvider } from './scoped_client_provider'; - -import * as errors from './errors'; -export { errors }; diff --git a/src/legacy/server/saved_objects/service/lib/index.ts b/src/legacy/server/saved_objects/service/lib/index.ts new file mode 100644 index 0000000000000..68fa240584100 --- /dev/null +++ b/src/legacy/server/saved_objects/service/lib/index.ts @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { SavedObjectsRepository, SavedObjectsRepositoryOptions } from './repository'; +export { + SavedObjectsClientWrapperFactory, + SavedObjectsClientWrapperOptions, + ScopedSavedObjectsClientProvider, +} from './scoped_client_provider'; + +import * as errors from './errors'; +export { errors }; diff --git a/src/legacy/server/saved_objects/service/lib/repository.d.ts b/src/legacy/server/saved_objects/service/lib/repository.d.ts deleted file mode 100644 index 488c886f12e46..0000000000000 --- a/src/legacy/server/saved_objects/service/lib/repository.d.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { BaseOptions, SavedObject } from '../saved_objects_client'; - -export interface SavedObjectsRepositoryOptions { - index: string | string[]; - mappings: unknown; - callCluster: unknown; - schema: unknown; - serializer: unknown; - migrator: unknown; - onBeforeWrite: unknown; -} - -export declare class SavedObjectsRepository { - // ATTENTION: this interface is incomplete - - public get: (type: string, id: string, options?: BaseOptions) => Promise; - public incrementCounter: ( - type: string, - id: string, - counterFieldName: string, - options?: BaseOptions - ) => Promise; - - constructor(options: SavedObjectsRepositoryOptions); -} diff --git a/src/legacy/server/saved_objects/service/lib/repository.js b/src/legacy/server/saved_objects/service/lib/repository.js deleted file mode 100644 index c132643cffa98..0000000000000 --- a/src/legacy/server/saved_objects/service/lib/repository.js +++ /dev/null @@ -1,688 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { omit } from 'lodash'; -import { getRootPropertiesObjects } from '../../../mappings'; -import { getSearchDsl } from './search_dsl'; -import { includedFields } from './included_fields'; -import { decorateEsError } from './decorate_es_error'; -import * as errors from './errors'; -import { decodeRequestVersion, encodeVersion, encodeHitVersion } from '../../version'; - -// BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository -// so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient. - -export class SavedObjectsRepository { - constructor(options) { - const { - index, - mappings, - callCluster, - schema, - serializer, - migrator, - allowedTypes = [], - onBeforeWrite = () => {}, - } = options; - - // It's important that we migrate documents / mark them as up-to-date - // prior to writing them to the index. Otherwise, we'll cause unecessary - // index migrations to run at Kibana startup, and those will probably fail - // due to invalidly versioned documents in the index. - // - // The migrator performs double-duty, and validates the documents prior - // to returning them. - this._migrator = migrator; - this._index = index; - this._mappings = mappings; - this._schema = schema; - if (allowedTypes.length === 0) { - throw new Error('Empty or missing types for saved object repository!'); - } - this._allowedTypes = allowedTypes; - - this._onBeforeWrite = onBeforeWrite; - this._unwrappedCallCluster = async (...args) => { - await migrator.awaitMigration(); - return callCluster(...args); - }; - this._schema = schema; - this._serializer = serializer; - } - - /** - * Persists an object - * - * @param {string} type - * @param {object} attributes - * @param {object} [options={}] - * @property {string} [options.id] - force id on creation, not recommended - * @property {boolean} [options.overwrite=false] - * @property {object} [options.migrationVersion=undefined] - * @property {string} [options.namespace] - * @property {array} [options.references] - [{ name, type, id }] - * @returns {promise} - { id, type, version, attributes } - */ - async create(type, attributes = {}, options = {}) { - const { id, migrationVersion, overwrite = false, namespace, references = [] } = options; - - if (!this._isTypeAllowed(type)) { - throw errors.createUnsupportedTypeError(type); - } - - const method = id && !overwrite ? 'create' : 'index'; - const time = this._getCurrentTime(); - - try { - const migrated = this._migrator.migrateDocument({ - id, - type, - namespace, - attributes, - migrationVersion, - updated_at: time, - references, - }); - - const raw = this._serializer.savedObjectToRaw(migrated); - - const response = await this._writeToCluster(method, { - id: raw._id, - index: this._index, - refresh: 'wait_for', - body: raw._source, - }); - - return this._rawToSavedObject({ - ...raw, - ...response, - }); - } catch (error) { - if (errors.isNotFoundError(error)) { - // See "503s from missing index" above - throw errors.createEsAutoCreateIndexError(); - } - - throw error; - } - } - - /** - * Creates multiple documents at once - * - * @param {array} objects - [{ type, id, attributes, references, migrationVersion }] - * @param {object} [options={}] - * @property {boolean} [options.overwrite=false] - overwrites existing documents - * @property {string} [options.namespace] - * @returns {promise} - {saved_objects: [[{ id, type, version, references, attributes, error: { message } }]} - */ - async bulkCreate(objects, options = {}) { - const { namespace, overwrite = false } = options; - const time = this._getCurrentTime(); - const bulkCreateParams = []; - - let requestIndexCounter = 0; - const expectedResults = objects.map(object => { - if (!this._isTypeAllowed(object.type)) { - return { - response: { - id: object.id, - type: object.type, - error: errors.createUnsupportedTypeError(object.type).output.payload, - }, - }; - } - - const method = object.id && !overwrite ? 'create' : 'index'; - const expectedResult = { - esRequestIndex: requestIndexCounter++, - requestedId: object.id, - rawMigratedDoc: this._serializer.savedObjectToRaw( - this._migrator.migrateDocument({ - id: object.id, - type: object.type, - attributes: object.attributes, - migrationVersion: object.migrationVersion, - namespace, - updated_at: time, - references: object.references || [], - }) - ), - }; - - bulkCreateParams.push( - { - [method]: { - _id: expectedResult.rawMigratedDoc._id, - }, - }, - expectedResult.rawMigratedDoc._source - ); - - return expectedResult; - }); - - const esResponse = await this._writeToCluster('bulk', { - index: this._index, - refresh: 'wait_for', - body: bulkCreateParams, - }); - - return { - saved_objects: expectedResults.map(expectedResult => { - if (expectedResult.response) { - return expectedResult.response; - } - - const { requestedId, rawMigratedDoc, esRequestIndex } = expectedResult; - const response = esResponse.items[esRequestIndex]; - const { - error, - _id: responseId, - _seq_no: seqNo, - _primary_term: primaryTerm, - } = Object.values(response)[0]; - - const { - _source: { type, [type]: attributes, references = [] }, - } = rawMigratedDoc; - - const id = requestedId || responseId; - if (error) { - if (error.type === 'version_conflict_engine_exception') { - return { - id, - type, - error: { statusCode: 409, message: 'version conflict, document already exists' }, - }; - } - return { - id, - type, - error: { - message: error.reason || JSON.stringify(error), - }, - }; - } - - return { - id, - type, - updated_at: time, - version: encodeVersion(seqNo, primaryTerm), - attributes, - references, - }; - }), - }; - } - - /** - * Deletes an object - * - * @param {string} type - * @param {string} id - * @param {object} [options={}] - * @property {string} [options.namespace] - * @returns {promise} - */ - async delete(type, id, options = {}) { - if (!this._isTypeAllowed(type)) { - throw errors.createGenericNotFoundError(); - } - - const { namespace } = options; - - const response = await this._writeToCluster('delete', { - id: this._serializer.generateRawId(namespace, type, id), - index: this._index, - refresh: 'wait_for', - ignore: [404], - }); - - const deleted = response.result === 'deleted'; - if (deleted) { - return {}; - } - - const docNotFound = response.result === 'not_found'; - const indexNotFound = response.error && response.error.type === 'index_not_found_exception'; - if (docNotFound || indexNotFound) { - // see "404s from missing index" above - throw errors.createGenericNotFoundError(type, id); - } - - throw new Error( - `Unexpected Elasticsearch DELETE response: ${JSON.stringify({ type, id, response })}` - ); - } - - /** - * Deletes all objects from the provided namespace. - * - * @param {string} namespace - * @returns {promise} - { took, timed_out, total, deleted, batches, version_conflicts, noops, retries, failures } - */ - async deleteByNamespace(namespace) { - if (!namespace || typeof namespace !== 'string') { - throw new TypeError(`namespace is required, and must be a string`); - } - - const allTypes = Object.keys(getRootPropertiesObjects(this._mappings)); - - const typesToDelete = allTypes.filter(type => !this._schema.isNamespaceAgnostic(type)); - - const esOptions = { - index: this._index, - ignore: [404], - refresh: 'wait_for', - body: { - conflicts: 'proceed', - ...getSearchDsl(this._mappings, this._schema, { - namespace, - type: typesToDelete, - }), - }, - }; - - return await this._writeToCluster('deleteByQuery', esOptions); - } - - /** - * @param {object} [options={}] - * @property {(string|Array)} [options.type] - * @property {string} [options.search] - * @property {string} [options.defaultSearchOperator] - * @property {Array} [options.searchFields] - see Elasticsearch Simple Query String - * Query field argument for more information - * @property {integer} [options.page=1] - * @property {integer} [options.perPage=20] - * @property {string} [options.sortField] - * @property {string} [options.sortOrder] - * @property {Array} [options.fields] - * @property {string} [options.namespace] - * @property {object} [options.hasReference] - { type, id } - * @returns {promise} - { saved_objects: [{ id, type, version, attributes }], total, per_page, page } - */ - async find(options = {}) { - const { - search, - defaultSearchOperator = 'OR', - searchFields, - hasReference, - page = 1, - perPage = 20, - sortField, - sortOrder, - fields, - namespace, - } = options; - let { type } = options; - - if (!type) { - throw new TypeError(`options.type must be a string or an array of strings`); - } - - if (Array.isArray(type)) { - type = type.filter(type => this._isTypeAllowed(type)); - if (type.length === 0) { - return { - page, - per_page: perPage, - total: 0, - saved_objects: [], - }; - } - } else { - if (!this._isTypeAllowed(type)) { - return { - page, - per_page: perPage, - total: 0, - saved_objects: [], - }; - } - } - - if (searchFields && !Array.isArray(searchFields)) { - throw new TypeError('options.searchFields must be an array'); - } - - if (fields && !Array.isArray(fields)) { - throw new TypeError('options.fields must be an array'); - } - - const esOptions = { - index: this._index, - size: perPage, - from: perPage * (page - 1), - _source: includedFields(type, fields), - ignore: [404], - rest_total_hits_as_int: true, - body: { - seq_no_primary_term: true, - ...getSearchDsl(this._mappings, this._schema, { - search, - defaultSearchOperator, - searchFields, - type, - sortField, - sortOrder, - namespace, - hasReference, - }), - }, - }; - - const response = await this._callCluster('search', esOptions); - - if (response.status === 404) { - // 404 is only possible here if the index is missing, which - // we don't want to leak, see "404s from missing index" above - return { - page, - per_page: perPage, - total: 0, - saved_objects: [], - }; - } - - return { - page, - per_page: perPage, - total: response.hits.total, - saved_objects: response.hits.hits.map(hit => this._rawToSavedObject(hit)), - }; - } - - /** - * Returns an array of objects by id - * - * @param {array} objects - an array of objects containing id, type and optionally fields - * @param {object} [options={}] - * @property {string} [options.namespace] - * @returns {promise} - { saved_objects: [{ id, type, version, attributes }] } - * @example - * - * bulkGet([ - * { id: 'one', type: 'config' }, - * { id: 'foo', type: 'index-pattern' } - * ]) - */ - async bulkGet(objects = [], options = {}) { - const { namespace } = options; - - if (objects.length === 0) { - return { saved_objects: [] }; - } - - const unsupportedTypes = []; - const response = await this._callCluster('mget', { - index: this._index, - body: { - docs: objects.reduce((acc, { type, id, fields }) => { - if (this._isTypeAllowed(type)) { - acc.push({ - _id: this._serializer.generateRawId(namespace, type, id), - _source: includedFields(type, fields), - }); - } else { - unsupportedTypes.push({ - id, - type, - error: errors.createUnsupportedTypeError(type).output.payload, - }); - } - return acc; - }, []), - }, - }); - - return { - saved_objects: response.docs - .map((doc, i) => { - const { id, type } = objects[i]; - - if (!doc.found) { - return { - id, - type, - error: { statusCode: 404, message: 'Not found' }, - }; - } - - const time = doc._source.updated_at; - return { - id, - type, - ...(time && { updated_at: time }), - version: encodeHitVersion(doc), - attributes: doc._source[type], - references: doc._source.references || [], - migrationVersion: doc._source.migrationVersion, - }; - }) - .concat(unsupportedTypes), - }; - } - - /** - * Gets a single object - * - * @param {string} type - * @param {string} id - * @param {object} [options={}] - * @property {string} [options.namespace] - * @returns {promise} - { id, type, version, attributes } - */ - async get(type, id, options = {}) { - if (!this._isTypeAllowed(type)) { - throw errors.createGenericNotFoundError(type, id); - } - - const { namespace } = options; - - const response = await this._callCluster('get', { - id: this._serializer.generateRawId(namespace, type, id), - index: this._index, - ignore: [404], - }); - - const docNotFound = response.found === false; - const indexNotFound = response.status === 404; - if (docNotFound || indexNotFound) { - // see "404s from missing index" above - throw errors.createGenericNotFoundError(type, id); - } - - const { updated_at: updatedAt } = response._source; - - return { - id, - type, - ...(updatedAt && { updated_at: updatedAt }), - version: encodeHitVersion(response), - attributes: response._source[type], - references: response._source.references || [], - migrationVersion: response._source.migrationVersion, - }; - } - - /** - * Updates an object - * - * @param {string} type - * @param {string} id - * @param {object} [options={}] - * @property {string} options.version - ensures version matches that of persisted object - * @property {string} [options.namespace] - * @property {array} [options.references] - [{ name, type, id }] - * @returns {promise} - */ - async update(type, id, attributes, options = {}) { - if (!this._isTypeAllowed(type)) { - throw errors.createGenericNotFoundError(type, id); - } - - const { version, namespace, references = [] } = options; - - const time = this._getCurrentTime(); - const response = await this._writeToCluster('update', { - id: this._serializer.generateRawId(namespace, type, id), - index: this._index, - ...(version && decodeRequestVersion(version)), - refresh: 'wait_for', - ignore: [404], - body: { - doc: { - [type]: attributes, - updated_at: time, - references, - }, - }, - }); - - if (response.status === 404) { - // see "404s from missing index" above - throw errors.createGenericNotFoundError(type, id); - } - - return { - id, - type, - updated_at: time, - version: encodeHitVersion(response), - references, - attributes, - }; - } - - /** - * Increases a counter field by one. Creates the document if one doesn't exist for the given id. - * - * @param {string} type - * @param {string} id - * @param {string} counterFieldName - * @param {object} [options={}] - * @property {object} [options.migrationVersion=undefined] - * @returns {promise} - */ - async incrementCounter(type, id, counterFieldName, options = {}) { - if (typeof type !== 'string') { - throw new Error('"type" argument must be a string'); - } - if (typeof counterFieldName !== 'string') { - throw new Error('"counterFieldName" argument must be a string'); - } - if (!this._isTypeAllowed(type)) { - throw errors.createUnsupportedTypeError(type); - } - - const { migrationVersion, namespace } = options; - - const time = this._getCurrentTime(); - - const migrated = this._migrator.migrateDocument({ - id, - type, - attributes: { [counterFieldName]: 1 }, - migrationVersion, - updated_at: time, - }); - - const raw = this._serializer.savedObjectToRaw(migrated); - - const response = await this._writeToCluster('update', { - id: this._serializer.generateRawId(namespace, type, id), - index: this._index, - refresh: 'wait_for', - _source: true, - body: { - script: { - source: ` - if (ctx._source[params.type][params.counterFieldName] == null) { - ctx._source[params.type][params.counterFieldName] = params.count; - } - else { - ctx._source[params.type][params.counterFieldName] += params.count; - } - ctx._source.updated_at = params.time; - `, - lang: 'painless', - params: { - count: 1, - time, - type, - counterFieldName, - }, - }, - upsert: raw._source, - }, - }); - - return { - id, - type, - updated_at: time, - references: response.get._source.references, - version: encodeHitVersion(response), - attributes: response.get._source[type], - }; - } - - async _writeToCluster(method, params) { - try { - await this._onBeforeWrite(); - return await this._callCluster(method, params); - } catch (err) { - throw decorateEsError(err); - } - } - - async _callCluster(method, params) { - try { - return await this._unwrappedCallCluster(method, params); - } catch (err) { - throw decorateEsError(err); - } - } - - _getCurrentTime() { - return new Date().toISOString(); - } - - // The internal representation of the saved object that the serializer returns - // includes the namespace, and we use this for migrating documents. However, we don't - // want the namespcae to be returned from the repository, as the repository scopes each - // method transparently to the specified namespace. - _rawToSavedObject(raw) { - const savedObject = this._serializer.rawToSavedObject(raw); - return omit(savedObject, 'namespace'); - } - - _isTypeAllowed(types) { - const toCheck = [].concat(types); - for (const type of toCheck) { - if (!this._allowedTypes.includes(type)) { - return false; - } - } - return true; - } -} diff --git a/src/legacy/server/saved_objects/service/lib/repository.test.js b/src/legacy/server/saved_objects/service/lib/repository.test.js index d336e334557c4..29ccdb3b8002a 100644 --- a/src/legacy/server/saved_objects/service/lib/repository.test.js +++ b/src/legacy/server/saved_objects/service/lib/repository.test.js @@ -214,6 +214,11 @@ describe('SavedObjectsRepository', () => { type: 'keyword', }, }, + baz: { + properties: { + type: 'keyword', + }, + }, 'index-pattern': { properties: { someField: { @@ -249,6 +254,7 @@ describe('SavedObjectsRepository', () => { globaltype: { isNamespaceAgnostic: true }, foo: { isNamespaceAgnostic: true }, bar: { isNamespaceAgnostic: true }, + baz: { indexPattern: 'beats' }, hiddenType: { isNamespaceAgnostic: true, hidden: true }, }); @@ -358,6 +364,36 @@ describe('SavedObjectsRepository', () => { expect(onBeforeWrite).toHaveBeenCalledTimes(1); }); + it('should use default index', async () => { + await savedObjectsRepository.create('index-pattern', { + id: 'logstash-*', + title: 'Logstash', + }); + + expect(onBeforeWrite).toHaveBeenCalledTimes(1); + expect(onBeforeWrite).toHaveBeenCalledWith( + 'index', + expect.objectContaining({ + index: '.kibana-test', + }) + ); + }); + + it('should use custom index', async () => { + await savedObjectsRepository.create('baz', { + id: 'logstash-*', + title: 'Logstash', + }); + + expect(onBeforeWrite).toHaveBeenCalledTimes(1); + expect(onBeforeWrite).toHaveBeenCalledWith( + 'index', + expect.objectContaining({ + index: 'beats', + }) + ); + }); + it('migrates the doc', async () => { migrator.migrateDocument = doc => { doc.attributes.title = doc.attributes.title + '!!'; @@ -572,14 +608,14 @@ describe('SavedObjectsRepository', () => { expect(bulkCalls.length).toEqual(1); expect(bulkCalls[0][1].body).toEqual([ - { create: { _id: 'config:one' } }, + { create: { _index: '.kibana-test', _id: 'config:one' } }, { type: 'config', ...mockTimestampFields, config: { title: 'Test One' }, references: [{ name: 'ref_0', type: 'test', id: '1' }], }, - { create: { _id: 'index-pattern:two' } }, + { create: { _index: '.kibana-test', _id: 'index-pattern:two' } }, { type: 'index-pattern', ...mockTimestampFields, @@ -629,7 +665,7 @@ describe('SavedObjectsRepository', () => { 'bulk', expect.objectContaining({ body: [ - { create: { _id: 'config:one' } }, + { create: { _index: '.kibana-test', _id: 'config:one' } }, { type: 'config', ...mockTimestampFields, @@ -637,7 +673,7 @@ describe('SavedObjectsRepository', () => { migrationVersion: { foo: '2.3.4' }, references: [{ name: 'search_0', type: 'search', id: '123' }], }, - { create: { _id: 'index-pattern:two' } }, + { create: { _index: '.kibana-test', _id: 'index-pattern:two' } }, { type: 'index-pattern', ...mockTimestampFields, @@ -689,7 +725,7 @@ describe('SavedObjectsRepository', () => { expect.objectContaining({ body: [ // uses create because overwriting is not allowed - { create: { _id: 'foo:bar' } }, + { create: { _index: '.kibana-test', _id: 'foo:bar' } }, { type: 'foo', ...mockTimestampFields, foo: {}, references: [] }, ], }) @@ -713,7 +749,7 @@ describe('SavedObjectsRepository', () => { expect.objectContaining({ body: [ // uses index because overwriting is allowed - { index: { _id: 'foo:bar' } }, + { index: { _index: '.kibana-test', _id: 'foo:bar' } }, { type: 'foo', ...mockTimestampFields, foo: {}, references: [] }, ], }) @@ -829,6 +865,7 @@ describe('SavedObjectsRepository', () => { create: { _type: '_doc', _id: 'foo-namespace:config:one', + _index: '.kibana-test', _primary_term: 1, _seq_no: 2, }, @@ -857,7 +894,7 @@ describe('SavedObjectsRepository', () => { 'bulk', expect.objectContaining({ body: [ - { create: { _id: 'foo-namespace:config:one' } }, + { create: { _index: '.kibana-test', _id: 'foo-namespace:config:one' } }, { namespace: 'foo-namespace', type: 'config', @@ -865,7 +902,7 @@ describe('SavedObjectsRepository', () => { config: { title: 'Test One' }, references: [], }, - { create: { _id: 'foo-namespace:index-pattern:two' } }, + { create: { _index: '.kibana-test', _id: 'foo-namespace:index-pattern:two' } }, { namespace: 'foo-namespace', type: 'index-pattern', @@ -908,14 +945,14 @@ describe('SavedObjectsRepository', () => { 'bulk', expect.objectContaining({ body: [ - { create: { _id: 'config:one' } }, + { create: { _id: 'config:one', _index: '.kibana-test' } }, { type: 'config', ...mockTimestampFields, config: { title: 'Test One' }, references: [], }, - { create: { _id: 'index-pattern:two' } }, + { create: { _id: 'index-pattern:two', _index: '.kibana-test' } }, { type: 'index-pattern', ...mockTimestampFields, @@ -943,7 +980,7 @@ describe('SavedObjectsRepository', () => { 'bulk', expect.objectContaining({ body: [ - { create: { _id: 'globaltype:one' } }, + { create: { _id: 'globaltype:one', _index: '.kibana-test' } }, { type: 'globaltype', ...mockTimestampFields, @@ -1063,13 +1100,13 @@ describe('SavedObjectsRepository', () => { expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, schema, { namespace: 'my-namespace', - type: ['config', 'index-pattern', 'dashboard'], + type: ['config', 'baz', 'index-pattern', 'dashboard'], }); expect(callAdminCluster).toHaveBeenCalledWith('deleteByQuery', { body: { conflicts: 'proceed' }, ignore: [404], - index: '.kibana-test', + index: ['.kibana-test', 'beats'], refresh: 'wait_for', }); }); @@ -1123,7 +1160,7 @@ describe('SavedObjectsRepository', () => { namespace: 'foo-namespace', search: 'foo*', searchFields: ['foo'], - type: 'bar', + type: ['bar'], sortField: 'name', sortOrder: 'desc', defaultSearchOperator: 'AND', @@ -1401,9 +1438,9 @@ describe('SavedObjectsRepository', () => { expect.objectContaining({ body: { docs: [ - { _id: 'config:one' }, - { _id: 'index-pattern:two' }, - { _id: 'globaltype:three' }, + { _id: 'config:one', _index: '.kibana-test' }, + { _id: 'index-pattern:two', _index: '.kibana-test' }, + { _id: 'globaltype:three', _index: '.kibana-test' }, ], }, }) @@ -1432,9 +1469,9 @@ describe('SavedObjectsRepository', () => { expect.objectContaining({ body: { docs: [ - { _id: 'foo-namespace:config:one' }, - { _id: 'foo-namespace:index-pattern:two' }, - { _id: 'globaltype:three' }, + { _id: 'foo-namespace:config:one', _index: '.kibana-test' }, + { _id: 'foo-namespace:index-pattern:two', _index: '.kibana-test' }, + { _id: 'globaltype:three', _index: '.kibana-test' }, ], }, }) @@ -1523,6 +1560,90 @@ describe('SavedObjectsRepository', () => { error: { statusCode: 404, message: 'Not found' }, }); }); + + it('returns errors when requesting unsupported types', async () => { + callAdminCluster.mockResolvedValue({ + docs: [ + { + _type: '_doc', + _id: 'one', + found: true, + ...mockVersionProps, + _source: { ...mockTimestampFields, config: { title: 'Test1' } }, + }, + { + _type: '_doc', + _id: 'three', + found: true, + ...mockVersionProps, + _source: { ...mockTimestampFields, config: { title: 'Test3' } }, + }, + { + _type: '_doc', + _id: 'five', + found: true, + ...mockVersionProps, + _source: { ...mockTimestampFields, config: { title: 'Test5' } }, + }, + ], + }); + + const { saved_objects: savedObjects } = await savedObjectsRepository.bulkGet([ + { id: 'one', type: 'config' }, + { id: 'two', type: 'invalidtype' }, + { id: 'three', type: 'config' }, + { id: 'four', type: 'invalidtype' }, + { id: 'five', type: 'config' }, + ]); + + expect(savedObjects).toEqual([ + { + attributes: { title: 'Test1' }, + id: 'one', + ...mockTimestampFields, + references: [], + type: 'config', + version: mockVersion, + migrationVersion: undefined, + }, + { + attributes: { title: 'Test3' }, + id: 'three', + ...mockTimestampFields, + references: [], + type: 'config', + version: mockVersion, + migrationVersion: undefined, + }, + { + attributes: { title: 'Test5' }, + id: 'five', + ...mockTimestampFields, + references: [], + type: 'config', + version: mockVersion, + migrationVersion: undefined, + }, + { + error: { + error: 'Bad Request', + message: "Unsupported saved object type: 'invalidtype': Bad Request", + statusCode: 400, + }, + id: 'two', + type: 'invalidtype', + }, + { + error: { + error: 'Bad Request', + message: "Unsupported saved object type: 'invalidtype': Bad Request", + statusCode: 400, + }, + id: 'four', + type: 'invalidtype', + }, + ]); + }); }); describe('#update', () => { @@ -1966,6 +2087,14 @@ describe('SavedObjectsRepository', () => { }); }); + describe('types on custom index', () => { + it("should error when attempting to 'update' an unsupported type", async () => { + await expect( + savedObjectsRepository.update('hiddenType', 'bogus', { title: 'some title' }) + ).rejects.toEqual(new Error('Saved object [hiddenType/bogus] not found')); + }); + }); + describe('unsupported types', () => { it("should error when attempting to 'update' an unsupported type", async () => { await expect( @@ -1985,59 +2114,6 @@ describe('SavedObjectsRepository', () => { ).rejects.toEqual(new Error("Unsupported saved object type: 'hiddenType': Bad Request")); }); - it("should return an error object when attempting to 'bulkGet' an unsupported type", async () => { - callAdminCluster.mockReturnValue({ - docs: [ - { - id: 'one', - type: 'config', - _primary_term: 1, - _seq_no: 1, - found: true, - _source: { - updated_at: mockTimestamp, - }, - }, - { - id: 'bad', - type: 'config', - found: false, - }, - ], - }); - const { saved_objects: savedObjects } = await savedObjectsRepository.bulkGet([ - { id: 'one', type: 'config' }, - { id: 'bad', type: 'config' }, - { id: 'four', type: 'hiddenType' }, - ]); - expect(savedObjects).toEqual([ - { - id: 'one', - type: 'config', - updated_at: mockTimestamp, - references: [], - version: 'WzEsMV0=', - }, - { - error: { - message: 'Not found', - statusCode: 404, - }, - id: 'bad', - type: 'config', - }, - { - id: 'four', - error: { - error: 'Bad Request', - message: "Unsupported saved object type: 'hiddenType': Bad Request", - statusCode: 400, - }, - type: 'hiddenType', - }, - ]); - }); - it("should not return hidden saved ojects when attempting to 'find' support and unsupported types", async () => { callAdminCluster.mockReturnValue({ hits: { diff --git a/src/legacy/server/saved_objects/service/lib/repository.ts b/src/legacy/server/saved_objects/service/lib/repository.ts new file mode 100644 index 0000000000000..ef4b17f5106c9 --- /dev/null +++ b/src/legacy/server/saved_objects/service/lib/repository.ts @@ -0,0 +1,771 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { omit } from 'lodash'; +import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; +import { getRootPropertiesObjects, IndexMapping } from '../../../mappings'; +import { getSearchDsl } from './search_dsl'; +import { includedFields } from './included_fields'; +import { decorateEsError } from './decorate_es_error'; +import * as errors from './errors'; +import { decodeRequestVersion, encodeVersion, encodeHitVersion } from '../../version'; +import { SavedObjectsSchema } from '../../schema'; +import { KibanaMigrator } from '../../migrations'; +import { SavedObjectsSerializer, SanitizedSavedObjectDoc, RawDoc } from '../../serialization'; +import { + BulkCreateObject, + CreateOptions, + SavedObject, + FindOptions, + SavedObjectAttributes, + FindResponse, + BulkGetObject, + BulkResponse, + UpdateOptions, + BaseOptions, + MigrationVersion, + UpdateResponse, +} from '../saved_objects_client'; + +// BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository +// so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient. + +// eslint-disable-next-line @typescript-eslint/prefer-interface +type Left = { + tag: 'Left'; + error: T; +}; +// eslint-disable-next-line @typescript-eslint/prefer-interface +type Right = { + tag: 'Right'; + value: T; +}; + +type Either = Left | Right; +const isLeft = (either: Either): either is Left => { + return either.tag === 'Left'; +}; + +export interface SavedObjectsRepositoryOptions { + index: string; + mappings: IndexMapping; + callCluster: CallCluster; + schema: SavedObjectsSchema; + serializer: SavedObjectsSerializer; + migrator: KibanaMigrator; + allowedTypes: string[]; + onBeforeWrite?: (...args: Parameters) => Promise; +} + +export interface IncrementCounterOptions extends BaseOptions { + migrationVersion?: MigrationVersion; +} + +export class SavedObjectsRepository { + private _migrator: KibanaMigrator; + private _index: string; + private _mappings: IndexMapping; + private _schema: SavedObjectsSchema; + private _allowedTypes: string[]; + private _onBeforeWrite: (...args: Parameters) => Promise; + private _unwrappedCallCluster: CallCluster; + private _serializer: SavedObjectsSerializer; + + constructor(options: SavedObjectsRepositoryOptions) { + const { + index, + mappings, + callCluster, + schema, + serializer, + migrator, + allowedTypes = [], + onBeforeWrite = () => Promise.resolve(), + } = options; + + // It's important that we migrate documents / mark them as up-to-date + // prior to writing them to the index. Otherwise, we'll cause unecessary + // index migrations to run at Kibana startup, and those will probably fail + // due to invalidly versioned documents in the index. + // + // The migrator performs double-duty, and validates the documents prior + // to returning them. + this._migrator = migrator; + this._index = index; + this._mappings = mappings; + this._schema = schema; + if (allowedTypes.length === 0) { + throw new Error('Empty or missing types for saved object repository!'); + } + this._allowedTypes = allowedTypes; + + this._onBeforeWrite = onBeforeWrite; + + this._unwrappedCallCluster = async (...args: Parameters) => { + await migrator.awaitMigration(); + return callCluster(...args); + }; + this._schema = schema; + this._serializer = serializer; + } + + /** + * Persists an object + * + * @param {string} type + * @param {object} attributes + * @param {object} [options={}] + * @property {string} [options.id] - force id on creation, not recommended + * @property {boolean} [options.overwrite=false] + * @property {object} [options.migrationVersion=undefined] + * @property {string} [options.namespace] + * @property {array} [options.references] - [{ name, type, id }] + * @returns {promise} - { id, type, version, attributes } + */ + public async create( + type: string, + attributes: T, + options: CreateOptions = { overwrite: false, references: [] } + ): Promise> { + const { id, migrationVersion, overwrite, namespace, references } = options; + + if (!this._allowedTypes.includes(type)) { + throw errors.createUnsupportedTypeError(type); + } + + const method = id && !overwrite ? 'create' : 'index'; + const time = this._getCurrentTime(); + + try { + const migrated = this._migrator.migrateDocument({ + id, + type, + namespace, + attributes, + migrationVersion, + updated_at: time, + references, + }); + + const raw = this._serializer.savedObjectToRaw(migrated as SanitizedSavedObjectDoc); + + const response = await this._writeToCluster(method, { + id: raw._id, + index: this.getIndexForType(type), + refresh: 'wait_for', + body: raw._source, + }); + + return this._rawToSavedObject({ + ...raw, + ...response, + }); + } catch (error) { + if (errors.isNotFoundError(error)) { + // See "503s from missing index" above + throw errors.createEsAutoCreateIndexError(); + } + + throw error; + } + } + + /** + * Creates multiple documents at once + * + * @param {array} objects - [{ type, id, attributes, references, migrationVersion }] + * @param {object} [options={}] + * @property {boolean} [options.overwrite=false] - overwrites existing documents + * @property {string} [options.namespace] + * @returns {promise} - {saved_objects: [[{ id, type, version, references, attributes, error: { message } }]} + */ + async bulkCreate( + objects: Array>, + options: CreateOptions = {} + ): Promise> { + const { namespace, overwrite = false } = options; + const time = this._getCurrentTime(); + const bulkCreateParams: object[] = []; + + let requestIndexCounter = 0; + const expectedResults: Array> = objects.map(object => { + if (!this._allowedTypes.includes(object.type)) { + return { + tag: 'Left' as 'Left', + error: { + id: object.id, + type: object.type, + error: errors.createUnsupportedTypeError(object.type).output.payload, + }, + }; + } + + const method = object.id && !overwrite ? 'create' : 'index'; + const expectedResult = { + esRequestIndex: requestIndexCounter++, + requestedId: object.id, + rawMigratedDoc: this._serializer.savedObjectToRaw(this._migrator.migrateDocument({ + id: object.id, + type: object.type, + attributes: object.attributes, + migrationVersion: object.migrationVersion, + namespace, + updated_at: time, + references: object.references || [], + }) as SanitizedSavedObjectDoc), + }; + + bulkCreateParams.push( + { + [method]: { + _id: expectedResult.rawMigratedDoc._id, + _index: this.getIndexForType(object.type), + }, + }, + expectedResult.rawMigratedDoc._source + ); + + return { tag: 'Right' as 'Right', value: expectedResult }; + }); + + const esResponse = await this._writeToCluster('bulk', { + refresh: 'wait_for', + body: bulkCreateParams, + }); + + return { + saved_objects: expectedResults.map(expectedResult => { + if (isLeft(expectedResult)) { + return expectedResult.error; + } + + const { requestedId, rawMigratedDoc, esRequestIndex } = expectedResult.value; + const response = esResponse.items[esRequestIndex]; + const { + error, + _id: responseId, + _seq_no: seqNo, + _primary_term: primaryTerm, + } = Object.values(response)[0] as any; + + const { + _source: { type, [type]: attributes, references = [] }, + } = rawMigratedDoc; + + const id = requestedId || responseId; + if (error) { + if (error.type === 'version_conflict_engine_exception') { + return { + id, + type, + error: { statusCode: 409, message: 'version conflict, document already exists' }, + }; + } + return { + id, + type, + error: { + message: error.reason || JSON.stringify(error), + }, + }; + } + + return { + id, + type, + updated_at: time, + version: encodeVersion(seqNo, primaryTerm), + attributes, + references, + }; + }), + }; + } + + /** + * Deletes an object + * + * @param {string} type + * @param {string} id + * @param {object} [options={}] + * @property {string} [options.namespace] + * @returns {promise} + */ + async delete(type: string, id: string, options: BaseOptions = {}): Promise<{}> { + if (!this._allowedTypes.includes(type)) { + throw errors.createGenericNotFoundError(); + } + + const { namespace } = options; + + const response = await this._writeToCluster('delete', { + id: this._serializer.generateRawId(namespace, type, id), + index: this.getIndexForType(type), + refresh: 'wait_for', + ignore: [404], + }); + + const deleted = response.result === 'deleted'; + if (deleted) { + return {}; + } + + const docNotFound = response.result === 'not_found'; + const indexNotFound = response.error && response.error.type === 'index_not_found_exception'; + if (docNotFound || indexNotFound) { + // see "404s from missing index" above + throw errors.createGenericNotFoundError(type, id); + } + + throw new Error( + `Unexpected Elasticsearch DELETE response: ${JSON.stringify({ type, id, response })}` + ); + } + + /** + * Deletes all objects from the provided namespace. + * + * @param {string} namespace + * @returns {promise} - { took, timed_out, total, deleted, batches, version_conflicts, noops, retries, failures } + */ + async deleteByNamespace(namespace: string): Promise { + if (!namespace || typeof namespace !== 'string') { + throw new TypeError(`namespace is required, and must be a string`); + } + + const allTypes = Object.keys(getRootPropertiesObjects(this._mappings)); + + const typesToDelete = allTypes.filter(type => !this._schema.isNamespaceAgnostic(type)); + + const esOptions = { + index: this.getIndicesForTypes(typesToDelete), + ignore: [404], + refresh: 'wait_for', + body: { + conflicts: 'proceed', + ...getSearchDsl(this._mappings, this._schema, { + namespace, + type: typesToDelete, + }), + }, + }; + + return await this._writeToCluster('deleteByQuery', esOptions); + } + + /** + * @param {object} [options={}] + * @property {(string|Array)} [options.type] + * @property {string} [options.search] + * @property {string} [options.defaultSearchOperator] + * @property {Array} [options.searchFields] - see Elasticsearch Simple Query String + * Query field argument for more information + * @property {integer} [options.page=1] + * @property {integer} [options.perPage=20] + * @property {string} [options.sortField] + * @property {string} [options.sortOrder] + * @property {Array} [options.fields] + * @property {string} [options.namespace] + * @property {object} [options.hasReference] - { type, id } + * @returns {promise} - { saved_objects: [{ id, type, version, attributes }], total, per_page, page } + */ + async find({ + search, + defaultSearchOperator = 'OR', + searchFields, + hasReference, + page = 1, + perPage = 20, + sortField, + sortOrder, + fields, + namespace, + type, + }: FindOptions): Promise> { + if (!type) { + throw new TypeError(`options.type must be a string or an array of strings`); + } + + const types = Array.isArray(type) ? type : [type]; + const allowedTypes = types.filter(t => this._allowedTypes.includes(t)); + if (allowedTypes.length === 0) { + return { + page, + per_page: perPage, + total: 0, + saved_objects: [], + }; + } + + if (searchFields && !Array.isArray(searchFields)) { + throw new TypeError('options.searchFields must be an array'); + } + + if (fields && !Array.isArray(fields)) { + throw new TypeError('options.fields must be an array'); + } + + const esOptions = { + index: this.getIndicesForTypes(allowedTypes), + size: perPage, + from: perPage * (page - 1), + _source: includedFields(type, fields), + ignore: [404], + rest_total_hits_as_int: true, + body: { + seq_no_primary_term: true, + ...getSearchDsl(this._mappings, this._schema, { + search, + defaultSearchOperator, + searchFields, + type: allowedTypes, + sortField, + sortOrder, + namespace, + hasReference, + }), + }, + }; + + const response = await this._callCluster('search', esOptions); + + if (response.status === 404) { + // 404 is only possible here if the index is missing, which + // we don't want to leak, see "404s from missing index" above + return { + page, + per_page: perPage, + total: 0, + saved_objects: [], + }; + } + + return { + page, + per_page: perPage, + total: response.hits.total, + saved_objects: response.hits.hits.map((hit: RawDoc) => this._rawToSavedObject(hit)), + }; + } + + /** + * Returns an array of objects by id + * + * @param {array} objects - an array of objects containing id, type and optionally fields + * @param {object} [options={}] + * @property {string} [options.namespace] + * @returns {promise} - { saved_objects: [{ id, type, version, attributes }] } + * @example + * + * bulkGet([ + * { id: 'one', type: 'config' }, + * { id: 'foo', type: 'index-pattern' } + * ]) + */ + async bulkGet( + objects: BulkGetObject[] = [], + options: BaseOptions = {} + ): Promise> { + const { namespace } = options; + + if (objects.length === 0) { + return { saved_objects: [] }; + } + + const unsupportedTypeObjects = objects + .filter(o => !this._allowedTypes.includes(o.type)) + .map(({ type, id }) => { + return ({ + id, + type, + error: errors.createUnsupportedTypeError(type).output.payload, + } as any) as SavedObject; + }); + + const supportedTypeObjects = objects.filter(o => this._allowedTypes.includes(o.type)); + + const response = await this._callCluster('mget', { + body: { + docs: supportedTypeObjects.map(({ type, id, fields }) => { + return { + _id: this._serializer.generateRawId(namespace, type, id), + _index: this.getIndexForType(type), + _source: includedFields(type, fields), + }; + }), + }, + }); + + return { + saved_objects: (response.docs as any[]) + .map((doc, i) => { + const { id, type } = supportedTypeObjects[i]; + + if (!doc.found) { + return ({ + id, + type, + error: { statusCode: 404, message: 'Not found' }, + } as any) as SavedObject; + } + + const time = doc._source.updated_at; + return { + id, + type, + ...(time && { updated_at: time }), + version: encodeHitVersion(doc), + attributes: doc._source[type], + references: doc._source.references || [], + migrationVersion: doc._source.migrationVersion, + }; + }) + .concat(unsupportedTypeObjects), + }; + } + + /** + * Gets a single object + * + * @param {string} type + * @param {string} id + * @param {object} [options={}] + * @property {string} [options.namespace] + * @returns {promise} - { id, type, version, attributes } + */ + async get( + type: string, + id: string, + options: BaseOptions = {} + ): Promise> { + if (!this._allowedTypes.includes(type)) { + throw errors.createGenericNotFoundError(type, id); + } + + const { namespace } = options; + + const response = await this._callCluster('get', { + id: this._serializer.generateRawId(namespace, type, id), + index: this.getIndexForType(type), + ignore: [404], + }); + + const docNotFound = response.found === false; + const indexNotFound = response.status === 404; + if (docNotFound || indexNotFound) { + // see "404s from missing index" above + throw errors.createGenericNotFoundError(type, id); + } + + const { updated_at: updatedAt } = response._source; + + return { + id, + type, + ...(updatedAt && { updated_at: updatedAt }), + version: encodeHitVersion(response), + attributes: response._source[type], + references: response._source.references || [], + migrationVersion: response._source.migrationVersion, + }; + } + + /** + * Updates an object + * + * @param {string} type + * @param {string} id + * @param {object} [options={}] + * @property {string} options.version - ensures version matches that of persisted object + * @property {string} [options.namespace] + * @property {array} [options.references] - [{ name, type, id }] + * @returns {promise} + */ + async update( + type: string, + id: string, + attributes: Partial, + options: UpdateOptions = {} + ): Promise> { + if (!this._allowedTypes.includes(type)) { + throw errors.createGenericNotFoundError(type, id); + } + + const { version, namespace, references = [] } = options; + + const time = this._getCurrentTime(); + const response = await this._writeToCluster('update', { + id: this._serializer.generateRawId(namespace, type, id), + index: this.getIndexForType(type), + ...(version && decodeRequestVersion(version)), + refresh: 'wait_for', + ignore: [404], + body: { + doc: { + [type]: attributes, + updated_at: time, + references, + }, + }, + }); + + if (response.status === 404) { + // see "404s from missing index" above + throw errors.createGenericNotFoundError(type, id); + } + + return { + id, + type, + updated_at: time, + version: encodeHitVersion(response), + references, + attributes, + }; + } + + /** + * Increases a counter field by one. Creates the document if one doesn't exist for the given id. + * + * @param {string} type + * @param {string} id + * @param {string} counterFieldName + * @param {object} [options={}] + * @property {object} [options.migrationVersion=undefined] + * @returns {promise} + */ + async incrementCounter( + type: string, + id: string, + counterFieldName: string, + options: IncrementCounterOptions = {} + ) { + if (typeof type !== 'string') { + throw new Error('"type" argument must be a string'); + } + if (typeof counterFieldName !== 'string') { + throw new Error('"counterFieldName" argument must be a string'); + } + if (!this._allowedTypes.includes(type)) { + throw errors.createUnsupportedTypeError(type); + } + + const { migrationVersion, namespace } = options; + + const time = this._getCurrentTime(); + + const migrated = this._migrator.migrateDocument({ + id, + type, + attributes: { [counterFieldName]: 1 }, + migrationVersion, + updated_at: time, + }); + + const raw = this._serializer.savedObjectToRaw(migrated as SanitizedSavedObjectDoc); + + const response = await this._writeToCluster('update', { + id: this._serializer.generateRawId(namespace, type, id), + index: this.getIndexForType(type), + refresh: 'wait_for', + _source: true, + body: { + script: { + source: ` + if (ctx._source[params.type][params.counterFieldName] == null) { + ctx._source[params.type][params.counterFieldName] = params.count; + } + else { + ctx._source[params.type][params.counterFieldName] += params.count; + } + ctx._source.updated_at = params.time; + `, + lang: 'painless', + params: { + count: 1, + time, + type, + counterFieldName, + }, + }, + upsert: raw._source, + }, + }); + + return { + id, + type, + updated_at: time, + references: response.get._source.references, + version: encodeHitVersion(response), + attributes: response.get._source[type], + }; + } + + private async _writeToCluster(...args: Parameters) { + try { + await this._onBeforeWrite(...args); + return await this._callCluster(...args); + } catch (err) { + throw decorateEsError(err); + } + } + + private async _callCluster(...args: Parameters) { + try { + return await this._unwrappedCallCluster(...args); + } catch (err) { + throw decorateEsError(err); + } + } + + /** + * Returns index specified by the given type or the default index + * + * @param type - the type + */ + private getIndexForType(type: string) { + return this._schema.getIndexForType(type) || this._index; + } + + /** + * Returns an array of indices as specified in `this._schema` for each of the + * given `types`. If any of the types don't have an associated index, the + * default index `this._index` will be included. + * + * @param types The types whose indices should be retrieved + */ + private getIndicesForTypes(types: string[]) { + const unique = (array: string[]) => [...new Set(array)]; + return unique(types.map(t => this._schema.getIndexForType(t) || this._index)); + } + + private _getCurrentTime() { + return new Date().toISOString(); + } + + // The internal representation of the saved object that the serializer returns + // includes the namespace, and we use this for migrating documents. However, we don't + // want the namespcae to be returned from the repository, as the repository scopes each + // method transparently to the specified namespace. + private _rawToSavedObject(raw: RawDoc): SavedObject { + const savedObject = this._serializer.rawToSavedObject(raw); + return omit(savedObject, 'namespace'); + } +} diff --git a/src/legacy/server/saved_objects/service/lib/scoped_client_provider.d.ts b/src/legacy/server/saved_objects/service/lib/scoped_client_provider.d.ts deleted file mode 100644 index 10f3cce763f9a..0000000000000 --- a/src/legacy/server/saved_objects/service/lib/scoped_client_provider.d.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { SavedObjectsClient } from '..'; - -export interface SavedObjectsClientWrapperOptions { - client: SavedObjectsClient; - request: Request; -} - -export type SavedObjectsClientWrapperFactory = ( - options: SavedObjectsClientWrapperOptions -) => SavedObjectsClient; - -export interface ScopedSavedObjectsClientProvider { - // ATTENTION: these types are incomplete - - addClientWrapperFactory( - priority: number, - wrapperFactory: SavedObjectsClientWrapperFactory - ): void; - getClient(request: Request): SavedObjectsClient; -} diff --git a/src/legacy/server/saved_objects/service/lib/scoped_client_provider.js b/src/legacy/server/saved_objects/service/lib/scoped_client_provider.js deleted file mode 100644 index 4049dcbf49f74..0000000000000 --- a/src/legacy/server/saved_objects/service/lib/scoped_client_provider.js +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { PriorityCollection } from './priority_collection'; - -/** - * Provider for the Scoped Saved Object Client. - */ -export class ScopedSavedObjectsClientProvider { - _wrapperFactories = new PriorityCollection(); - - constructor({ defaultClientFactory }) { - this._originalClientFactory = this._clientFactory = defaultClientFactory; - } - - addClientWrapperFactory(priority, wrapperFactory) { - this._wrapperFactories.add(priority, wrapperFactory); - } - - setClientFactory(customClientFactory) { - if (this._clientFactory !== this._originalClientFactory) { - throw new Error(`custom client factory is already set, unable to replace the current one`); - } - - this._clientFactory = customClientFactory; - } - - getClient(request) { - const client = this._clientFactory({ - request, - }); - - return this._wrapperFactories - .toPrioritizedArray() - .reduceRight((clientToWrap, wrapperFactory) => { - return wrapperFactory({ - request, - client: clientToWrap, - }); - }, client); - } -} diff --git a/src/legacy/server/saved_objects/service/lib/scoped_client_provider.ts b/src/legacy/server/saved_objects/service/lib/scoped_client_provider.ts new file mode 100644 index 0000000000000..201b316005d7c --- /dev/null +++ b/src/legacy/server/saved_objects/service/lib/scoped_client_provider.ts @@ -0,0 +1,82 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { PriorityCollection } from './priority_collection'; +import { SavedObjectsClientContract } from '..'; + +export interface SavedObjectsClientWrapperOptions { + client: SavedObjectsClientContract; + request: Request; +} + +export type SavedObjectsClientWrapperFactory = ( + options: SavedObjectsClientWrapperOptions +) => SavedObjectsClientContract; + +export type SavedObjectsClientFactory = ( + { request }: { request: Request } +) => SavedObjectsClientContract; + +/** + * Provider for the Scoped Saved Object Client. + */ +export class ScopedSavedObjectsClientProvider { + private readonly _wrapperFactories = new PriorityCollection< + SavedObjectsClientWrapperFactory + >(); + private _clientFactory: SavedObjectsClientFactory; + private readonly _originalClientFactory: SavedObjectsClientFactory; + + constructor({ + defaultClientFactory, + }: { + defaultClientFactory: SavedObjectsClientFactory; + }) { + this._originalClientFactory = this._clientFactory = defaultClientFactory; + } + + addClientWrapperFactory( + priority: number, + wrapperFactory: SavedObjectsClientWrapperFactory + ): void { + this._wrapperFactories.add(priority, wrapperFactory); + } + + setClientFactory(customClientFactory: SavedObjectsClientFactory) { + if (this._clientFactory !== this._originalClientFactory) { + throw new Error(`custom client factory is already set, unable to replace the current one`); + } + + this._clientFactory = customClientFactory; + } + + getClient(request: Request): SavedObjectsClientContract { + const client = this._clientFactory({ + request, + }); + + return this._wrapperFactories + .toPrioritizedArray() + .reduceRight((clientToWrap, wrapperFactory) => { + return wrapperFactory({ + request, + client: clientToWrap, + }); + }, client); + } +} diff --git a/src/legacy/server/saved_objects/service/lib/search_dsl/search_dsl.ts b/src/legacy/server/saved_objects/service/lib/search_dsl/search_dsl.ts index 5baf46e2edab7..83e06eb17ccf2 100644 --- a/src/legacy/server/saved_objects/service/lib/search_dsl/search_dsl.ts +++ b/src/legacy/server/saved_objects/service/lib/search_dsl/search_dsl.ts @@ -25,7 +25,7 @@ import { getQueryParams } from './query_params'; import { getSortingParams } from './sorting_params'; interface GetSearchDslOptions { - type: string; + type: string | string[]; search?: string; defaultSearchOperator?: string; searchFields?: string[]; diff --git a/src/legacy/server/saved_objects/service/saved_objects_client.d.ts b/src/legacy/server/saved_objects/service/saved_objects_client.d.ts deleted file mode 100644 index cb9ace7bcb1ba..0000000000000 --- a/src/legacy/server/saved_objects/service/saved_objects_client.d.ts +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { errors, SavedObjectsRepository } from './lib'; - -export interface BaseOptions { - namespace?: string; -} - -export interface CreateOptions extends BaseOptions { - id?: string; - overwrite?: boolean; - migrationVersion?: MigrationVersion; - references?: SavedObjectReference[]; -} - -export interface BulkCreateObject { - id?: string; - type: string; - attributes: T; - extraDocumentProperties?: string[]; -} - -export interface BulkCreateResponse { - saved_objects: Array>; -} - -export interface FindOptions extends BaseOptions { - type?: string | string[]; - page?: number; - perPage?: number; - sortField?: string; - sortOrder?: string; - fields?: string[]; - search?: string; - searchFields?: string[]; - hasReference?: { type: string; id: string }; - defaultSearchOperator?: 'AND' | 'OR'; -} - -export interface FindResponse { - saved_objects: Array>; - total: number; - per_page: number; - page: number; -} - -export interface UpdateOptions extends BaseOptions { - version?: string; -} - -export interface BulkGetObject { - id: string; - type: string; - fields?: string[]; -} -export type BulkGetObjects = BulkGetObject[]; - -export interface BulkGetResponse { - saved_objects: Array>; -} - -export interface MigrationVersion { - [pluginName: string]: string; -} - -export interface SavedObjectAttributes { - [key: string]: SavedObjectAttributes | string | number | boolean | null; -} - -export interface VisualizationAttributes extends SavedObjectAttributes { - visState: string; -} - -export interface SavedObject { - id: string; - type: string; - version?: string; - updated_at?: string; - error?: { - message: string; - statusCode: number; - }; - attributes: T; - references: SavedObjectReference[]; - migrationVersion?: MigrationVersion; -} - -export interface SavedObjectReference { - name: string; - type: string; - id: string; -} - -export type GetResponse = SavedObject; -export type CreateResponse = SavedObject; -export type UpdateResponse = SavedObject; - -export declare class SavedObjectsClient { - public static errors: typeof errors; - public errors: typeof errors; - - constructor(repository: SavedObjectsRepository); - - public create( - type: string, - attributes: T, - options?: CreateOptions - ): Promise>; - public bulkCreate( - objects: Array>, - options?: CreateOptions - ): Promise>; - public delete(type: string, id: string, options?: BaseOptions): Promise<{}>; - public find( - options: FindOptions - ): Promise>; - public bulkGet( - objects: BulkGetObjects, - options?: BaseOptions - ): Promise>; - public get( - type: string, - id: string, - options?: BaseOptions - ): Promise>; - public update( - type: string, - id: string, - attributes: Partial, - options?: UpdateOptions - ): Promise>; -} diff --git a/src/legacy/server/saved_objects/service/saved_objects_client.js b/src/legacy/server/saved_objects/service/saved_objects_client.js deleted file mode 100644 index f98b99688bf23..0000000000000 --- a/src/legacy/server/saved_objects/service/saved_objects_client.js +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { errors } from './lib'; - -export class SavedObjectsClient { - constructor(repository) { - this._repository = repository; - } - - /** - * ## SavedObjectsClient errors - * - * Since the SavedObjectsClient has its hands in everything we - * are a little paranoid about the way we present errors back to - * to application code. Ideally, all errors will be either: - * - * 1. Caused by bad implementation (ie. undefined is not a function) and - * as such unpredictable - * 2. An error that has been classified and decorated appropriately - * by the decorators in `./lib/errors` - * - * Type 1 errors are inevitable, but since all expected/handle-able errors - * should be Type 2 the `isXYZError()` helpers exposed at - * `savedObjectsClient.errors` should be used to understand and manage error - * responses from the `SavedObjectsClient`. - * - * Type 2 errors are decorated versions of the source error, so if - * the elasticsearch client threw an error it will be decorated based - * on its type. That means that rather than looking for `error.body.error.type` or - * doing substring checks on `error.body.error.reason`, just use the helpers to - * understand the meaning of the error: - * - * ```js - * if (savedObjectsClient.errors.isNotFoundError(error)) { - * // handle 404 - * } - * - * if (savedObjectsClient.errors.isNotAuthorizedError(error)) { - * // 401 handling should be automatic, but in case you wanted to know - * } - * - * // always rethrow the error unless you handle it - * throw error; - * ``` - * - * ### 404s from missing index - * - * From the perspective of application code and APIs the SavedObjectsClient is - * a black box that persists objects. One of the internal details that users have - * no control over is that we use an elasticsearch index for persistance and that - * index might be missing. - * - * At the time of writing we are in the process of transitioning away from the - * operating assumption that the SavedObjects index is always available. Part of - * this transition is handling errors resulting from an index missing. These used - * to trigger a 500 error in most cases, and in others cause 404s with different - * error messages. - * - * From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The - * object the request/call was targeting could not be found. This is why #14141 - * takes special care to ensure that 404 errors are generic and don't distinguish - * between index missing or document missing. - * - * ### 503s from missing index - * - * Unlike all other methods, create requests are supposed to succeed even when - * the Kibana index does not exist because it will be automatically created by - * elasticsearch. When that is not the case it is because Elasticsearch's - * `action.auto_create_index` setting prevents it from being created automatically - * so we throw a special 503 with the intention of informing the user that their - * Elasticsearch settings need to be updated. - * - * @type {ErrorHelpers} see ./lib/errors - */ - static errors = errors; - errors = errors; - - /** - * Persists an object - * - * @param {string} type - * @param {object} attributes - * @param {object} [options={}] - * @property {string} [options.id] - force id on creation, not recommended - * @property {boolean} [options.overwrite=false] - * @property {object} [options.migrationVersion=undefined] - * @property {string} [options.namespace] - * @property {array} [options.references] - [{ name, type, id }] - * @returns {promise} - { id, type, version, attributes } - */ - async create(type, attributes = {}, options = {}) { - return this._repository.create(type, attributes, options); - } - - /** - * Creates multiple documents at once - * - * @param {array} objects - [{ type, id, attributes }] - * @param {object} [options={}] - * @property {boolean} [options.overwrite=false] - overwrites existing documents - * @property {string} [options.namespace] - * @returns {promise} - { saved_objects: [{ id, type, version, attributes, error: { message } }]} - */ - async bulkCreate(objects, options = {}) { - return this._repository.bulkCreate(objects, options); - } - - /** - * Deletes an object - * - * @param {string} type - * @param {string} id - * @param {object} [options={}] - * @property {string} [options.namespace] - * @returns {promise} - */ - async delete(type, id, options = {}) { - return this._repository.delete(type, id, options); - } - - /** - * @param {object} [options={}] - * @property {(string|Array)} [options.type] - * @property {string} [options.search] - * @property {string} [options.defaultSearchOperator] - * @property {Array} [options.searchFields] - see Elasticsearch Simple Query String - * Query field argument for more information - * @property {integer} [options.page=1] - * @property {integer} [options.perPage=20] - * @property {string} [options.sortField] - * @property {string} [options.sortOrder] - * @property {Array} [options.fields] - * @property {string} [options.namespace] - * @property {object} [options.hasReference] - { type, id } - * @returns {promise} - { saved_objects: [{ id, type, version, attributes }], total, per_page, page } - */ - async find(options = {}) { - return this._repository.find(options); - } - - /** - * Returns an array of objects by id - * - * @param {array} objects - an array ids, or an array of objects containing id and optionally type - * @param {object} [options={}] - * @property {string} [options.namespace] - * @returns {promise} - { saved_objects: [{ id, type, version, attributes }] } - * @example - * - * bulkGet([ - * { id: 'one', type: 'config' }, - * { id: 'foo', type: 'index-pattern' } - * ]) - */ - async bulkGet(objects = [], options = {}) { - return this._repository.bulkGet(objects, options); - } - - /** - * Gets a single object - * - * @param {string} type - * @param {string} id - * @param {object} [options={}] - * @property {string} [options.namespace] - * @returns {promise} - { id, type, version, attributes } - */ - async get(type, id, options = {}) { - return this._repository.get(type, id, options); - } - - /** - * Updates an object - * - * @param {string} type - * @param {string} id - * @param {object} [options={}] - * @property {integer} options.version - ensures version matches that of persisted object - * @property {string} [options.namespace] - * @returns {promise} - */ - async update(type, id, attributes, options = {}) { - return this._repository.update(type, id, attributes, options); - } -} diff --git a/src/legacy/server/saved_objects/service/saved_objects_client.mock.ts b/src/legacy/server/saved_objects/service/saved_objects_client.mock.ts new file mode 100644 index 0000000000000..16de6e4eb5b52 --- /dev/null +++ b/src/legacy/server/saved_objects/service/saved_objects_client.mock.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObjectsClientContract } from './saved_objects_client'; +import * as errors from './lib/errors'; + +const create = (): jest.Mocked => ({ + errors, + create: jest.fn(), + bulkCreate: jest.fn(), + delete: jest.fn(), + bulkGet: jest.fn(), + find: jest.fn(), + get: jest.fn(), + update: jest.fn(), +}); + +export const SavedObjectsClientMock = { create }; diff --git a/src/legacy/server/saved_objects/service/saved_objects_client.ts b/src/legacy/server/saved_objects/service/saved_objects_client.ts new file mode 100644 index 0000000000000..a0d378bfc5a97 --- /dev/null +++ b/src/legacy/server/saved_objects/service/saved_objects_client.ts @@ -0,0 +1,307 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { errors, SavedObjectsRepository } from './lib'; + +type Omit = Pick>; + +export interface BaseOptions { + /** Specify the namespace for this operation */ + namespace?: string; +} + +export interface CreateOptions extends BaseOptions { + /** (not recommended) Specify an id for the document */ + id?: string; + /** Overwrite existing documents (defaults to false) */ + overwrite?: boolean; + migrationVersion?: MigrationVersion; + references?: SavedObjectReference[]; +} + +export interface BulkCreateObject { + id?: string; + type: string; + attributes: T; + references?: SavedObjectReference[]; + migrationVersion?: MigrationVersion; +} + +export interface BulkResponse { + saved_objects: Array>; +} + +export interface FindOptions extends BaseOptions { + type?: string | string[]; + page?: number; + perPage?: number; + sortField?: string; + sortOrder?: string; + fields?: string[]; + search?: string; + /** see Elasticsearch Simple Query String Query field argument for more information */ + searchFields?: string[]; + hasReference?: { type: string; id: string }; + defaultSearchOperator?: 'AND' | 'OR'; +} + +export interface FindResponse { + saved_objects: Array>; + total: number; + per_page: number; + page: number; +} + +export interface UpdateOptions extends BaseOptions { + /** Ensures version matches that of persisted object */ + version?: string; + references?: SavedObjectReference[]; +} + +export interface BulkGetObject { + id: string; + type: string; + /** SavedObject fields to include in the response */ + fields?: string[]; +} + +export interface BulkResponse { + saved_objects: Array>; +} + +export interface UpdateResponse + extends Omit, 'attributes'> { + attributes: Partial; +} + +/** + * A dictionary of saved object type -> version used to determine + * what migrations need to be applied to a saved object. + */ +export interface MigrationVersion { + [pluginName: string]: string; +} + +export interface SavedObjectAttributes { + [key: string]: SavedObjectAttributes | string | number | boolean | null; +} + +export interface VisualizationAttributes extends SavedObjectAttributes { + visState: string; +} + +export interface SavedObject { + id: string; + type: string; + version?: string; + updated_at?: string; + error?: { + message: string; + statusCode: number; + }; + attributes: T; + references: SavedObjectReference[]; + migrationVersion?: MigrationVersion; +} + +/** + * A reference to another saved object. + */ +export interface SavedObjectReference { + name: string; + type: string; + id: string; +} + +export type SavedObjectsClientContract = Pick; + +export class SavedObjectsClient { + /** + * ## SavedObjectsClient errors + * + * Since the SavedObjectsClient has its hands in everything we + * are a little paranoid about the way we present errors back to + * to application code. Ideally, all errors will be either: + * + * 1. Caused by bad implementation (ie. undefined is not a function) and + * as such unpredictable + * 2. An error that has been classified and decorated appropriately + * by the decorators in `./lib/errors` + * + * Type 1 errors are inevitable, but since all expected/handle-able errors + * should be Type 2 the `isXYZError()` helpers exposed at + * `savedObjectsClient.errors` should be used to understand and manage error + * responses from the `SavedObjectsClient`. + * + * Type 2 errors are decorated versions of the source error, so if + * the elasticsearch client threw an error it will be decorated based + * on its type. That means that rather than looking for `error.body.error.type` or + * doing substring checks on `error.body.error.reason`, just use the helpers to + * understand the meaning of the error: + * + * ```js + * if (savedObjectsClient.errors.isNotFoundError(error)) { + * // handle 404 + * } + * + * if (savedObjectsClient.errors.isNotAuthorizedError(error)) { + * // 401 handling should be automatic, but in case you wanted to know + * } + * + * // always rethrow the error unless you handle it + * throw error; + * ``` + * + * ### 404s from missing index + * + * From the perspective of application code and APIs the SavedObjectsClient is + * a black box that persists objects. One of the internal details that users have + * no control over is that we use an elasticsearch index for persistance and that + * index might be missing. + * + * At the time of writing we are in the process of transitioning away from the + * operating assumption that the SavedObjects index is always available. Part of + * this transition is handling errors resulting from an index missing. These used + * to trigger a 500 error in most cases, and in others cause 404s with different + * error messages. + * + * From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The + * object the request/call was targeting could not be found. This is why #14141 + * takes special care to ensure that 404 errors are generic and don't distinguish + * between index missing or document missing. + * + * ### 503s from missing index + * + * Unlike all other methods, create requests are supposed to succeed even when + * the Kibana index does not exist because it will be automatically created by + * elasticsearch. When that is not the case it is because Elasticsearch's + * `action.auto_create_index` setting prevents it from being created automatically + * so we throw a special 503 with the intention of informing the user that their + * Elasticsearch settings need to be updated. + * + * @type {ErrorHelpers} see ./lib/errors + */ + public static errors = errors; + public errors = errors; + + private _repository: SavedObjectsRepository; + + constructor(repository: SavedObjectsRepository) { + this._repository = repository; + } + + /** + * Persists a SavedObject + * + * @param type + * @param attributes + * @param options + */ + async create( + type: string, + attributes: T, + options?: CreateOptions + ) { + return await this._repository.create(type, attributes, options); + } + + /** + * Persists multiple documents batched together as a single request + * + * @param objects + * @param options + */ + async bulkCreate( + objects: Array>, + options?: CreateOptions + ) { + return await this._repository.bulkCreate(objects, options); + } + + /** + * Deletes a SavedObject + * + * @param type + * @param id + * @param options + */ + async delete(type: string, id: string, options: BaseOptions = {}) { + return await this._repository.delete(type, id, options); + } + + /** + * Find all SavedObjects matching the search query + * + * @param options + */ + async find( + options: FindOptions + ): Promise> { + return await this._repository.find(options); + } + + /** + * Returns an array of objects by id + * + * @param objects - an array of ids, or an array of objects containing id, type and optionally fields + * @example + * + * bulkGet([ + * { id: 'one', type: 'config' }, + * { id: 'foo', type: 'index-pattern' } + * ]) + */ + async bulkGet( + objects: BulkGetObject[] = [], + options: BaseOptions = {} + ): Promise> { + return await this._repository.bulkGet(objects, options); + } + + /** + * Retrieves a single object + * + * @param type - The type of SavedObject to retrieve + * @param id - The ID of the SavedObject to retrieve + * @param options + */ + async get( + type: string, + id: string, + options: BaseOptions = {} + ): Promise> { + return await this._repository.get(type, id, options); + } + + /** + * Updates an SavedObject + * + * @param type + * @param id + * @param options + */ + async update( + type: string, + id: string, + attributes: Partial, + options: UpdateOptions = {} + ): Promise> { + return await this._repository.update(type, id, attributes, options); + } +} diff --git a/src/legacy/server/status/collectors/get_ops_stats_collector.js b/src/legacy/server/status/collectors/get_ops_stats_collector.js index 0ebb6b3539c0e..aded85384fd85 100644 --- a/src/legacy/server/status/collectors/get_ops_stats_collector.js +++ b/src/legacy/server/status/collectors/get_ops_stats_collector.js @@ -45,6 +45,7 @@ export function getOpsStatsCollector(server, kbnServer) { ...kbnServer.metrics // latest metrics captured from the ops event listener in src/legacy/server/status/index }; }, + isReady: () => true, ignoreForInternalUploader: true, // Ignore this one from internal uploader. A different stats collector is used there. }); } diff --git a/src/legacy/server/status/routes/api/register_stats.js b/src/legacy/server/status/routes/api/register_stats.js index 60545dc3b66bd..91272ead1d2c1 100644 --- a/src/legacy/server/status/routes/api/register_stats.js +++ b/src/legacy/server/status/routes/api/register_stats.js @@ -18,10 +18,15 @@ */ import Joi from 'joi'; -import { boomify } from 'boom'; +import boom from 'boom'; +import { i18n } from '@kbn/i18n'; import { wrapAuthConfig } from '../../wrap_auth_config'; import { KIBANA_STATS_TYPE } from '../../constants'; +const STATS_NOT_READY_MESSAGE = i18n.translate('server.stats.notReadyMessage', { + defaultMessage: 'Stats are not ready yet. Please try again later.', +}); + /* * API for Kibana meta info and accumulated operations stats * Including ?extended in the query string fetches Elasticsearch cluster_uuid and server.usage.collectorSet data @@ -69,15 +74,19 @@ export function registerStatsApi(kbnServer, server, config) { if (isExtended) { const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('admin'); const callCluster = (...args) => callWithRequest(req, ...args); + const collectorsReady = await collectorSet.areAllCollectorsReady(); + + if (shouldGetUsage && !collectorsReady) { + return boom.serverUnavailable(STATS_NOT_READY_MESSAGE); + } - const usagePromise = shouldGetUsage ? getUsage(callCluster) : Promise.resolve(); + const usagePromise = shouldGetUsage ? getUsage(callCluster) : Promise.resolve({}); try { const [ usage, clusterUuid ] = await Promise.all([ usagePromise, getClusterUuid(callCluster), ]); - let modifiedUsage = usage; if (isLegacy) { // In an effort to make telemetry more easily augmented, we need to ensure @@ -123,7 +132,7 @@ export function registerStatsApi(kbnServer, server, config) { }); } } catch (e) { - throw boomify(e); + throw boom.boomify(e); } } @@ -131,6 +140,9 @@ export function registerStatsApi(kbnServer, server, config) { * for health-checking Kibana and fetch does not rely on fetching data * from ES */ const kibanaStatsCollector = collectorSet.getCollectorByType(KIBANA_STATS_TYPE); + if (!await kibanaStatsCollector.isReady()) { + return boom.serverUnavailable(STATS_NOT_READY_MESSAGE); + } let kibanaStats = await kibanaStatsCollector.fetch(); kibanaStats = collectorSet.toApiFieldNames(kibanaStats); diff --git a/src/legacy/server/usage/classes/collector.js b/src/legacy/server/usage/classes/collector.js index f103126740e13..40b004f51e49a 100644 --- a/src/legacy/server/usage/classes/collector.js +++ b/src/legacy/server/usage/classes/collector.js @@ -28,7 +28,7 @@ export class Collector { * @param {Function} options.formatForBulkUpload - optional * @param {Function} options.rest - optional other properties */ - constructor(server, { type, init, fetch, formatForBulkUpload = null, ...options } = {}) { + constructor(server, { type, init, fetch, formatForBulkUpload = null, isReady = null, ...options } = {}) { if (type === undefined) { throw new Error('Collector must be instantiated with a options.type string property'); } @@ -49,6 +49,9 @@ export class Collector { const defaultFormatterForBulkUpload = result => ({ type, payload: result }); this._formatForBulkUpload = formatForBulkUpload || defaultFormatterForBulkUpload; + if (typeof isReady === 'function') { + this.isReady = isReady; + } } /* @@ -69,4 +72,8 @@ export class Collector { formatForBulkUpload(result) { return this._formatForBulkUpload(result); } + + isReady() { + throw `isReady() must be implemented in ${this.type} collector`; + } } diff --git a/src/legacy/server/usage/classes/collector_set.js b/src/legacy/server/usage/classes/collector_set.js index c7140b7abb486..a6b7e69b31261 100644 --- a/src/legacy/server/usage/classes/collector_set.js +++ b/src/legacy/server/usage/classes/collector_set.js @@ -23,18 +23,19 @@ import { getCollectorLogger } from '../lib'; import { Collector } from './collector'; import { UsageCollector } from './usage_collector'; +let _waitingForAllCollectorsTimestamp = null; + /* * A collector object has types registered into it with the register(type) * function. Each type that gets registered defines how to fetch its own data * and optionally, how to combine it into a unified payload for bulk upload. */ export class CollectorSet { - /* * @param {Object} server - server object * @param {Array} collectors to initialize, usually as a result of filtering another CollectorSet instance */ - constructor(server, collectors = []) { + constructor(server, collectors = [], config = null) { this._log = getCollectorLogger(server); this._collectors = collectors; @@ -44,7 +45,9 @@ export class CollectorSet { */ this.makeStatsCollector = options => new Collector(server, options); this.makeUsageCollector = options => new UsageCollector(server, options); - this._makeCollectorSetFromArray = collectorsArray => new CollectorSet(server, collectorsArray); + this._makeCollectorSetFromArray = collectorsArray => new CollectorSet(server, collectorsArray, config); + + this._maximumWaitTimeForAllCollectorsInS = config ? config.get('stats.maximumWaitTimeForAllCollectorsInS') : 60; } /* @@ -73,6 +76,40 @@ export class CollectorSet { return x instanceof UsageCollector; } + async areAllCollectorsReady(collectorSet = this) { + if (!(collectorSet instanceof CollectorSet)) { + throw new Error(`areAllCollectorsReady method given bad collectorSet parameter: ` + typeof collectorSet); + } + + const collectorTypesNotReady = []; + let allReady = true; + await collectorSet.asyncEach(async collector => { + if (!await collector.isReady()) { + allReady = false; + collectorTypesNotReady.push(collector.type); + } + }); + + if (!allReady && this._maximumWaitTimeForAllCollectorsInS >= 0) { + const nowTimestamp = +new Date(); + _waitingForAllCollectorsTimestamp = _waitingForAllCollectorsTimestamp || nowTimestamp; + const timeWaitedInMS = nowTimestamp - _waitingForAllCollectorsTimestamp; + const timeLeftInMS = (this._maximumWaitTimeForAllCollectorsInS * 1000) - timeWaitedInMS; + if (timeLeftInMS <= 0) { + this._log.debug(`All collectors are not ready (waiting for ${collectorTypesNotReady.join(',')}) ` + + `but we have waited the required ` + + `${this._maximumWaitTimeForAllCollectorsInS}s and will return data from all collectors that are ready.`); + return true; + } else { + this._log.debug(`All collectors are not ready. Waiting for ${timeLeftInMS}ms longer.`); + } + } else { + _waitingForAllCollectorsTimestamp = null; + } + + return allReady; + } + /* * Call a bunch of fetch methods and then do them in bulk * @param {CollectorSet} collectorSet - a set of collectors to fetch. Default to all registered collectors @@ -155,4 +192,14 @@ export class CollectorSet { map(mapFn) { return this._collectors.map(mapFn); } + + some(someFn) { + return this._collectors.some(someFn); + } + + async asyncEach(eachFn) { + for (const collector of this._collectors) { + await eachFn(collector); + } + } } diff --git a/src/legacy/server/usage/index.js b/src/legacy/server/usage/index.js index 7cf9ae5b55f52..2a02070a55f95 100644 --- a/src/legacy/server/usage/index.js +++ b/src/legacy/server/usage/index.js @@ -19,8 +19,8 @@ import { CollectorSet } from './classes'; -export function usageMixin(kbnServer, server) { - const collectorSet = new CollectorSet(server); +export function usageMixin(kbnServer, server, config) { + const collectorSet = new CollectorSet(server, undefined, config); /* * expose the collector set object on the server diff --git a/src/legacy/ui/public/_index.scss b/src/legacy/ui/public/_index.scss index 9c889d540d665..0ccfe4b84a1ff 100644 --- a/src/legacy/ui/public/_index.scss +++ b/src/legacy/ui/public/_index.scss @@ -22,7 +22,6 @@ @import './markdown/index'; @import './notify/index'; @import './share/index'; -@import './filter_bar/index'; @import './style_compile/index'; // The following are prefixed with "vis" diff --git a/src/legacy/ui/public/agg_response/hierarchical/build_hierarchical_data.test.js b/src/legacy/ui/public/agg_response/hierarchical/build_hierarchical_data.test.js index 58152f5557fa3..dce7047efbe44 100644 --- a/src/legacy/ui/public/agg_response/hierarchical/build_hierarchical_data.test.js +++ b/src/legacy/ui/public/agg_response/hierarchical/build_hierarchical_data.test.js @@ -18,7 +18,7 @@ */ import { buildHierarchicalData } from './build_hierarchical_data'; -import { LegacyResponseHandlerProvider as legacyResponseHandlerProvider } from '../../vis/response_handlers/legacy'; +import { legacyResponseHandlerProvider } from '../../vis/response_handlers/legacy'; jest.mock('../../registry/field_formats', () => ({ fieldFormats: { diff --git a/src/legacy/ui/public/agg_response/point_series/__tests__/_tooltip_formatter.js b/src/legacy/ui/public/agg_response/point_series/__tests__/_tooltip_formatter.js deleted file mode 100644 index 472836af52b90..0000000000000 --- a/src/legacy/ui/public/agg_response/point_series/__tests__/_tooltip_formatter.js +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import $ from 'jquery'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import { PointSeriesTooltipFormatter } from '../_tooltip_formatter'; - -describe('tooltipFormatter', function () { - - let tooltipFormatter; - - beforeEach(ngMock.module('kibana')); - beforeEach(ngMock.inject(function (Private) { - tooltipFormatter = Private(PointSeriesTooltipFormatter)(); - })); - - function cell($row, i) { - return $row.eq(i).text().trim(); - } - - const baseEvent = { - data: { - xAxisLabel: 'inner', - xAxisFormatter: _.identity, - yAxisLabel: 'middle', - yAxisFormatter: _.identity, - zAxisLabel: 'top', - zAxisFormatter: _.identity, - series: [ - { - rawId: '1', - label: 'middle', - zLabel: 'top', - yAxisFormatter: _.identity, - zAxisFormatter: _.identity - } - ] - }, - datum: { - x: 3, y: 2, z: 1, - extraMetrics: [], - seriesId: '1' - } - }; - - it('returns html based on the mouse event', function () { - const event = _.cloneDeep(baseEvent); - const $el = $(tooltipFormatter(event)); - const $rows = $el.find('tr'); - expect($rows.length).to.be(3); - - const $row1 = $rows.eq(0).find('td'); - expect(cell($row1, 0)).to.be('inner'); - expect(cell($row1, 1)).to.be('3'); - - const $row2 = $rows.eq(1).find('td'); - expect(cell($row2, 0)).to.be('middle'); - expect(cell($row2, 1)).to.be('2'); - - const $row3 = $rows.eq(2).find('td'); - expect(cell($row3, 0)).to.be('top'); - expect(cell($row3, 1)).to.be('1'); - }); - - it('renders correctly on missing extraMetrics in datum', function () { - const event = _.cloneDeep(baseEvent); - delete event.datum.extraMetrics; - const $el = $(tooltipFormatter(event)); - const $rows = $el.find('tr'); - expect($rows.length).to.be(3); - }); -}); diff --git a/src/legacy/ui/public/agg_response/point_series/__tests__/point_series.js b/src/legacy/ui/public/agg_response/point_series/__tests__/point_series.js index a649b7f38fc91..531fecae8ba9d 100644 --- a/src/legacy/ui/public/agg_response/point_series/__tests__/point_series.js +++ b/src/legacy/ui/public/agg_response/point_series/__tests__/point_series.js @@ -28,5 +28,4 @@ describe('Point Series Agg Response', function () { require('./_init_x_axis'); require('./_init_y_axis'); require('./_ordered_date_axis'); - require('./_tooltip_formatter'); }); diff --git a/src/legacy/ui/public/agg_response/point_series/_tooltip_formatter.js b/src/legacy/ui/public/agg_response/point_series/_tooltip_formatter.js deleted file mode 100644 index ed35d03b17c39..0000000000000 --- a/src/legacy/ui/public/agg_response/point_series/_tooltip_formatter.js +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import $ from 'jquery'; -import { getFormat } from '../../visualize/loader/pipeline_helpers/utilities'; - -export function PointSeriesTooltipFormatter($compile, $rootScope) { - - const $tooltipScope = $rootScope.$new(); - const $tooltip = $(require('ui/agg_response/point_series/_tooltip.html')); - $compile($tooltip)($tooltipScope); - - return function () { - return function tooltipFormatter(event) { - const data = event.data; - const datum = event.datum; - if (!datum) return ''; - - const details = $tooltipScope.details = []; - - const currentSeries = data.series && data.series.find(serie => serie.rawId === datum.seriesId); - const addDetail = (label, value) => details.push({ label, value }); - - if (datum.extraMetrics) { - datum.extraMetrics.forEach(metric => { - addDetail(metric.label, metric.value); - }); - } - - if (datum.x) { - addDetail(data.xAxisLabel, data.xAxisFormatter(datum.x)); - } - if (datum.y) { - const value = datum.yScale ? datum.yScale * datum.y : datum.y; - addDetail(currentSeries.label, currentSeries.yAxisFormatter(value)); - } - if (datum.z) { - addDetail(currentSeries.zLabel, currentSeries.zAxisFormatter(datum.z)); - } - if (datum.series && datum.parent) { - const dimension = datum.parent; - const seriesFormatter = getFormat(dimension.format); - addDetail(dimension.title, seriesFormatter.convert(datum.series)); - } - if (datum.tableRaw) { - addDetail(datum.tableRaw.title, datum.tableRaw.value); - } - - $tooltipScope.$apply(); - return $tooltip[0].outerHTML; - }; - }; -} diff --git a/src/legacy/ui/public/agg_types/__tests__/buckets/_date_range.js b/src/legacy/ui/public/agg_types/__tests__/buckets/_date_range.js new file mode 100644 index 0000000000000..10ac4f3befeb7 --- /dev/null +++ b/src/legacy/ui/public/agg_types/__tests__/buckets/_date_range.js @@ -0,0 +1,70 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { set } from 'lodash'; +import expect from '@kbn/expect'; +import sinon from 'sinon'; +import ngMock from 'ng_mock'; +import AggParamWriterProvider from '../agg_param_writer'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; +import chrome from '../../../chrome'; + +const config = chrome.getUiSettingsClient(); + +describe('date_range params', function () { + let paramWriter; + let timeField; + + beforeEach(ngMock.module('kibana')); + beforeEach(ngMock.inject(function (Private) { + const AggParamWriter = Private(AggParamWriterProvider); + const indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); + + timeField = indexPattern.timeFieldName; + paramWriter = new AggParamWriter({ aggType: 'date_range' }); + })); + describe('time_zone', () => { + beforeEach(() => { + sinon.stub(config, 'get'); + sinon.stub(config, 'isDefault'); + }); + + it('should use the specified time_zone', () => { + const output = paramWriter.write({ time_zone: 'Europe/Kiev' }); + expect(output.params).to.have.property('time_zone', 'Europe/Kiev'); + }); + + it('should use the Kibana time_zone if no parameter specified', () => { + config.isDefault.withArgs('dateFormat:tz').returns(false); + config.get.withArgs('dateFormat:tz').returns('Europe/Riga'); + const output = paramWriter.write({}); + expect(output.params).to.have.property('time_zone', 'Europe/Riga'); + }); + + it('should use the fixed time_zone from the index pattern typeMeta', () => { + set(paramWriter.indexPattern, ['typeMeta', 'aggs', 'date_range', timeField, 'time_zone'], 'Europe/Rome'); + const output = paramWriter.write({ field: timeField }); + expect(output.params).to.have.property('time_zone', 'Europe/Rome'); + }); + + afterEach(() => { + config.get.restore(); + config.isDefault.restore(); + }); + }); +}); diff --git a/src/legacy/ui/public/agg_types/__tests__/buckets/_geo_hash.js b/src/legacy/ui/public/agg_types/__tests__/buckets/_geo_hash.js index 39c27f0b8db24..a9bb92ff26dcf 100644 --- a/src/legacy/ui/public/agg_types/__tests__/buckets/_geo_hash.js +++ b/src/legacy/ui/public/agg_types/__tests__/buckets/_geo_hash.js @@ -94,7 +94,7 @@ describe('Geohash Agg', () => { describe('precision parameter', () => { - const PRECISION_PARAM_INDEX = 7; + const PRECISION_PARAM_INDEX = 2; let precisionParam; beforeEach(() => { precisionParam = geoHashBucketAgg.params[PRECISION_PARAM_INDEX]; diff --git a/src/legacy/ui/public/agg_types/__tests__/buckets/create_filter/filters.js b/src/legacy/ui/public/agg_types/__tests__/buckets/create_filter/filters.js index 01337f2725045..93e1bc6497289 100644 --- a/src/legacy/ui/public/agg_types/__tests__/buckets/create_filter/filters.js +++ b/src/legacy/ui/public/agg_types/__tests__/buckets/create_filter/filters.js @@ -43,8 +43,8 @@ describe('AggConfig Filters', function () { schema: 'segment', params: { filters: [ - { input: { query: { query_string: { query: 'type:apache' } } } }, - { input: { query: { query_string: { query: 'type:nginx' } } } } + { input: { query: 'type:apache', language: 'lucene' } }, + { input: { query: 'type:nginx', language: 'lucene' } } ] } } @@ -53,9 +53,9 @@ describe('AggConfig Filters', function () { const aggConfig = vis.aggs.byTypeName.filters[0]; const filter = createFilterFilters(aggConfig, 'type:nginx'); - expect(filter.query.query_string.query).to.be('type:nginx'); + expect(filter.query.bool.must[0].query_string.query).to.be('type:nginx'); expect(filter.meta).to.have.property('index', indexPattern.id); - + expect(filter.meta).to.have.property('alias', 'type:nginx'); }); }); }); diff --git a/src/legacy/ui/public/agg_types/__tests__/buckets/terms.js b/src/legacy/ui/public/agg_types/__tests__/buckets/terms.js index fd335fc5ba5eb..0ad43dace3e8d 100644 --- a/src/legacy/ui/public/agg_types/__tests__/buckets/terms.js +++ b/src/legacy/ui/public/agg_types/__tests__/buckets/terms.js @@ -28,7 +28,7 @@ describe('Terms Agg', function () { function init({ responseValueAggs = [], aggParams = {} }) { ngMock.module('kibana'); - ngMock.inject(function (Private, $controller, _$rootScope_) { + ngMock.inject(function ($controller, _$rootScope_) { const terms = aggTypes.byName.terms; const orderAggController = terms.params.byName.orderAgg.controller; @@ -47,84 +47,8 @@ describe('Terms Agg', function () { }); } - it('defaults to the first metric agg', function () { - init({ - responseValueAggs: [ - { - id: 'agg1', - type: { - name: 'count' - } - }, - { - id: 'agg2', - type: { - name: 'count' - } - } - ] - }); - expect($rootScope.agg.params.orderBy).to.be('agg1'); - }); - - it('defaults to the first metric agg that is compatible with the terms bucket', function () { - init({ - responseValueAggs: [ - { - id: 'agg1', - type: { - name: 'top_hits' - } - }, - { - id: 'agg2', - type: { - name: 'percentiles' - } - }, - { - id: 'agg3', - type: { - name: 'median' - } - }, - { - id: 'agg4', - type: { - name: 'std_dev' - } - }, - { - id: 'agg5', - type: { - name: 'count' - } - } - ] - }); - expect($rootScope.agg.params.orderBy).to.be('agg5'); - }); - - it('defaults to the _key metric if no agg is compatible', function () { - init({ - responseValueAggs: [ - { - id: 'agg1', - type: { - name: 'top_hits' - } - } - ] - }); - expect($rootScope.agg.params.orderBy).to.be('_key'); - }); - - it('selects _key if there are no metric aggs', function () { - init({}); - expect($rootScope.agg.params.orderBy).to.be('_key'); - }); - - it('selects _key if the selected metric becomes incompatible', function () { + // should be rewritten after EUIficate order_agg.html + it.skip('selects _key if the selected metric becomes incompatible', function () { init({ responseValueAggs: [ { @@ -148,36 +72,8 @@ describe('Terms Agg', function () { expect($rootScope.agg.params.orderBy).to.be('_key'); }); - it('selects first metric if it is avg', function () { - init({ - responseValueAggs: [ - { - id: 'agg1', - type: { - name: 'avg', - field: 'bytes' - } - } - ] - }); - expect($rootScope.agg.params.orderBy).to.be('agg1'); - }); - - it('selects _key if the first metric is avg_bucket', function () { - $rootScope.responseValueAggs = [ - { - id: 'agg1', - type: { - name: 'avg_bucket', - metric: 'custom' - } - } - ]; - $rootScope.$digest(); - expect($rootScope.agg.params.orderBy).to.be('_key'); - }); - - it('selects _key if the selected metric is removed', function () { + // should be rewritten after EUIficate order_agg.html + it.skip('selects _key if the selected metric is removed', function () { init({ responseValueAggs: [ { diff --git a/src/legacy/ui/public/agg_types/__tests__/controls/number_list.js b/src/legacy/ui/public/agg_types/__tests__/controls/number_list.js deleted file mode 100644 index f1b92317c6f94..0000000000000 --- a/src/legacy/ui/public/agg_types/__tests__/controls/number_list.js +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import $ from 'jquery'; -import expect from '@kbn/expect'; -import simulateKeys from 'test_utils/simulate_keys'; -import ngMock from 'ng_mock'; -import '../../number_list'; -describe('NumberList directive', function () { - - - let $el; - let $scope; - let compile; - - function onlyValidValues() { - return $el.find('[ng-model]').toArray().map(function (el) { - const ngModel = $(el).controller('ngModel'); - return ngModel.$valid ? ngModel.$modelValue : undefined; - }); - } - - beforeEach(ngMock.module('kibana')); - beforeEach(ngMock.inject(function ($injector) { - const $compile = $injector.get('$compile'); - const $rootScope = $injector.get('$rootScope'); - - $scope = $rootScope.$new(); - $el = $(''); - compile = function (vals, range) { - $scope.vals = vals || []; - $el.attr('range', range); - - $compile($el)($scope); - $scope.$apply(); - }; - })); - - afterEach(function () { - $el.remove(); - $scope.$destroy(); - }); - - it('fails on invalid numbers', function () { - compile([1, 'foo']); - expect(onlyValidValues()).to.eql([1, undefined]); - }); - - it('supports decimals', function () { - compile(['1.2', '000001.6', '99.10']); - expect(onlyValidValues()).to.eql([1.2, 1.6, 99.1]); - }); - - it('ensures that the values are in order', function () { - compile([1, 2, 3, 10, 4, 5]); - expect(onlyValidValues()).to.eql([1, 2, 3, 10, undefined, 5]); - }); - - it('requires that values are within a range', function () { - compile([50, 100, 200, 250], '[100, 200)'); - expect(onlyValidValues()).to.eql([undefined, 100, undefined, undefined]); - }); - - describe('listens for keyboard events', function () { - it('up arrow increases by 1', function () { - compile([1]); - - return simulateKeys( - function () { return $el.find('input').first(); }, - ['up', 'up', 'up'] - ) - .then(function () { - expect(onlyValidValues()).to.eql([4]); - }); - }); - - it('shift-up increases by 0.1', function () { - compile([4.8]); - - const seq = [ - { - type: 'press', - key: 'shift', - events: [ - 'up', - 'up', - 'up' - ] - } - ]; - - return simulateKeys( - function () { return $el.find('input').first(); }, - seq - ) - .then(function () { - expect(onlyValidValues()).to.eql([5.1]); - }); - }); - - it('down arrow decreases by 1', function () { - compile([5]); - - return simulateKeys( - function () { return $el.find('input').first(); }, - ['down', 'down', 'down'] - ) - .then(function () { - expect(onlyValidValues()).to.eql([2]); - }); - }); - - it('shift-down decreases by 0.1', function () { - compile([5.1]); - - const seq = [ - { - type: 'press', - key: 'shift', - events: [ - 'down', - 'down', - 'down' - ] - } - ]; - - return simulateKeys( - function () { return $el.find('input').first(); }, - seq - ) - .then(function () { - expect(onlyValidValues()).to.eql([4.8]); - }); - }); - - it('maintains valid number', function () { - compile([9, 11, 13]); - - const seq = [ - 'down', // 10 (11 - 1) - 'down' // 10 (limited by 9) - ]; - - const getEl = function () { return $el.find('input').eq(1); }; - - return simulateKeys(getEl, seq) - .then(function () { - expect(onlyValidValues()).to.eql([9, 10, 13]); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/agg_types/__tests__/directives/auto_select_if_only_one.js b/src/legacy/ui/public/agg_types/__tests__/directives/auto_select_if_only_one.js deleted file mode 100644 index 9e83243811d95..0000000000000 --- a/src/legacy/ui/public/agg_types/__tests__/directives/auto_select_if_only_one.js +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - - -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import '../../directives/auto_select_if_only_one'; - -describe('Auto-select if only one directive', function () { - let $compile; - let $rootScope; - const zeroOptions = []; - const oneOption = [{ label: 'foo' }]; - const multiOptions = [{ label: 'foo' }, { label: 'bar' }]; - - beforeEach(ngMock.module('kibana')); - - beforeEach(ngMock.inject(function (_$compile_, _$rootScope_) { - $compile = _$compile_; - $rootScope = _$rootScope_; - const html = ''; - $compile(html)($rootScope); - $rootScope.value = null; - })); - - it('should not auto-select if there are no options', function () { - $rootScope.options = zeroOptions; - $rootScope.$digest(); - expect($rootScope.value).to.not.be.ok(); - }); - - it('should not auto-select if there are multiple options', function () { - $rootScope.options = multiOptions; - $rootScope.$digest(); - expect($rootScope.value).to.not.be.ok(); - }); - - it('should auto-select if there is only one option', function () { - $rootScope.options = oneOption; - $rootScope.$digest(); - expect($rootScope.value).to.be(oneOption[0]); - }); - - it('should still auto select if the collection contains 2 items but is filtered to 1', function () { - $rootScope.options = multiOptions; - const html = ''; - $compile(html)($rootScope); - $rootScope.value = null; - $rootScope.$digest(); - - expect($rootScope.value).to.be(multiOptions[1]); - }); - - it('should auto-select if the collection changes', function () { - $rootScope.options = multiOptions; - $rootScope.$digest(); - expect($rootScope.value).to.not.be.ok(); - $rootScope.options = oneOption; - $rootScope.$digest(); - expect($rootScope.value).to.be(oneOption[0]); - }); - - it('should auto-select if the collection is mutated', function () { - $rootScope.options = multiOptions.slice(); - $rootScope.$digest(); - expect($rootScope.value).to.not.be.ok(); - $rootScope.options.length = 1; - $rootScope.$digest(); - expect($rootScope.value).to.be($rootScope.options[0]); - }); -}); diff --git a/src/legacy/ui/public/agg_types/__tests__/directives/validate_cidr_mask.js b/src/legacy/ui/public/agg_types/__tests__/directives/validate_cidr_mask.js deleted file mode 100644 index 3d5d2531ee85f..0000000000000 --- a/src/legacy/ui/public/agg_types/__tests__/directives/validate_cidr_mask.js +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import '../../directives/validate_cidr_mask'; - - -describe('Validate CIDR mask directive', function () { - let $compile; - let $rootScope; - const html = ''; - - beforeEach(ngMock.module('kibana')); - - beforeEach(ngMock.inject(function (_$compile_, _$rootScope_) { - $compile = _$compile_; - $rootScope = _$rootScope_; - })); - - it('should allow empty input', function () { - const element = $compile(html)($rootScope); - - $rootScope.value = ''; - $rootScope.$digest(); - expect(element.hasClass('ng-valid')).to.be.ok(); - - $rootScope.value = null; - $rootScope.$digest(); - expect(element.hasClass('ng-valid')).to.be.ok(); - - $rootScope.value = undefined; - $rootScope.$digest(); - expect(element.hasClass('ng-valid')).to.be.ok(); - }); - - it('should allow valid CIDR masks', function () { - const element = $compile(html)($rootScope); - - $rootScope.value = '0.0.0.0/1'; - $rootScope.$digest(); - expect(element.hasClass('ng-valid')).to.be.ok(); - - $rootScope.value = '128.0.0.1/31'; - $rootScope.$digest(); - expect(element.hasClass('ng-valid')).to.be.ok(); - - $rootScope.value = '1.2.3.4/2'; - $rootScope.$digest(); - expect(element.hasClass('ng-valid')).to.be.ok(); - - $rootScope.value = '67.129.65.201/27'; - $rootScope.$digest(); - expect(element.hasClass('ng-valid')).to.be.ok(); - }); - - it('should disallow invalid CIDR masks', function () { - const element = $compile(html)($rootScope); - - $rootScope.value = 'hello, world'; - $rootScope.$digest(); - expect(element.hasClass('ng-invalid')).to.be.ok(); - - $rootScope.value = '0.0.0.0'; - $rootScope.$digest(); - expect(element.hasClass('ng-invalid')).to.be.ok(); - - $rootScope.value = '0.0.0.0/0'; - $rootScope.$digest(); - expect(element.hasClass('ng-invalid')).to.be.ok(); - - $rootScope.value = '0.0.0.0/33'; - $rootScope.$digest(); - expect(element.hasClass('ng-invalid')).to.be.ok(); - - $rootScope.value = '256.0.0.0/32'; - $rootScope.$digest(); - expect(element.hasClass('ng-invalid')).to.be.ok(); - - $rootScope.value = '0.0.0.0/32/32'; - $rootScope.$digest(); - expect(element.hasClass('ng-invalid')).to.be.ok(); - - $rootScope.value = '1.2.3/1'; - $rootScope.$digest(); - expect(element.hasClass('ng-invalid')).to.be.ok(); - }); -}); diff --git a/src/legacy/ui/public/agg_types/__tests__/directives/validate_ip.js b/src/legacy/ui/public/agg_types/__tests__/directives/validate_ip.js deleted file mode 100644 index ad4c91e9081bf..0000000000000 --- a/src/legacy/ui/public/agg_types/__tests__/directives/validate_ip.js +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import '../../directives/validate_ip'; - - -describe('Validate IP directive', function () { - let $compile; - let $rootScope; - const html = ''; - - beforeEach(ngMock.module('kibana')); - - beforeEach(ngMock.inject(function (_$compile_, _$rootScope_) { - $compile = _$compile_; - $rootScope = _$rootScope_; - })); - - it('should allow empty input', function () { - const element = $compile(html)($rootScope); - - $rootScope.value = ''; - $rootScope.$digest(); - expect(element.hasClass('ng-valid')).to.be.ok(); - - $rootScope.value = null; - $rootScope.$digest(); - expect(element.hasClass('ng-valid')).to.be.ok(); - - $rootScope.value = undefined; - $rootScope.$digest(); - expect(element.hasClass('ng-valid')).to.be.ok(); - }); - - it('should allow valid IP addresses', function () { - const element = $compile(html)($rootScope); - - $rootScope.value = '0.0.0.0'; - $rootScope.$digest(); - expect(element.hasClass('ng-valid')).to.be.ok(); - - $rootScope.value = '0.0.0.1'; - $rootScope.$digest(); - expect(element.hasClass('ng-valid')).to.be.ok(); - - $rootScope.value = '126.45.211.34'; - $rootScope.$digest(); - expect(element.hasClass('ng-valid')).to.be.ok(); - - $rootScope.value = '255.255.255.255'; - $rootScope.$digest(); - expect(element.hasClass('ng-valid')).to.be.ok(); - }); - - it('should disallow invalid IP addresses', function () { - const element = $compile(html)($rootScope); - - $rootScope.value = 'hello, world'; - $rootScope.$digest(); - expect(element.hasClass('ng-invalid')).to.be.ok(); - - $rootScope.value = '0.0.0'; - $rootScope.$digest(); - expect(element.hasClass('ng-invalid')).to.be.ok(); - - $rootScope.value = '256.0.0.0'; - $rootScope.$digest(); - expect(element.hasClass('ng-invalid')).to.be.ok(); - - $rootScope.value = '-1.0.0.0'; - $rootScope.$digest(); - expect(element.hasClass('ng-invalid')).to.be.ok(); - - $rootScope.value = Number.MAX_VALUE; - $rootScope.$digest(); - expect(element.hasClass('ng-invalid')).to.be.ok(); - }); -}); diff --git a/src/legacy/ui/public/agg_types/__tests__/metrics/top_hit.js b/src/legacy/ui/public/agg_types/__tests__/metrics/top_hit.js index d206b8f47df9b..4ed86da68c408 100644 --- a/src/legacy/ui/public/agg_types/__tests__/metrics/top_hit.js +++ b/src/legacy/ui/public/agg_types/__tests__/metrics/top_hit.js @@ -39,10 +39,10 @@ describe('Top hit metric', function () { params.field = field; } params.sortOrder = { - val: sortOrder + value: sortOrder }; params.aggregate = { - val: aggregate + value: aggregate }; params.size = size; const vis = new Vis(indexPattern, { diff --git a/src/legacy/ui/public/agg_types/buckets/_inline_comp_wrapper.js b/src/legacy/ui/public/agg_types/buckets/_inline_comp_wrapper.js index 8091d8e28dde5..0c8161035adcf 100644 --- a/src/legacy/ui/public/agg_types/buckets/_inline_comp_wrapper.js +++ b/src/legacy/ui/public/agg_types/buckets/_inline_comp_wrapper.js @@ -21,7 +21,7 @@ import React from 'react'; const wrapWithInlineComp = Component => props => (
- +
); export { wrapWithInlineComp }; diff --git a/src/legacy/ui/public/agg_types/buckets/_terms_helper.tsx b/src/legacy/ui/public/agg_types/buckets/_terms_helper.tsx new file mode 100644 index 0000000000000..f613673b0484a --- /dev/null +++ b/src/legacy/ui/public/agg_types/buckets/_terms_helper.tsx @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { AggConfig } from 'ui/vis'; +import { i18n } from '@kbn/i18n'; + +const aggFilter = [ + '!top_hits', + '!percentiles', + '!median', + '!std_dev', + '!derivative', + '!moving_avg', + '!serial_diff', + '!cumulative_sum', + '!avg_bucket', + '!max_bucket', + '!min_bucket', + '!sum_bucket', +]; + +// Returns true if the agg is compatible with the terms bucket +function isCompatibleAgg(agg: AggConfig) { + return !aggFilter.includes(`!${agg.type.name}`); +} + +function safeMakeLabel(agg: AggConfig) { + try { + return agg.makeLabel(); + } catch (e) { + return i18n.translate('common.ui.aggTypes.buckets.terms.aggNotValidLabel', { + defaultMessage: '- agg not valid -', + }); + } +} + +export { aggFilter, isCompatibleAgg, safeMakeLabel }; diff --git a/src/legacy/ui/public/agg_types/buckets/create_filter/filters.js b/src/legacy/ui/public/agg_types/buckets/create_filter/filters.js index e2e141ddf2adc..c213a4aa103d9 100644 --- a/src/legacy/ui/public/agg_types/buckets/create_filter/filters.js +++ b/src/legacy/ui/public/agg_types/buckets/create_filter/filters.js @@ -26,6 +26,6 @@ export function createFilterFilters(aggConfig, key) { const filter = dslFilters[key]; if (filter) { - return buildQueryFilter(filter.query, aggConfig.getIndexPattern().id); + return buildQueryFilter(filter.query, aggConfig.getIndexPattern().id, key); } } diff --git a/src/legacy/ui/public/agg_types/buckets/date_range.js b/src/legacy/ui/public/agg_types/buckets/date_range.js index 3f74791bb3a94..e975a33035d8c 100644 --- a/src/legacy/ui/public/agg_types/buckets/date_range.js +++ b/src/legacy/ui/public/agg_types/buckets/date_range.js @@ -16,7 +16,9 @@ * specific language governing permissions and limitations * under the License. */ - +import { get } from 'lodash'; +import chrome from '../../chrome'; +import moment from 'moment-timezone'; import { dateRange } from '../../utils/date_range'; import '../directives/validate_date_math'; import '../directives/documentation_href'; @@ -26,6 +28,10 @@ import { fieldFormats } from '../../registry/field_formats'; import dateRangesTemplate from '../controls/date_ranges.html'; import { i18n } from '@kbn/i18n'; +const config = chrome.getUiSettingsClient(); +const detectedTimezone = moment.tz.guess(); +const tzOffset = moment().format('Z'); + export const dateRangeBucketAgg = new BucketAggType({ name: 'date_range', title: i18n.translate('common.ui.aggTypes.buckets.dateRangeTitle', { @@ -56,5 +62,21 @@ export const dateRangeBucketAgg = new BucketAggType({ to: 'now' }], editor: dateRangesTemplate + }, { + name: 'time_zone', + default: undefined, + // Implimentation method is the same as that of date_histogram + serialize: () => undefined, + write: (agg, output) => { + let tz = agg.params.time_zone; + if (!tz && agg.params.field) { + tz = get(agg.getIndexPattern(), ['typeMeta', 'aggs', 'date_range', agg.params.field.name, 'time_zone']); + } + if (!tz) { + const isDefaultTimezone = config.isDefault('dateFormat:tz'); + tz = isDefaultTimezone ? detectedTimezone || tzOffset : config.get('dateFormat:tz'); + } + output.params.time_zone = tz; + } }] }); diff --git a/src/legacy/ui/public/agg_types/buckets/filters.js b/src/legacy/ui/public/agg_types/buckets/filters.js index b052b3444e8f0..87b275abef277 100644 --- a/src/legacy/ui/public/agg_types/buckets/filters.js +++ b/src/legacy/ui/public/agg_types/buckets/filters.js @@ -22,11 +22,15 @@ import angular from 'angular'; import { BucketAggType } from './_bucket_agg_type'; import { createFilterFilters } from './create_filter/filters'; -import { decorateQuery, luceneStringToDsl } from '@kbn/es-query'; import { FiltersParamEditor } from '../controls/filters'; import { i18n } from '@kbn/i18n'; import chrome from 'ui/chrome'; +import { buildEsQuery } from '@kbn/es-query'; +import { data } from 'plugins/data'; + +const { getQueryLog } = data.query.helpers; +const config = chrome.getUiSettingsClient(); export const filtersBucketAgg = new BucketAggType({ name: 'filters', @@ -40,32 +44,36 @@ export const filtersBucketAgg = new BucketAggType({ { name: 'filters', editorComponent: FiltersParamEditor, - default: [ { input: {}, label: '' } ], + default: [ { input: { query: '', language: config.get('search:queryLanguage') }, label: '' } ], write: function (aggConfig, output) { const inFilters = aggConfig.params.filters; if (!_.size(inFilters)) return; + inFilters.forEach((filter) => { + const persistedLog = getQueryLog('filtersAgg', filter.input.language); + persistedLog.add(filter.input.query); + }); + const outFilters = _.transform(inFilters, function (filters, filter) { - const input = _.cloneDeep(filter.input); + let input = _.cloneDeep(filter.input); - if (!input) { + if (!input || !input.query) { console.log('malformed filter agg params, missing "input" query'); // eslint-disable-line no-console return; } - const query = input.query = luceneStringToDsl(input.query); + const query = input = buildEsQuery(aggConfig.getIndexPattern(), [input], [], config); + if (!query) { console.log('malformed filter agg params, missing "query" on input'); // eslint-disable-line no-console return; } - const config = chrome.getUiSettingsClient(); - const queryStringOptions = config.get('query:queryString:options'); - - decorateQuery(query, queryStringOptions); - const matchAllLabel = (filter.input.query === '' && _.has(query, 'match_all')) ? '*' : ''; - const label = filter.label || matchAllLabel || _.get(query, 'query_string.query') || angular.toJson(query); - filters[label] = input; + const matchAllLabel = filter.input.query === '' ? '*' : ''; + const label = filter.label + || matchAllLabel + || (typeof filter.input.query === 'string' ? filter.input.query : angular.toJson(filter.input.query)); + filters[label] = { query: input }; }, {}); if (!_.size(outFilters)) return; diff --git a/src/legacy/ui/public/agg_types/buckets/geo_hash.js b/src/legacy/ui/public/agg_types/buckets/geo_hash.js index f3bc503da2d22..4e05883c04a4e 100644 --- a/src/legacy/ui/public/agg_types/buckets/geo_hash.js +++ b/src/legacy/ui/public/agg_types/buckets/geo_hash.js @@ -17,10 +17,12 @@ * under the License. */ -import _ from 'lodash'; import chrome from '../../chrome'; import { BucketAggType } from './_bucket_agg_type'; -import precisionTemplate from '../controls/precision.html'; +import { AutoPrecisionParamEditor } from '../controls/auto_precision'; +import { UseGeocentroidParamEditor } from '../controls/use_geocentroid'; +import { IsFilteredByCollarParamEditor } from '../controls/is_filtered_by_collar'; +import { PrecisionParamEditor } from '../controls/precision'; import { geohashColumns } from '../../utils/decode_geo_hash'; import { geoContains, scaleBounds } from '../../utils/geo_utils'; import { i18n } from '@kbn/i18n'; @@ -80,47 +82,48 @@ export const geoHashBucketAgg = new BucketAggType({ }, { name: 'autoPrecision', + editorComponent: AutoPrecisionParamEditor, default: true, - write: _.noop + write: () => {}, }, { - name: 'isFilteredByCollar', - default: true, - write: _.noop + name: 'precision', + editorComponent: PrecisionParamEditor, + default: defaultPrecision, + deserialize: getPrecision, + write: function (aggConfig, output) { + const currZoom = aggConfig.params.mapZoom; + const autoPrecisionVal = zoomPrecision[currZoom]; + output.params.precision = aggConfig.params.autoPrecision ? + autoPrecisionVal : getPrecision(aggConfig.params.precision); + } }, { name: 'useGeocentroid', + editorComponent: UseGeocentroidParamEditor, + default: true, + write: () => {}, + }, + { + name: 'isFilteredByCollar', + editorComponent: IsFilteredByCollarParamEditor, default: true, - write: _.noop + write: () => {}, }, { name: 'mapZoom', default: 2, - write: _.noop + write: () => {}, }, { name: 'mapCenter', default: [0, 0], - write: _.noop + write: () => {}, }, { name: 'mapBounds', default: null, - write: _.noop - }, - { - name: 'precision', - editor: precisionTemplate, - default: defaultPrecision, - deserialize: getPrecision, - controller: function () { - }, - write: function (aggConfig, output) { - const currZoom = aggConfig.params.mapZoom; - const autoPrecisionVal = zoomPrecision[currZoom]; - output.params.precision = aggConfig.params.autoPrecision ? - autoPrecisionVal : getPrecision(aggConfig.params.precision); - } + write: () => {}, } ], getRequestAggs: function (agg) { diff --git a/src/legacy/ui/public/agg_types/buckets/ip_range.js b/src/legacy/ui/public/agg_types/buckets/ip_range.js index 621e52e1a99ec..023f2d3cdb520 100644 --- a/src/legacy/ui/public/agg_types/buckets/ip_range.js +++ b/src/legacy/ui/public/agg_types/buckets/ip_range.js @@ -18,11 +18,10 @@ */ import _ from 'lodash'; -import '../directives/validate_ip'; -import '../directives/validate_cidr_mask'; import { BucketAggType } from './_bucket_agg_type'; import { createFilterIpRange } from './create_filter/ip_range'; -import ipRangesTemplate from '../controls/ip_ranges.html'; +import { IpRangeTypeParamEditor } from '../controls/ip_range_type'; +import { IpRangesParamEditor } from '../controls/ip_ranges'; import { i18n } from '@kbn/i18n'; export const ipRangeBucketAgg = new BucketAggType({ @@ -52,6 +51,7 @@ export const ipRangeBucketAgg = new BucketAggType({ filterFieldTypes: 'ip' }, { name: 'ipRangeType', + editorComponent: IpRangeTypeParamEditor, default: 'fromTo', write: _.noop }, { @@ -66,7 +66,7 @@ export const ipRangeBucketAgg = new BucketAggType({ { mask: '128.0.0.0/2' } ] }, - editor: ipRangesTemplate, + editorComponent: IpRangesParamEditor, write: function (aggConfig, output) { const ipRangeType = aggConfig.params.ipRangeType; let ranges = aggConfig.params.ranges[ipRangeType]; diff --git a/src/legacy/ui/public/agg_types/buckets/terms.js b/src/legacy/ui/public/agg_types/buckets/terms.js index 8b2093a8b1c1e..4eee239730007 100644 --- a/src/legacy/ui/public/agg_types/buckets/terms.js +++ b/src/legacy/ui/public/agg_types/buckets/terms.js @@ -17,29 +17,23 @@ * under the License. */ -import _ from 'lodash'; import chrome from 'ui/chrome'; +import { i18n } from '@kbn/i18n'; import { BucketAggType } from './_bucket_agg_type'; import { AggConfig } from '../../vis/agg_config'; import { Schemas } from '../../vis/editors/default/schemas'; +import { getRequestInspectorStats, getResponseInspectorStats } from '../../courier/utils/courier_inspector_utils'; import { createFilterTerms } from './create_filter/terms'; +import { wrapWithInlineComp } from './_inline_comp_wrapper'; +import { buildOtherBucketAgg, mergeOtherBucketAggResponse, updateMissingBucket } from './_terms_other_bucket_helper'; +import { isStringType, migrateIncludeExcludeFormat } from './migrate_include_exclude_format'; +import { aggFilter } from './_terms_helper'; import orderAggTemplate from '../controls/order_agg.html'; import { OrderParamEditor } from '../controls/order'; +import { OrderAggParamEditor } from '../controls/order_agg'; import { SizeParamEditor } from '../controls/size'; -import { wrapWithInlineComp } from './_inline_comp_wrapper'; -import { i18n } from '@kbn/i18n'; - -import { getRequestInspectorStats, getResponseInspectorStats } from '../../courier/utils/courier_inspector_utils'; -import { buildOtherBucketAgg, mergeOtherBucketAggResponse, updateMissingBucket } from './_terms_other_bucket_helper'; import { MissingBucketParamEditor } from '../controls/missing_bucket'; import { OtherBucketParamEditor } from '../controls/other_bucket'; -import { isStringType, migrateIncludeExcludeFormat } from './migrate_include_exclude_format'; - -const aggFilter = [ - '!top_hits', '!percentiles', '!median', '!std_dev', - '!derivative', '!moving_avg', '!serial_diff', '!cumulative_sum', - '!avg_bucket', '!max_bucket', '!min_bucket', '!sum_bucket' -]; const orderAggSchema = (new Schemas([ { @@ -120,6 +114,11 @@ export const termsBucketAgg = new BucketAggType({ type: 'field', filterFieldTypes: ['number', 'boolean', 'date', 'ip', 'string'] }, + { + name: 'orderBy', + editorComponent: OrderAggParamEditor, + write: () => {} // prevent default write, it's handled by orderAgg + }, { name: 'orderAgg', type: AggConfig, @@ -139,33 +138,9 @@ export const termsBucketAgg = new BucketAggType({ return orderAgg; }, controller: function ($scope) { - $scope.safeMakeLabel = function (agg) { - try { - return agg.makeLabel(); - } catch (e) { - return i18n.translate('common.ui.aggTypes.buckets.terms.aggNotValidLabel', { - defaultMessage: '- agg not valid -', - }); - } - }; - - const INIT = {}; // flag to know when prevOrderBy has changed - let prevOrderBy = INIT; - $scope.$watch('responseValueAggs', updateOrderAgg); $scope.$watch('agg.params.orderBy', updateOrderAgg); - // Returns true if the agg is not compatible with the terms bucket - $scope.rejectAgg = function rejectAgg(agg) { - return aggFilter.includes(`!${agg.type.name}`); - }; - - $scope.$watch('agg.params.field.type', (type) => { - if (type !== 'string') { - $scope.agg.params.missingBucket = false; - } - }); - function updateOrderAgg() { // abort until we get the responseValueAggs if (!$scope.responseValueAggs) return; @@ -174,27 +149,9 @@ export const termsBucketAgg = new BucketAggType({ const orderBy = params.orderBy; const paramDef = agg.type.params.byName.orderAgg; - // setup the initial value of orderBy - if (!orderBy && prevOrderBy === INIT) { - let respAgg = _($scope.responseValueAggs).filter((agg) => !$scope.rejectAgg(agg)).first(); - if (!respAgg) { - respAgg = { id: '_key' }; - } - params.orderBy = respAgg.id; - return; - } - - // track the previous value - prevOrderBy = orderBy; - // we aren't creating a custom aggConfig if (!orderBy || orderBy !== 'custom') { params.orderAgg = null; - // ensure that orderBy is set to a valid agg - const respAgg = _($scope.responseValueAggs).filter((agg) => !$scope.rejectAgg(agg)).find({ id: orderBy }); - if (!respAgg) { - params.orderBy = '_key'; - } return; } @@ -256,22 +213,18 @@ export const termsBucketAgg = new BucketAggType({ value: 'asc' } ], - write: _.noop // prevent default write, it's handled by orderAgg + write: () => {} // prevent default write, it's handled by orderAgg }, { name: 'size', editorComponent: wrapWithInlineComp(SizeParamEditor), default: 5 }, - { - name: 'orderBy', - write: _.noop // prevent default write, it's handled by orderAgg - }, { name: 'otherBucket', default: false, editorComponent: OtherBucketParamEditor, - write: _.noop, + write: () => {}, }, { name: 'otherBucketLabel', @@ -283,13 +236,13 @@ export const termsBucketAgg = new BucketAggType({ defaultMessage: 'Label for other bucket', }), shouldShow: agg => agg.params.otherBucket, - write: _.noop, + write: () => {}, }, { name: 'missingBucket', default: false, editorComponent: MissingBucketParamEditor, - write: _.noop, + write: () => {}, }, { name: 'missingBucketLabel', @@ -303,7 +256,7 @@ export const termsBucketAgg = new BucketAggType({ defaultMessage: 'Label for missing values', }), shouldShow: agg => agg.params.missingBucket, - write: _.noop, + write: () => {}, }, { name: 'exclude', diff --git a/src/legacy/ui/public/agg_types/controls/auto_precision.tsx b/src/legacy/ui/public/agg_types/controls/auto_precision.tsx new file mode 100644 index 0000000000000..00c006a5f8548 --- /dev/null +++ b/src/legacy/ui/public/agg_types/controls/auto_precision.tsx @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { EuiSwitch, EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { AggParamEditorProps } from 'ui/vis/editors/default'; + +function AutoPrecisionParamEditor({ value, setValue }: AggParamEditorProps) { + const label = i18n.translate('common.ui.aggTypes.changePrecisionLabel', { + defaultMessage: 'Change precision on map zoom', + }); + + return ( + + setValue(ev.target.checked)} /> + + ); +} + +export { AutoPrecisionParamEditor }; diff --git a/src/legacy/ui/public/agg_types/controls/components/from_to_list.tsx b/src/legacy/ui/public/agg_types/controls/components/from_to_list.tsx new file mode 100644 index 0000000000000..99d689199e24c --- /dev/null +++ b/src/legacy/ui/public/agg_types/controls/components/from_to_list.tsx @@ -0,0 +1,124 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiFieldText, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import Ipv4Address from '../../../utils/ipv4_address'; +import { InputList, InputListConfig, InputModel, InputObject, InputItem } from './input_list'; + +const EMPTY_STRING = ''; + +export interface FromToObject extends InputObject { + from?: string; + to?: string; +} + +type FromToModel = InputModel & { + from: InputItem; + to: InputItem; +}; + +interface FromToListProps { + list: FromToObject[]; + showValidation: boolean; + onBlur(): void; + onChange(list: FromToObject[]): void; + setValidity(isValid: boolean): void; +} + +function FromToList({ showValidation, onBlur, ...rest }: FromToListProps) { + const fromToListConfig: InputListConfig = { + defaultValue: { + from: { value: '0.0.0.0', model: '0.0.0.0', isInvalid: false }, + to: { value: '255.255.255.255', model: '255.255.255.255', isInvalid: false }, + }, + defaultEmptyValue: { + from: { value: EMPTY_STRING, model: EMPTY_STRING, isInvalid: false }, + to: { value: EMPTY_STRING, model: EMPTY_STRING, isInvalid: false }, + }, + validateClass: Ipv4Address, + getModelValue: (item: FromToObject) => ({ + from: { + value: item.from || EMPTY_STRING, + model: item.from || EMPTY_STRING, + isInvalid: false, + }, + to: { value: item.to || EMPTY_STRING, model: item.to || EMPTY_STRING, isInvalid: false }, + }), + getRemoveBtnAriaLabel: (item: FromToModel) => + i18n.translate('common.ui.aggTypes.ipRanges.removeRangeAriaLabel', { + defaultMessage: 'Remove the range of {from} to {to}', + values: { from: item.from.value || '*', to: item.to.value || '*' }, + }), + onChangeFn: ({ from, to }: FromToModel) => { + const result: FromToObject = {}; + if (from.model) { + result.from = from.model; + } + if (to.model) { + result.to = to.model; + } + return result; + }, + hasInvalidValuesFn: ({ from, to }: FromToModel) => from.isInvalid || to.isInvalid, + renderInputRow: (item: FromToModel, index, onChangeValue) => ( + <> + + { + onChangeValue(index, ev.target.value, 'from'); + }} + value={item.from.value} + onBlur={onBlur} + /> + + + { + onChangeValue(index, ev.target.value, 'to'); + }} + value={item.to.value} + onBlur={onBlur} + /> + + + ), + validateModel: (validateFn, object: FromToObject, model: FromToModel) => { + validateFn(object.from, model.from); + validateFn(object.to, model.to); + }, + }; + + return ; +} + +export { FromToList }; diff --git a/src/legacy/ui/public/agg_types/controls/components/input_list.tsx b/src/legacy/ui/public/agg_types/controls/components/input_list.tsx new file mode 100644 index 0000000000000..68960ca396b3d --- /dev/null +++ b/src/legacy/ui/public/agg_types/controls/components/input_list.tsx @@ -0,0 +1,231 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useState, useEffect, Fragment } from 'react'; +import { + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + htmlIdGenerator, + EuiButtonEmpty, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export interface InputListConfig { + defaultValue: InputItemModel; + defaultEmptyValue: InputItemModel; + validateClass: new (value: string) => { toString(): string }; + getModelValue(item: InputObject): InputItemModel; + getRemoveBtnAriaLabel(model: InputModel): string; + onChangeFn(model: InputModel): InputObject; + hasInvalidValuesFn(model: InputModel): boolean; + renderInputRow( + model: InputModel, + index: number, + onChangeFn: (index: number, value: string, modelName: string) => void + ): React.ReactNode; + validateModel( + validateFn: (value: string | undefined, modelObj: InputItem) => void, + object: InputObject, + model: InputModel + ): void; +} +interface InputModelBase { + id: string; +} +export type InputObject = object; +export interface InputItem { + model: string; + value: string; + isInvalid: boolean; +} + +interface InputItemModel { + [model: string]: InputItem; +} + +// InputModel can have the following implementations: +// for Mask List - { id: 'someId', mask: { model: '', value: '', isInvalid: false }} +// for FromTo List - { id: 'someId', from: { model: '', value: '', isInvalid: false }, to: { model: '', value: '', isInvalid: false }} +export type InputModel = InputModelBase & InputItemModel; + +interface InputListProps { + config: InputListConfig; + list: InputObject[]; + onChange(list: InputObject[]): void; + setValidity(isValid: boolean): void; +} + +const generateId = htmlIdGenerator(); + +function InputList({ config, list, onChange, setValidity }: InputListProps) { + const [models, setModels] = useState( + list.length + ? list.map( + item => + ({ + id: generateId(), + ...config.getModelValue(item), + } as InputModel) + ) + : [ + { + id: generateId(), + ...config.defaultValue, + } as InputModel, + ] + ); + + const onUpdate = (modelList: InputModel[]) => { + setModels(modelList); + onChange(modelList.map(config.onChangeFn)); + }; + + const onChangeValue = (index: number, value: string, modelName: string) => { + const range = models[index][modelName]; + const { model, isInvalid } = validateValue(value); + range.value = value; + range.model = model; + range.isInvalid = isInvalid; + onUpdate(models); + }; + const onDelete = (id: string) => { + const newArray = models.filter(model => model.id !== id); + onUpdate(newArray); + }; + + const onAdd = () => { + const newArray = [ + ...models, + { + id: generateId(), + ...config.defaultEmptyValue, + } as InputModel, + ]; + onUpdate(newArray); + }; + + const getUpdatedModels = (objList: InputObject[], modelList: InputModel[]) => { + if (!objList.length) { + return modelList; + } + return objList.map((item, index) => { + const model = modelList[index] || { + id: generateId(), + ...config.getModelValue(item), + }; + + config.validateModel(validateItem, item, model); + + return model; + }); + }; + + const validateItem = (value: string | undefined, modelObj: InputItem) => { + const { model, isInvalid } = validateValue(value); + if (value !== modelObj.model) { + modelObj.value = model; + } + modelObj.model = model; + modelObj.isInvalid = isInvalid; + }; + + const validateValue = (inputValue: string | undefined) => { + const result = { + model: inputValue || '', + isInvalid: false, + }; + if (!inputValue) { + result.isInvalid = false; + return result; + } + try { + const InputObject = config.validateClass; + result.model = new InputObject(inputValue).toString(); + result.isInvalid = false; + return result; + } catch (e) { + result.isInvalid = true; + return result; + } + }; + + const hasInvalidValues = (modelList: InputModel[]) => { + return !!modelList.find(config.hasInvalidValuesFn); + }; + + // responsible for discarding changes + useEffect( + () => { + setModels(getUpdatedModels(list, models)); + }, + [list] + ); + + useEffect( + () => { + setValidity(!hasInvalidValues(models)); + }, + [models] + ); + + // resposible for setting up an initial value when there is no default value + useEffect(() => { + onChange(models.map(config.onChangeFn)); + }, []); + + if (!list || !list.length) { + return null; + } + + return ( + <> + {models.map((item, index) => ( + + + {config.renderInputRow(item, index, onChangeValue)} + + onDelete(item.id)} + /> + + + + + ))} + + + + + + + + ); +} + +export { InputList }; diff --git a/src/legacy/ui/public/agg_types/controls/components/mask_list.tsx b/src/legacy/ui/public/agg_types/controls/components/mask_list.tsx new file mode 100644 index 0000000000000..efb5ca7643c93 --- /dev/null +++ b/src/legacy/ui/public/agg_types/controls/components/mask_list.tsx @@ -0,0 +1,101 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiFieldText, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { CidrMask } from '../../../utils/cidr_mask'; +import { InputList, InputListConfig, InputObject, InputModel, InputItem } from './input_list'; + +const EMPTY_STRING = ''; + +export interface MaskObject extends InputObject { + mask?: string; +} + +type MaskModel = InputModel & { + mask: InputItem; +}; + +interface MaskListProps { + list: MaskObject[]; + showValidation: boolean; + onBlur(): void; + onChange(list: MaskObject[]): void; + setValidity(isValid: boolean): void; +} + +function MaskList({ showValidation, onBlur, ...rest }: MaskListProps) { + const maskListConfig: InputListConfig = { + defaultValue: { + mask: { model: '0.0.0.0/1', value: '0.0.0.0/1', isInvalid: false }, + }, + defaultEmptyValue: { + mask: { model: EMPTY_STRING, value: EMPTY_STRING, isInvalid: false }, + }, + validateClass: CidrMask, + getModelValue: (item: MaskObject) => ({ + mask: { + model: item.mask || EMPTY_STRING, + value: item.mask || EMPTY_STRING, + isInvalid: false, + }, + }), + getRemoveBtnAriaLabel: (item: MaskModel) => + item.mask.value + ? i18n.translate('common.ui.aggTypes.ipRanges.removeCidrMaskButtonAriaLabel', { + defaultMessage: 'Remove the CIDR mask value of {mask}', + values: { mask: item.mask.value }, + }) + : i18n.translate('common.ui.aggTypes.ipRanges.removeEmptyCidrMaskButtonAriaLabel', { + defaultMessage: 'Remove the CIDR mask default value', + }), + onChangeFn: ({ mask }: MaskModel) => { + if (mask.model) { + return { mask: mask.model }; + } + return {}; + }, + hasInvalidValuesFn: ({ mask }) => mask.isInvalid, + renderInputRow: ({ mask }: MaskModel, index, onChangeValue) => ( + + { + onChangeValue(index, ev.target.value, 'mask'); + }} + value={mask.value} + onBlur={onBlur} + /> + + ), + validateModel: (validateFn, object: MaskObject, model: MaskModel) => { + validateFn(object.mask, model.mask); + }, + }; + + return ; +} + +export { MaskList }; diff --git a/src/legacy/ui/public/agg_types/controls/drop_partials.tsx b/src/legacy/ui/public/agg_types/controls/drop_partials.tsx index ee9961476dbde..8b104dc9734b1 100644 --- a/src/legacy/ui/public/agg_types/controls/drop_partials.tsx +++ b/src/legacy/ui/public/agg_types/controls/drop_partials.tsx @@ -18,32 +18,23 @@ */ import React from 'react'; -import { EuiSpacer, EuiSwitch, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { AggParamEditorProps } from 'ui/vis/editors/default'; +import { SwitchParamEditor } from './switch'; -function DropPartialsParamEditor({ agg, aggParam, value, setValue }: AggParamEditorProps) { - const content = i18n.translate('common.ui.aggTypes.dropPartialBucketsTooltip', { - defaultMessage: - "Remove buckets that span time outside the time range so the histogram doesn't start and end with incomplete buckets.", - }); - - const label = i18n.translate('common.ui.aggTypes.dropPartialBucketsLabel', { - defaultMessage: 'Drop partial buckets', - }); - +function DropPartialsParamEditor(props: AggParamEditorProps) { return ( - <> - - setValue(ev.target.checked)} - /> - - - + ); } diff --git a/src/legacy/ui/public/agg_types/controls/field.tsx b/src/legacy/ui/public/agg_types/controls/field.tsx index 8b6749054e92d..0fb44cc9f920b 100644 --- a/src/legacy/ui/public/agg_types/controls/field.tsx +++ b/src/legacy/ui/public/agg_types/controls/field.tsx @@ -30,16 +30,23 @@ import { FieldParamType } from '../param_types'; const label = i18n.translate('common.ui.aggTypes.field.fieldLabel', { defaultMessage: 'Field' }); +interface FieldParamEditorProps extends AggParamEditorProps { + customError?: string; + customLabel?: string; +} + function FieldParamEditor({ agg, aggParam, + customError, + customLabel, indexedFields = [], showValidation, value, setTouched, setValidity, setValue, -}: AggParamEditorProps) { +}: FieldParamEditorProps) { const selectedOptions: ComboBoxGroupedOption[] = value ? [{ label: value.displayName, value }] : []; @@ -56,6 +63,10 @@ function FieldParamEditor({ }; const errors = []; + if (customError) { + errors.push(customError); + } + if (!indexedFields.length) { errors.push( i18n.translate('common.ui.aggTypes.field.noCompatibleFieldsDescription', { @@ -70,7 +81,7 @@ function FieldParamEditor({ setTouched(); } - const isValid = !!value && !!indexedFields.length; + const isValid = !!value && !errors.length; useEffect( () => { @@ -79,9 +90,24 @@ function FieldParamEditor({ [isValid] ); + useEffect(() => { + // set field if only one available + if (indexedFields.length !== 1) { + return; + } + + const options = indexedFields[0].options; + + if (!options) { + setValue(indexedFields[0].value); + } else if (options.length === 1) { + setValue(options[0].value); + } + }, []); + return ( - onChangeValue(id, query, customLabel)} + disableAutoFocus={!autoFocus} data-test-subj={dataTestSubj} - onChange={ev => onChangeValue(id, ev.target.value, customLabel)} - fullWidth={true} - autoFocus={autoFocus} + bubbleSubmitEvent={true} + languageSwitcherPopoverAnchorPosition="leftDown" /> {showCustomLabel ? ( diff --git a/src/legacy/ui/public/agg_types/controls/filters.tsx b/src/legacy/ui/public/agg_types/controls/filters.tsx index 1d393b996b4ea..5c8d38e5a4e8a 100644 --- a/src/legacy/ui/public/agg_types/controls/filters.tsx +++ b/src/legacy/ui/public/agg_types/controls/filters.tsx @@ -22,14 +22,15 @@ import { omit, isEqual } from 'lodash'; import { htmlIdGenerator, EuiButton, EuiSpacer } from '@elastic/eui'; import { AggParamEditorProps } from 'ui/vis/editors/default'; import { FormattedMessage } from '@kbn/i18n/react'; -import { data } from 'plugins/data'; +import chrome from 'ui/chrome'; +import { Query } from 'plugins/data'; import { FilterRow } from './filter'; -const { toUser, fromUser } = data.query.helpers; const generateId = htmlIdGenerator(); +const config = chrome.getUiSettingsClient(); interface FilterValue { - input: any; + input: Query; label: string; id: string; } @@ -41,11 +42,7 @@ function FiltersParamEditor({ agg, value, setValue }: AggParamEditorProps { // set parsed values into model after initialization - setValue( - filters.map(filter => - omit({ ...filter, input: { query: fromUser(filter.input.query) } }, 'id') - ) - ); + setValue(filters.map(filter => omit({ ...filter, input: filter.input }, 'id'))); }, []); useEffect( @@ -68,15 +65,22 @@ function FiltersParamEditor({ agg, value, setValue }: AggParamEditorProps - updateFilters([...filters, { input: { query: '' }, label: '', id: generateId() }]); + updateFilters([ + ...filters, + { + input: { query: '', language: config.get('search:queryLanguage') }, + label: '', + id: generateId(), + }, + ]); const onRemoveFilter = (id: string) => updateFilters(filters.filter(filter => filter.id !== id)); - const onChangeValue = (id: string, query: string, label: string) => + const onChangeValue = (id: string, query: Query, label: string) => updateFilters( filters.map(filter => filter.id === id ? { ...filter, - input: { query: fromUser(query) }, + input: query, label, } : filter @@ -91,10 +95,11 @@ function FiltersParamEditor({ agg, value, setValue }: AggParamEditorProps diff --git a/src/legacy/ui/public/agg_types/controls/ip_range_type.tsx b/src/legacy/ui/public/agg_types/controls/ip_range_type.tsx new file mode 100644 index 0000000000000..24964785b72df --- /dev/null +++ b/src/legacy/ui/public/agg_types/controls/ip_range_type.tsx @@ -0,0 +1,67 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { EuiButtonGroup, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { AggParamEditorProps } from '../../vis/editors/default'; + +enum IpRangeTypes { + MASK = 'mask', + FROM_TO = 'fromTo', +} + +function IpRangeTypeParamEditor({ agg, value, setValue }: AggParamEditorProps) { + const options = [ + { + id: `visEditorIpRangeFromToLabel${agg.id}`, + label: i18n.translate('common.ui.aggTypes.ipRanges.fromToButtonLabel', { + defaultMessage: 'From/to', + }), + }, + { + id: `visEditorIpRangeCidrLabel${agg.id}`, + label: i18n.translate('common.ui.aggTypes.ipRanges.cidrMasksButtonLabel', { + defaultMessage: 'CIDR masks', + }), + }, + ]; + + const onClick = (optionId: string) => { + setValue(optionId === options[0].id ? IpRangeTypes.FROM_TO : IpRangeTypes.MASK); + }; + + return ( + <> + + + + ); +} + +export { IpRangeTypeParamEditor, IpRangeTypes }; diff --git a/src/legacy/ui/public/agg_types/controls/ip_ranges.html b/src/legacy/ui/public/agg_types/controls/ip_ranges.html deleted file mode 100644 index 5450fae62cb19..0000000000000 --- a/src/legacy/ui/public/agg_types/controls/ip_ranges.html +++ /dev/null @@ -1,154 +0,0 @@ -
- -

- - -

- -
- -
- - - - - - - - - - - -
- - - -
- - - - - -
- - -
-

- - - -

-
- - -
- -
- - - - - - - - - -
- -
- - - -
- - -
-

- - - -

-
- - -
-
diff --git a/src/legacy/ui/public/agg_types/controls/ip_ranges.tsx b/src/legacy/ui/public/agg_types/controls/ip_ranges.tsx new file mode 100644 index 0000000000000..9a1b804432b76 --- /dev/null +++ b/src/legacy/ui/public/agg_types/controls/ip_ranges.tsx @@ -0,0 +1,70 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiFormRow } from '@elastic/eui'; + +import { AggParamEditorProps } from 'ui/vis/editors/default'; +import { FromToList, FromToObject } from './components/from_to_list'; +import { MaskList, MaskObject } from './components/mask_list'; +import { IpRangeTypes } from './ip_range_type'; +interface IpRange { + fromTo: FromToObject[]; + mask: MaskObject[]; +} + +function IpRangesParamEditor({ + agg, + value = { fromTo: [] as FromToObject[], mask: [] as MaskObject[] }, + setTouched, + setValue, + setValidity, + showValidation, +}: AggParamEditorProps) { + const handleChange = (modelName: IpRangeTypes, items: Array) => { + setValue({ + ...value, + [modelName]: items, + }); + }; + + return ( + + {agg.params.ipRangeType === IpRangeTypes.MASK ? ( + handleChange(IpRangeTypes.MASK, items)} + setValidity={setValidity} + /> + ) : ( + handleChange(IpRangeTypes.FROM_TO, items)} + setValidity={setValidity} + /> + )} + + ); +} + +export { IpRangesParamEditor }; diff --git a/src/legacy/ui/public/agg_types/controls/is_filtered_by_collar.tsx b/src/legacy/ui/public/agg_types/controls/is_filtered_by_collar.tsx new file mode 100644 index 0000000000000..f3197d85c377e --- /dev/null +++ b/src/legacy/ui/public/agg_types/controls/is_filtered_by_collar.tsx @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { AggParamEditorProps } from 'ui/vis/editors/default'; +import { SwitchParamEditor } from './switch'; + +function IsFilteredByCollarParamEditor(props: AggParamEditorProps) { + return ( + + ); +} + +export { IsFilteredByCollarParamEditor }; diff --git a/src/legacy/ui/public/agg_types/controls/min_doc_count.tsx b/src/legacy/ui/public/agg_types/controls/min_doc_count.tsx index 7948d5f34e811..590ab0f6c3b7d 100644 --- a/src/legacy/ui/public/agg_types/controls/min_doc_count.tsx +++ b/src/legacy/ui/public/agg_types/controls/min_doc_count.tsx @@ -19,26 +19,21 @@ import React from 'react'; -import { EuiSpacer, EuiSwitch, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { AggParamEditorProps } from 'ui/vis/editors/default'; +import { SwitchParamEditor } from './switch'; -function MinDocCountParamEditor({ value, setValue }: AggParamEditorProps) { - const label = i18n.translate('common.ui.aggTypes.showEmptyBucketsLabel', { - defaultMessage: 'Show empty buckets', - }); - - const content = i18n.translate('common.ui.aggTypes.showEmptyBucketsTooltip', { - defaultMessage: 'Show all buckets, not only the buckets with results', - }); - +function MinDocCountParamEditor(props: AggParamEditorProps) { return ( -
- - setValue(ev.target.checked)} /> - - -
+ ); } diff --git a/src/legacy/ui/public/agg_types/controls/missing_bucket.tsx b/src/legacy/ui/public/agg_types/controls/missing_bucket.tsx index 88a27105d2d4e..eee6b8f169634 100644 --- a/src/legacy/ui/public/agg_types/controls/missing_bucket.tsx +++ b/src/legacy/ui/public/agg_types/controls/missing_bucket.tsx @@ -17,13 +17,24 @@ * under the License. */ -import React from 'react'; +import React, { useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { AggParamEditorProps } from 'ui/vis/editors/default'; import { SwitchParamEditor } from './switch'; import { isStringType } from '../buckets/migrate_include_exclude_format'; function MissingBucketParamEditor(props: AggParamEditorProps) { + const fieldTypeIsNotString = !isStringType(props.agg); + + useEffect( + () => { + if (fieldTypeIsNotString) { + props.setValue(false); + } + }, + [fieldTypeIsNotString] + ); + return ( ) { 'If not in the top N, and you enable "Group other values in separate bucket", ' + 'Elasticsearch adds the missing values to the "other" bucket.', })} - disabled={!isStringType(props.agg)} + disabled={fieldTypeIsNotString} {...props} /> ); diff --git a/src/legacy/ui/public/agg_types/controls/order.tsx b/src/legacy/ui/public/agg_types/controls/order.tsx index 0042c3d97d0ba..a122ad4c0543d 100644 --- a/src/legacy/ui/public/agg_types/controls/order.tsx +++ b/src/legacy/ui/public/agg_types/controls/order.tsx @@ -30,6 +30,7 @@ function OrderParamEditor({ setValue, setValidity, setTouched, + wrappedWithInlineComp, }: AggParamEditorProps & SelectParamEditorProps) { const label = i18n.translate('common.ui.aggTypes.orderLabel', { defaultMessage: 'Order', @@ -48,7 +49,7 @@ function OrderParamEditor({ label={label} fullWidth={true} isInvalid={showValidation ? !isValid : false} - className="visEditorSidebar__aggParamFormRow" + className={wrappedWithInlineComp ? undefined : 'visEditorSidebar__aggParamFormRow'} > -
- - -
-
- - -
+
+ +
diff --git a/src/legacy/ui/public/agg_types/controls/order_agg.test.tsx b/src/legacy/ui/public/agg_types/controls/order_agg.test.tsx new file mode 100644 index 0000000000000..b8ea98847762c --- /dev/null +++ b/src/legacy/ui/public/agg_types/controls/order_agg.test.tsx @@ -0,0 +1,167 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { mount } from 'enzyme'; +import { OrderAggParamEditor } from './order_agg'; + +describe('OrderAggParamEditor component', () => { + let setValue: jest.Mock; + let setValidity: jest.Mock; + let setTouched: jest.Mock; + let defaultProps: any; + + beforeEach(() => { + setValue = jest.fn(); + setValidity = jest.fn(); + setTouched = jest.fn(); + + defaultProps = { + agg: {}, + aggParam: { + name: 'orderAgg', + type: '', + }, + editorConfig: {}, + value: '', + showValidation: false, + setValue, + setValidity, + setTouched, + }; + }); + + it('defaults to the first metric agg after init', () => { + const responseValueAggs = [ + { + id: 'agg1', + type: { + name: 'count', + }, + }, + { + id: 'agg2', + type: { + name: 'count', + }, + }, + ]; + const props = { ...defaultProps, responseValueAggs }; + + mount(); + + expect(setValue).toHaveBeenCalledWith('agg1'); + }); + + it('defaults to the first metric agg that is compatible with the terms bucket', () => { + const responseValueAggs = [ + { + id: 'agg1', + type: { + name: 'top_hits', + }, + }, + { + id: 'agg2', + type: { + name: 'percentiles', + }, + }, + { + id: 'agg3', + type: { + name: 'median', + }, + }, + { + id: 'agg4', + type: { + name: 'std_dev', + }, + }, + { + id: 'agg5', + type: { + name: 'count', + }, + }, + ]; + const props = { ...defaultProps, responseValueAggs }; + + mount(); + + expect(setValue).toHaveBeenCalledWith('agg5'); + }); + + it('defaults to the _key metric if no agg is compatible', () => { + const responseValueAggs = [ + { + id: 'agg1', + type: { + name: 'top_hits', + }, + }, + ]; + const props = { ...defaultProps, responseValueAggs }; + + mount(); + + expect(setValue).toHaveBeenCalledWith('_key'); + }); + + it('selects first metric if it is avg', () => { + const responseValueAggs = [ + { + id: 'agg1', + type: { + name: 'avg', + field: 'bytes', + }, + }, + ]; + const props = { ...defaultProps, responseValueAggs }; + + mount(); + + expect(setValue).toHaveBeenCalledWith('agg1'); + }); + + it('selects _key if the first metric is avg_bucket', () => { + const responseValueAggs = [ + { + id: 'agg1', + type: { + name: 'avg_bucket', + metric: 'custom', + }, + }, + ]; + const props = { ...defaultProps, responseValueAggs }; + + mount(); + + expect(setValue).toHaveBeenCalledWith('_key'); + }); + + it('selects _key if there are no metric aggs', () => { + mount(); + + expect(setValue).toHaveBeenCalledWith('_key'); + }); +}); diff --git a/src/legacy/ui/public/agg_types/controls/order_agg.tsx b/src/legacy/ui/public/agg_types/controls/order_agg.tsx new file mode 100644 index 0000000000000..569fb4602e9f1 --- /dev/null +++ b/src/legacy/ui/public/agg_types/controls/order_agg.tsx @@ -0,0 +1,119 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useEffect } from 'react'; +import { EuiFormRow, EuiSelect } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { AggParamEditorProps } from 'ui/vis/editors/default'; +import { safeMakeLabel, isCompatibleAgg } from '../buckets/_terms_helper'; + +function OrderAggParamEditor({ + agg, + value, + showValidation, + setValue, + setValidity, + setTouched, + responseValueAggs, +}: AggParamEditorProps) { + const label = i18n.translate('common.ui.aggTypes.orderAgg.orderByLabel', { + defaultMessage: 'Order by', + }); + const isValid = !!value; + + useEffect( + () => { + setValidity(isValid); + }, + [isValid] + ); + + useEffect(() => { + // setup the initial value of orderBy + if (!value) { + let respAgg = { id: '_key' }; + + if (responseValueAggs) { + respAgg = responseValueAggs.filter(isCompatibleAgg)[0] || respAgg; + } + + setValue(respAgg.id); + } + }, []); + + useEffect( + () => { + if (responseValueAggs && value && value !== 'custom') { + // ensure that orderBy is set to a valid agg + const respAgg = responseValueAggs + .filter(isCompatibleAgg) + .find(aggregation => aggregation.id === value); + + if (!respAgg) { + setValue('_key'); + } + } + }, + [responseValueAggs] + ); + + const defaultOptions = [ + { + text: i18n.translate('common.ui.aggTypes.orderAgg.customMetricLabel', { + defaultMessage: 'Custom metric', + }), + value: 'custom', + }, + { + text: i18n.translate('common.ui.aggTypes.orderAgg.alphabeticalLabel', { + defaultMessage: 'Alphabetical', + }), + value: '_key', + }, + ]; + + const options = responseValueAggs + ? responseValueAggs.map(respAgg => ({ + text: i18n.translate('common.ui.aggTypes.orderAgg.metricLabel', { + defaultMessage: 'Metric: {metric}', + values: { + metric: safeMakeLabel(respAgg), + }, + }), + value: respAgg.id, + disabled: !isCompatibleAgg(respAgg), + })) + : []; + + return ( + + setValue(ev.target.value)} + fullWidth={true} + isInvalid={showValidation ? !isValid : false} + onBlur={setTouched} + data-test-subj={`visEditorOrderBy${agg.id}`} + /> + + ); +} + +export { OrderAggParamEditor }; diff --git a/src/legacy/ui/public/agg_types/controls/percentile_ranks.html b/src/legacy/ui/public/agg_types/controls/percentile_ranks.html deleted file mode 100644 index 72192ac6375d1..0000000000000 --- a/src/legacy/ui/public/agg_types/controls/percentile_ranks.html +++ /dev/null @@ -1,14 +0,0 @@ -
- - - -
diff --git a/src/legacy/ui/public/agg_types/controls/percentile_ranks.tsx b/src/legacy/ui/public/agg_types/controls/percentile_ranks.tsx new file mode 100644 index 0000000000000..78dce5c1d26e5 --- /dev/null +++ b/src/legacy/ui/public/agg_types/controls/percentile_ranks.tsx @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useState } from 'react'; + +import { EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { AggParamEditorProps } from '../../vis/editors/default'; +import { NumberList } from '../number_list'; + +function PercentileRanksEditor({ + agg, + showValidation, + value, + setTouched, + setValidity, + setValue, +}: AggParamEditorProps>) { + const label = i18n.translate('common.ui.aggTypes.percentileRanks.valuesLabel', { + defaultMessage: 'Values', + }); + const [isValid, setIsValid] = useState(true); + + const setModelValidy = (isListValid: boolean) => { + setIsValid(isListValid); + setValidity(isListValid); + }; + + return ( + + + + ); +} + +export { PercentileRanksEditor }; diff --git a/src/legacy/ui/public/agg_types/controls/percentiles.html b/src/legacy/ui/public/agg_types/controls/percentiles.html deleted file mode 100644 index 3a7afed6ac300..0000000000000 --- a/src/legacy/ui/public/agg_types/controls/percentiles.html +++ /dev/null @@ -1,15 +0,0 @@ -
- - - -
diff --git a/src/legacy/ui/public/agg_types/controls/percentiles.tsx b/src/legacy/ui/public/agg_types/controls/percentiles.tsx new file mode 100644 index 0000000000000..981a9b5b8cbb0 --- /dev/null +++ b/src/legacy/ui/public/agg_types/controls/percentiles.tsx @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useState } from 'react'; + +import { EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { AggParamEditorProps } from '../../vis/editors/default'; +import { NumberList } from '../number_list'; + +function PercentilesEditor({ + agg, + showValidation, + value, + setTouched, + setValidity, + setValue, +}: AggParamEditorProps>) { + const label = i18n.translate('common.ui.aggTypes.percentiles.percentsLabel', { + defaultMessage: 'Percents', + }); + const [isValid, setIsValid] = useState(true); + + const setModelValidy = (isListValid: boolean) => { + setIsValid(isListValid); + setValidity(isListValid); + }; + + return ( + + + + ); +} + +export { PercentilesEditor }; diff --git a/src/legacy/ui/public/agg_types/controls/precision.html b/src/legacy/ui/public/agg_types/controls/precision.html deleted file mode 100644 index 5857c04555678..0000000000000 --- a/src/legacy/ui/public/agg_types/controls/precision.html +++ /dev/null @@ -1,71 +0,0 @@ -
- -
- -
-
- -
- -
- {{agg.params.precision}} -
-
-
-
- -
- -
- -
- -
diff --git a/src/legacy/ui/public/agg_types/controls/precision.tsx b/src/legacy/ui/public/agg_types/controls/precision.tsx new file mode 100644 index 0000000000000..7612619edb916 --- /dev/null +++ b/src/legacy/ui/public/agg_types/controls/precision.tsx @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { EuiRange, EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { AggParamEditorProps } from 'ui/vis/editors/default'; + +function PrecisionParamEditor({ agg, config, value, setValue }: AggParamEditorProps) { + if (agg.params.autoPrecision) { + return null; + } + + const label = i18n.translate('common.ui.aggTypes.precisionLabel', { + defaultMessage: 'Precision', + }); + + return ( + + setValue(Number(ev.target.value))} + data-test-subj={`visEditorMapPrecision${agg.id}`} + showValue + /> + + ); +} + +export { PrecisionParamEditor }; diff --git a/src/legacy/ui/public/agg_types/controls/size.tsx b/src/legacy/ui/public/agg_types/controls/size.tsx index 14f071cfd5971..9357fa62f2ff2 100644 --- a/src/legacy/ui/public/agg_types/controls/size.tsx +++ b/src/legacy/ui/public/agg_types/controls/size.tsx @@ -21,19 +21,30 @@ import React, { useEffect } from 'react'; import { isUndefined } from 'lodash'; import { AggParamEditorProps } from 'ui/vis/editors/default'; import { EuiFormRow, EuiFieldNumber } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +interface SizeParamEditorProps extends AggParamEditorProps { + iconTip?: React.ReactNode; + disabled?: boolean; +} function SizeParamEditor({ + disabled, + iconTip, value, setValue, showValidation, setValidity, setTouched, -}: AggParamEditorProps) { - const label = i18n.translate('common.ui.aggTypes.sizeLabel', { - defaultMessage: 'Size', - }); - const isValid = Number(value) > 0; + wrappedWithInlineComp, +}: SizeParamEditorProps) { + const label = ( + <> + + {iconTip} + + ); + const isValid = disabled || Number(value) > 0; useEffect( () => { @@ -47,7 +58,7 @@ function SizeParamEditor({ label={label} fullWidth={true} isInvalid={showValidation ? !isValid : false} - className="visEditorSidebar__aggParamFormRow" + className={wrappedWithInlineComp ? undefined : 'visEditorSidebar__aggParamFormRow'} > diff --git a/src/legacy/ui/public/agg_types/controls/top_aggregate.tsx b/src/legacy/ui/public/agg_types/controls/top_aggregate.tsx new file mode 100644 index 0000000000000..6416ab96ffac1 --- /dev/null +++ b/src/legacy/ui/public/agg_types/controls/top_aggregate.tsx @@ -0,0 +1,138 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useEffect, useRef } from 'react'; +import { EuiFormRow, EuiIconTip, EuiSelect } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { AggParamEditorProps } from 'ui/vis/editors/default'; +import { AggConfig } from 'ui/vis'; +import { AggParam } from '../agg_param'; +import { SelectValueProp, SelectParamEditorProps } from '../param_types/select'; + +interface AggregateValueProp extends SelectValueProp { + isCompatibleType(filedType: string): boolean; + isCompatibleVis(visName: string): boolean; +} + +function getCompatibleAggs(agg: AggConfig, visName: string): AggregateValueProp[] { + const fieldType = agg.params.field && agg.params.field.type; + const { options = [] } = agg.getAggParams().find(({ name }: AggParam) => name === 'aggregate'); + return options.filter( + (option: AggregateValueProp) => + fieldType && option.isCompatibleType(fieldType) && option.isCompatibleVis(visName) + ); +} + +function TopAggregateParamEditor({ + agg, + aggParam, + value, + visName, + showValidation, + setValue, + setValidity, + setTouched, + wrappedWithInlineComp, +}: AggParamEditorProps & SelectParamEditorProps) { + const isFirstRun = useRef(true); + const fieldType = agg.params.field && agg.params.field.type; + const emptyValue = { text: '', value: 'EMPTY_VALUE', disabled: true, hidden: true }; + const filteredOptions = getCompatibleAggs(agg, visName) + .map(({ text, value: val }) => ({ text, value: val })) + .sort((a, b) => a.text.toLowerCase().localeCompare(b.text.toLowerCase())); + const options = [emptyValue, ...filteredOptions]; + const disabled = fieldType && !filteredOptions.length; + const isValid = disabled || !!value; + + const label = ( + <> + {' '} + + + ); + + useEffect( + () => { + setValidity(isValid); + }, + [isValid] + ); + + useEffect( + () => { + if (isFirstRun.current) { + isFirstRun.current = false; + return; + } + + if (value) { + if (aggParam.options.byValue[value.value]) { + return; + } + + setValue(); + } + + if (filteredOptions.length === 1) { + setValue(aggParam.options.byValue[filteredOptions[0].value]); + } + }, + [fieldType, visName] + ); + + const handleChange = (event: React.ChangeEvent) => { + if (event.target.value === emptyValue.value) { + setValue(); + } else { + setValue(aggParam.options.byValue[event.target.value]); + } + }; + + return ( + + + + ); +} + +export { TopAggregateParamEditor, getCompatibleAggs }; diff --git a/src/legacy/ui/public/agg_types/controls/top_aggregate_and_size.html b/src/legacy/ui/public/agg_types/controls/top_aggregate_and_size.html deleted file mode 100644 index c96f7570081e1..0000000000000 --- a/src/legacy/ui/public/agg_types/controls/top_aggregate_and_size.html +++ /dev/null @@ -1,43 +0,0 @@ -
-
- - - -
-
- - - -
-
diff --git a/src/legacy/ui/public/agg_types/controls/top_field.tsx b/src/legacy/ui/public/agg_types/controls/top_field.tsx new file mode 100644 index 0000000000000..9069f081ccf99 --- /dev/null +++ b/src/legacy/ui/public/agg_types/controls/top_field.tsx @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { AggParamEditorProps } from '../../vis/editors/default'; +import { FieldParamType } from '../param_types'; +import { FieldParamEditor } from './field'; +import { getCompatibleAggs } from './top_aggregate'; + +function TopFieldParamEditor(props: AggParamEditorProps) { + const compatibleAggs = getCompatibleAggs(props.agg, props.visName); + let customError; + + if (!compatibleAggs.length) { + customError = i18n.translate('common.ui.aggTypes.aggregateWith.noAggsErrorTooltip', { + defaultMessage: 'The chosen field has no compatible aggregations.', + }); + } + + return ; +} + +export { TopFieldParamEditor }; diff --git a/src/legacy/ui/public/agg_types/controls/top_size.tsx b/src/legacy/ui/public/agg_types/controls/top_size.tsx new file mode 100644 index 0000000000000..513b4681ecc74 --- /dev/null +++ b/src/legacy/ui/public/agg_types/controls/top_size.tsx @@ -0,0 +1,47 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { AggParamEditorProps } from 'ui/vis/editors/default'; +import { EuiIconTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { SizeParamEditor } from './size'; +import { getCompatibleAggs } from './top_aggregate'; + +function TopSizeParamEditor(props: AggParamEditorProps) { + const iconTip = ( + <> + {' '} + + + ); + const fieldType = props.agg.params.field && props.agg.params.field.type; + const disabled = fieldType && !getCompatibleAggs(props.agg, props.visName).length; + + return ; +} + +export { TopSizeParamEditor }; diff --git a/src/legacy/ui/public/agg_types/controls/top_sort.html b/src/legacy/ui/public/agg_types/controls/top_sort.html deleted file mode 100644 index bf3951c79ec94..0000000000000 --- a/src/legacy/ui/public/agg_types/controls/top_sort.html +++ /dev/null @@ -1,47 +0,0 @@ -
- - - - - {{$select.selected.displayName}} - - -
-
-
-
- -
- - - -
diff --git a/src/legacy/ui/public/agg_types/controls/top_sort_field.tsx b/src/legacy/ui/public/agg_types/controls/top_sort_field.tsx new file mode 100644 index 0000000000000..4ee8bb5341e02 --- /dev/null +++ b/src/legacy/ui/public/agg_types/controls/top_sort_field.tsx @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { AggParamEditorProps } from '../../vis/editors/default'; +import { FieldParamType } from '../param_types'; +import { FieldParamEditor } from './field'; + +function TopSortFieldParamEditor(props: AggParamEditorProps) { + const customLabel = i18n.translate('common.ui.aggTypes.sortOnLabel', { + defaultMessage: 'Sort on', + }); + + return ; +} + +export { TopSortFieldParamEditor }; diff --git a/src/legacy/ui/public/agg_types/controls/use_geocentroid.tsx b/src/legacy/ui/public/agg_types/controls/use_geocentroid.tsx new file mode 100644 index 0000000000000..d40ccad08b565 --- /dev/null +++ b/src/legacy/ui/public/agg_types/controls/use_geocentroid.tsx @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { EuiSwitch, EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { AggParamEditorProps } from 'ui/vis/editors/default'; + +function UseGeocentroidParamEditor({ value, setValue }: AggParamEditorProps) { + const label = i18n.translate('common.ui.aggTypes.placeMarkersOffGridLabel', { + defaultMessage: 'Place markers off grid (use geocentroid)', + }); + + return ( + + setValue(ev.target.checked)} /> + + ); +} + +export { UseGeocentroidParamEditor }; diff --git a/src/legacy/ui/public/agg_types/directives/auto_select_if_only_one.js b/src/legacy/ui/public/agg_types/directives/auto_select_if_only_one.js deleted file mode 100644 index f0bc12ceb3032..0000000000000 --- a/src/legacy/ui/public/agg_types/directives/auto_select_if_only_one.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { uiModules } from '../../modules'; -const module = uiModules.get('kibana'); - -module.directive('autoSelectIfOnlyOne', function () { - return { - restrict: 'A', - require: 'ngModel', - link: function (scope, element, attributes, ngModelCtrl) { - scope.$watchCollection(attributes.autoSelectIfOnlyOne, (options) => { - if (options && options.length === 1) { - ngModelCtrl.$setViewValue(options[0]); - ngModelCtrl.$render(); - } - }); - } - }; -}); diff --git a/src/legacy/ui/public/agg_types/directives/scroll_bottom.js b/src/legacy/ui/public/agg_types/directives/scroll_bottom.js deleted file mode 100644 index 6ffd2b4964e51..0000000000000 --- a/src/legacy/ui/public/agg_types/directives/scroll_bottom.js +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { uiModules } from '../../modules'; -const module = uiModules.get('kibana'); - -module.directive('kbnScrollBottom', function () { - return { - restrict: 'A', - link: function ($scope, $element, attr) { - let checkTimer; - - function onScroll() { - const position = $element.scrollTop() + $element.outerHeight(); - const height = $element[0].scrollHeight; - const remaining = height - position; - const margin = 50; - - if (!height || !position) return; - if (remaining <= margin) { - $scope.$evalAsync(attr.kbnScrollBottom); - } - } - - function scheduleCheck() { - if (checkTimer) return; - checkTimer = setTimeout(function () { - checkTimer = null; - onScroll(); - }, 50); - } - - $element.on('scroll', scheduleCheck); - $scope.$on('$destroy', function () { - clearTimeout(checkTimer); - $element.off('scroll', scheduleCheck); - }); - scheduleCheck(); - } - }; -}); diff --git a/src/legacy/ui/public/agg_types/directives/validate_cidr_mask.js b/src/legacy/ui/public/agg_types/directives/validate_cidr_mask.js deleted file mode 100644 index 6e33f8a141865..0000000000000 --- a/src/legacy/ui/public/agg_types/directives/validate_cidr_mask.js +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { CidrMask } from '../../utils/cidr_mask'; -import { uiModules } from '../../modules'; - -uiModules.get('kibana').directive('validateCidrMask', function () { - return { - restrict: 'A', - require: 'ngModel', - scope: { - 'ngModel': '=' - }, - link: function ($scope, elem, attr, ngModel) { - ngModel.$parsers.unshift(validateCidrMask); - ngModel.$formatters.unshift(validateCidrMask); - - function validateCidrMask(mask) { - if (mask == null || mask === '') { - ngModel.$setValidity('cidrMaskInput', true); - return null; - } - - try { - mask = new CidrMask(mask); - ngModel.$setValidity('cidrMaskInput', true); - return mask.toString(); - } catch (e) { - ngModel.$setValidity('cidrMaskInput', false); - } - } - } - }; -}); diff --git a/src/legacy/ui/public/agg_types/directives/validate_ip.js b/src/legacy/ui/public/agg_types/directives/validate_ip.js deleted file mode 100644 index a5e8704fb45fc..0000000000000 --- a/src/legacy/ui/public/agg_types/directives/validate_ip.js +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import Ipv4Address from '../../utils/ipv4_address'; -import { uiModules } from '../../modules'; - -uiModules - .get('kibana') - .directive('validateIp', function () { - return { - restrict: 'A', - require: 'ngModel', - scope: { - 'ngModel': '=', - }, - link: function ($scope, elem, attr, ngModel) { - function validateIp(ipAddress) { - if (ipAddress == null || ipAddress === '') { - ngModel.$setValidity('ipInput', true); - return null; - } - - try { - ipAddress = new Ipv4Address(ipAddress); - ngModel.$setValidity('ipInput', true); - return ipAddress.toString(); - } catch (e) { - ngModel.$setValidity('ipInput', false); - } - } - - // From User - ngModel.$parsers.unshift(validateIp); - - // To user - ngModel.$formatters.unshift(validateIp); - } - }; - }); diff --git a/src/legacy/ui/public/agg_types/filter/index.ts b/src/legacy/ui/public/agg_types/filter/index.ts index 9582119b28b36..3fc577e7e9a23 100644 --- a/src/legacy/ui/public/agg_types/filter/index.ts +++ b/src/legacy/ui/public/agg_types/filter/index.ts @@ -18,3 +18,4 @@ */ export { aggTypeFilters } from './agg_type_filters'; +export { propFilter } from './prop_filter'; diff --git a/src/legacy/ui/public/agg_types/filter/prop_filter.test.ts b/src/legacy/ui/public/agg_types/filter/prop_filter.test.ts new file mode 100644 index 0000000000000..0d32c9dc769da --- /dev/null +++ b/src/legacy/ui/public/agg_types/filter/prop_filter.test.ts @@ -0,0 +1,93 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { propFilter } from './prop_filter'; + +describe('prop filter', () => { + let nameFilter: Function; + + beforeEach(() => { + nameFilter = propFilter('name'); + }); + + function getObjects(...names: string[]) { + const count = new Map(); + const objects = []; + + for (const name of names) { + if (!count.has(name)) { + count.set(name, 1); + } + objects.push({ + name, + title: `${name} ${count.get(name)}`, + }); + count.set(name, count.get(name) + 1); + } + return objects; + } + + it('returns list when no filters are provided', () => { + const objects = getObjects('table', 'table', 'pie'); + expect(nameFilter(objects)).to.eql(objects); + }); + + it('returns list when empty list of filters is provided', () => { + const objects = getObjects('table', 'table', 'pie'); + expect(nameFilter(objects, [])).to.eql(objects); + }); + + it('should keep only the tables', () => { + const objects = getObjects('table', 'table', 'pie'); + expect(nameFilter(objects, 'table')).to.eql(getObjects('table', 'table')); + }); + + it('should support comma-separated values', () => { + const objects = getObjects('table', 'line', 'pie'); + expect(nameFilter(objects, 'table,line')).to.eql(getObjects('table', 'line')); + }); + + it('should support an array of values', () => { + const objects = getObjects('table', 'line', 'pie'); + expect(nameFilter(objects, ['table', 'line'])).to.eql(getObjects('table', 'line')); + }); + + it('should return all objects', () => { + const objects = getObjects('table', 'line', 'pie'); + expect(nameFilter(objects, '*')).to.eql(objects); + }); + + it('should allow negation', () => { + const objects = getObjects('table', 'line', 'pie'); + expect(nameFilter(objects, ['!line'])).to.eql(getObjects('table', 'pie')); + }); + + it('should support a function for specifying what should be kept', () => { + const objects = getObjects('table', 'line', 'pie'); + const line = (value: string) => value === 'line'; + expect(nameFilter(objects, line)).to.eql(getObjects('line')); + }); + + it('gracefully handles a filter function with zero arity', () => { + const objects = getObjects('table', 'line', 'pie'); + const rejectEverything = () => false; + expect(nameFilter(objects, rejectEverything)).to.eql([]); + }); +}); diff --git a/src/legacy/ui/public/agg_types/filter/prop_filter.ts b/src/legacy/ui/public/agg_types/filter/prop_filter.ts new file mode 100644 index 0000000000000..45f350ea6adc8 --- /dev/null +++ b/src/legacy/ui/public/agg_types/filter/prop_filter.ts @@ -0,0 +1,99 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { isFunction } from 'lodash'; + +type FilterFunc

= (item: T[P]) => boolean; + +/** + * Filters out a list by a given filter. This is currently used to implement: + * - fieldType filters a list of fields by their type property + * - aggFilter filters a list of aggs by their name property + * + * @returns the filter function which can be registered with angular + */ +function propFilter

(prop: P) { + /** + * List filtering function which accepts an array or list of values that a property + * must contain + * + * @param {array} list - array of items to filter + * @param {function|array|string} filters - the values to match against the list + * - if a function, it is expected to take the field property as argument and returns true to keep it. + * - Can be also an array, a single value as a string, or a comma-separated list of items + * @return {array} - the filtered list + */ + return function filterByName( + list: T[], + filters: string[] | string | FilterFunc = [] + ): T[] { + if (isFunction(filters)) { + return list.filter(item => (filters as FilterFunc)(item[prop])); + } + + if (!Array.isArray(filters)) { + filters = filters.split(','); + } + + if (filters.length === 0) { + return list; + } + + if (filters.includes('*')) { + return list; + } + + const options = filters.reduce( + (acc, filter) => { + let type = 'include'; + let value = filter; + + if (filter.charAt(0) === '!') { + type = 'exclude'; + value = filter.substr(1); + } + + if (!acc[type]) { + acc[type] = []; + } + acc[type].push(value); + return acc; + }, + {} as { [type: string]: string[] } + ); + + return list.filter(item => { + const value = item[prop]; + + const excluded = options.exclude && options.exclude.includes(value); + if (excluded) { + return false; + } + + const included = !options.include || options.include.includes(value); + if (included) { + return true; + } + + return false; + }); + }; +} + +export { propFilter }; diff --git a/src/legacy/ui/public/agg_types/filters/sort_prefix_first.js b/src/legacy/ui/public/agg_types/filters/sort_prefix_first.js deleted file mode 100644 index 553477b61f127..0000000000000 --- a/src/legacy/ui/public/agg_types/filters/sort_prefix_first.js +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { uiModules } from '../../modules'; -import { sortPrefixFirst } from '../../utils/sort_prefix_first'; - -uiModules - .get('kibana') - .filter('sortPrefixFirst', function () { - return sortPrefixFirst; - }); diff --git a/src/legacy/ui/public/agg_types/metrics/percentile_ranks.js b/src/legacy/ui/public/agg_types/metrics/percentile_ranks.js index 6713969d7dd14..2bb23ba1a0a2d 100644 --- a/src/legacy/ui/public/agg_types/metrics/percentile_ranks.js +++ b/src/legacy/ui/public/agg_types/metrics/percentile_ranks.js @@ -17,8 +17,7 @@ * under the License. */ -import valuesEditor from '../controls/percentile_ranks.html'; -import '../number_list'; +import { PercentileRanksEditor } from '../controls/percentile_ranks'; import { MetricAggType } from './metric_agg_type'; import { getResponseAggConfigClass } from './get_response_agg_config_class'; import { fieldFormats } from '../../registry/field_formats'; @@ -60,7 +59,7 @@ export const percentileRanksMetricAgg = new MetricAggType({ }, { name: 'values', - editor: valuesEditor, + editorComponent: PercentileRanksEditor, default: [] }, { diff --git a/src/legacy/ui/public/agg_types/metrics/percentiles.js b/src/legacy/ui/public/agg_types/metrics/percentiles.js index 966e320ff8b3c..d0299e2d6e6f7 100644 --- a/src/legacy/ui/public/agg_types/metrics/percentiles.js +++ b/src/legacy/ui/public/agg_types/metrics/percentiles.js @@ -18,8 +18,7 @@ */ import { ordinalSuffix } from '../../utils/ordinal_suffix'; -import percentsEditor from '../controls/percentiles.html'; -import '../number_list'; +import { PercentilesEditor } from '../controls/percentiles'; import { MetricAggType } from './metric_agg_type'; import { getResponseAggConfigClass } from './get_response_agg_config_class'; import { getPercentileValue } from './percentiles_get_value'; @@ -54,7 +53,7 @@ export const percentilesMetricAgg = new MetricAggType({ }, { name: 'percents', - editor: percentsEditor, + editorComponent: PercentilesEditor, default: [1, 5, 25, 50, 75, 95, 99] }, { diff --git a/src/legacy/ui/public/agg_types/metrics/top_hit.js b/src/legacy/ui/public/agg_types/metrics/top_hit.js index f6594d181a2a4..ce77afaca0c7d 100644 --- a/src/legacy/ui/public/agg_types/metrics/top_hit.js +++ b/src/legacy/ui/public/agg_types/metrics/top_hit.js @@ -19,13 +19,14 @@ import _ from 'lodash'; import { MetricAggType } from './metric_agg_type'; -import '../directives/auto_select_if_only_one'; -import '../directives/scroll_bottom'; -import '../filters/sort_prefix_first'; -import topSortEditor from '../controls/top_sort.html'; -import aggregateAndSizeEditor from '../controls/top_aggregate_and_size.html'; +import { TopSortFieldParamEditor } from '../controls/top_sort_field'; +import { OrderParamEditor } from '../controls/order'; import { aggTypeFieldFilters } from '../param_types/filter'; import { i18n } from '@kbn/i18n'; +import { wrapWithInlineComp } from '../buckets/_inline_comp_wrapper'; +import { TopFieldParamEditor } from '../controls/top_field'; +import { TopSizeParamEditor } from '../controls/top_size'; +import { TopAggregateParamEditor } from '../controls/top_aggregate'; const isNumber = function (type) { return type === 'number'; @@ -38,7 +39,7 @@ aggTypeFieldFilters.addFilter( aggConfig, vis ) => { - if (aggConfig.type.name !== 'top_hit' || vis.type.name === 'table' || vis.type.name === 'metric') { + if (aggConfig.type.name !== 'top_hits' || vis.type.name === 'table' || vis.type.name === 'metric') { return true; } return field.type === 'number'; @@ -57,7 +58,7 @@ export const topHitMetricAgg = new MetricAggType({ const firstPrefixLabel = i18n.translate('common.ui.aggTypes.metrics.topHit.firstPrefixLabel', { defaultMessage: 'First' }); - let prefix = aggConfig.params.sortOrder.val === 'desc' ? lastPrefixLabel : firstPrefixLabel; + let prefix = aggConfig.params.sortOrder.value === 'desc' ? lastPrefixLabel : firstPrefixLabel; if (aggConfig.params.size !== 1) { prefix += ` ${aggConfig.params.size}`; } @@ -68,6 +69,7 @@ export const topHitMetricAgg = new MetricAggType({ { name: 'field', type: 'field', + editorComponent: TopFieldParamEditor, onlyAggregatable: false, filterFieldTypes: '*', write(agg, output) { @@ -97,47 +99,47 @@ export const topHitMetricAgg = new MetricAggType({ }, { name: 'aggregate', - type: 'optioned', - editor: aggregateAndSizeEditor, + type: 'select', + editorComponent: wrapWithInlineComp(TopAggregateParamEditor), options: [ { - display: i18n.translate('common.ui.aggTypes.metrics.topHit.minLabel', { + text: i18n.translate('common.ui.aggTypes.metrics.topHit.minLabel', { defaultMessage: 'Min' }), isCompatibleType: isNumber, isCompatibleVis: _.constant(true), disabled: true, - val: 'min' + value: 'min' }, { - display: i18n.translate('common.ui.aggTypes.metrics.topHit.maxLabel', { + text: i18n.translate('common.ui.aggTypes.metrics.topHit.maxLabel', { defaultMessage: 'Max' }), isCompatibleType: isNumber, isCompatibleVis: _.constant(true), disabled: true, - val: 'max' + value: 'max' }, { - display: i18n.translate('common.ui.aggTypes.metrics.topHit.sumLabel', { + text: i18n.translate('common.ui.aggTypes.metrics.topHit.sumLabel', { defaultMessage: 'Sum' }), isCompatibleType: isNumber, isCompatibleVis: _.constant(true), disabled: true, - val: 'sum' + value: 'sum' }, { - display: i18n.translate('common.ui.aggTypes.metrics.topHit.averageLabel', { + text: i18n.translate('common.ui.aggTypes.metrics.topHit.averageLabel', { defaultMessage: 'Average' }), isCompatibleType: isNumber, isCompatibleVis: _.constant(true), disabled: true, - val: 'average' + value: 'average' }, { - display: i18n.translate('common.ui.aggTypes.metrics.topHit.concatenateLabel', { + text: i18n.translate('common.ui.aggTypes.metrics.topHit.concatenateLabel', { defaultMessage: 'Concatenate' }), isCompatibleType: _.constant(true), @@ -145,33 +147,20 @@ export const topHitMetricAgg = new MetricAggType({ return name === 'metric' || name === 'table'; }, disabled: true, - val: 'concat' + value: 'concat' } ], - controller: function ($scope) { - $scope.options = []; - $scope.$watchGroup([ 'vis.type.name', 'agg.params.field.type' ], function ([ visName, fieldType ]) { - if (fieldType && visName) { - $scope.options = _.filter($scope.aggParam.options, option => { - return option.isCompatibleVis(visName) && option.isCompatibleType(fieldType); - }); - if ($scope.options.length === 1) { - $scope.agg.params.aggregate = $scope.options[0]; - } - } - }); - }, write: _.noop }, { name: 'size', - editor: null, // size setting is done together with the aggregation setting + editorComponent: wrapWithInlineComp(TopSizeParamEditor), default: 1 }, { name: 'sortField', type: 'field', - editor: null, + editorComponent: TopSortFieldParamEditor, filterFieldTypes: [ 'number', 'date', 'ip', 'string' ], default: function (agg) { return agg.getIndexPattern().timeFieldName; @@ -180,21 +169,21 @@ export const topHitMetricAgg = new MetricAggType({ }, { name: 'sortOrder', - type: 'optioned', + type: 'select', default: 'desc', - editor: topSortEditor, + editorComponent: OrderParamEditor, options: [ { - display: i18n.translate('common.ui.aggTypes.metrics.topHit.descendingLabel', { + text: i18n.translate('common.ui.aggTypes.metrics.topHit.descendingLabel', { defaultMessage: 'Descending' }), - val: 'desc' + value: 'desc' }, { - display: i18n.translate('common.ui.aggTypes.metrics.topHit.ascendingLabel', { + text: i18n.translate('common.ui.aggTypes.metrics.topHit.ascendingLabel', { defaultMessage: 'Ascending' }), - val: 'asc' + value: 'asc' } ], write(agg, output) { @@ -210,7 +199,7 @@ export const topHitMetricAgg = new MetricAggType({ lang: sortField.lang }, type: sortField.type, - order: sortOrder.val + order: sortOrder.value } } ]; @@ -218,7 +207,7 @@ export const topHitMetricAgg = new MetricAggType({ output.params.sort = [ { [ sortField.name ]: { - order: sortOrder.val + order: sortOrder.value } } ]; @@ -247,7 +236,7 @@ export const topHitMetricAgg = new MetricAggType({ if (!_.compact(values).length) { return null; } - switch (agg.params.aggregate.val) { + switch (agg.params.aggregate.value) { case 'max': return _.max(values); case 'min': diff --git a/src/legacy/ui/public/agg_types/number_list/index.js b/src/legacy/ui/public/agg_types/number_list/index.js deleted file mode 100644 index 2d3e1e2452897..0000000000000 --- a/src/legacy/ui/public/agg_types/number_list/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import './number_list'; diff --git a/src/legacy/ui/public/agg_types/number_list/index.ts b/src/legacy/ui/public/agg_types/number_list/index.ts new file mode 100644 index 0000000000000..ac830c20cf5ae --- /dev/null +++ b/src/legacy/ui/public/agg_types/number_list/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { NumberList } from './number_list'; diff --git a/src/legacy/ui/public/agg_types/number_list/number_list.html b/src/legacy/ui/public/agg_types/number_list/number_list.html deleted file mode 100644 index 260043979d1c5..0000000000000 --- a/src/legacy/ui/public/agg_types/number_list/number_list.html +++ /dev/null @@ -1,54 +0,0 @@ -

-
- -
- -
- -
-
- -

- -

- -
- - diff --git a/src/legacy/ui/public/agg_types/number_list/number_list.js b/src/legacy/ui/public/agg_types/number_list/number_list.js deleted file mode 100644 index 8e9fa8aed45ab..0000000000000 --- a/src/legacy/ui/public/agg_types/number_list/number_list.js +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import { parseRange } from '../../utils/range'; -import './number_list_input'; -import '../../directives/input_focus'; -import { uiModules } from '../../modules'; -import numberListTemplate from './number_list.html'; - -uiModules - .get('kibana') - .directive('kbnNumberList', function () { - return { - restrict: 'E', - template: numberListTemplate, - controllerAs: 'numberListCntr', - require: 'ngModel', - scope: { - validateAscendingOrder: '=?', - labelledbyId: '@', - }, - controller: function ($scope, $attrs, $parse) { - const self = this; - - self.labelledbyId = $scope.labelledbyId; - - // Called from the pre-link function once we have the controllers - self.init = function (modelCntr) { - self.modelCntr = modelCntr; - - self.getList = function () { - return self.modelCntr.$modelValue; - }; - - self.getUnitName = _.partial($parse($attrs.unit), $scope); - - const defaultRange = self.range = parseRange('[0,Infinity)'); - self.validateAscOrder = _.isUndefined($scope.validateAscendingOrder) ? true : $scope.validateAscendingOrder; - - $scope.$watch(function () { - return $attrs.range; - }, function (range) { - if (!range) { - self.range = defaultRange; - return; - } - - try { - self.range = parseRange(range); - } catch (e) { - throw new TypeError('Unable to parse range: ' + e.message); - } - }); - - /** - * Remove an item from list by index - * @param {number} index - * @return {undefined} - */ - self.remove = function (index) { - const list = self.getList(); - if (!list) return; - - list.splice(index, 1); - }; - - /** - * Add an item to the end of the list - * @return {undefined} - */ - self.add = function () { - const list = self.getList(); - if (!list) return; - - function getNext() { - if (list.length === 0) { - // returning NaN adds an empty input - return NaN; - } - - const next = _.last(list) + 1; - if (next < self.range.max) { - return next; - } - - return self.range.max - 1; - } - - const next = getNext(); - list.push(next); - }; - - /** - * Check to see if the list is too short. - * - * @return {Boolean} - */ - self.tooShort = function () { - return _.size(self.getList()) < 1; - }; - - /** - * Check to see if the list is too short, but simply - * because the user hasn't interacted with it yet - * - * @return {Boolean} - */ - self.undefinedLength = function () { - return self.tooShort() && (self.modelCntr.$untouched && self.modelCntr.$pristine); - }; - - /** - * Check to see if the list is too short - * - * @return {Boolean} - */ - self.invalidLength = function () { - return self.tooShort() && !self.undefinedLength(); - }; - - $scope.$watchCollection(self.getList, function () { - self.modelCntr.$setValidity('numberListLength', !self.tooShort()); - }); - }; - }, - link: { - pre: function ($scope, $el, attrs, ngModelCntr) { - $scope.numberListCntr.init(ngModelCntr); - } - }, - }; - }); diff --git a/src/legacy/ui/public/agg_types/number_list/number_list.tsx b/src/legacy/ui/public/agg_types/number_list/number_list.tsx new file mode 100644 index 0000000000000..4a075fe399e67 --- /dev/null +++ b/src/legacy/ui/public/agg_types/number_list/number_list.tsx @@ -0,0 +1,178 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { Fragment, useState, useEffect } from 'react'; + +import { EuiSpacer, EuiButtonEmpty, EuiFlexItem, EuiFormErrorText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { NumberRow, NumberRowModel } from './number_row'; +import { + parse, + EMPTY_STRING, + getRange, + validateOrder, + validateValue, + getNextModel, + getInitModelList, + getUpdatedModels, + hasInvalidValues, +} from './utils'; + +interface NumberListProps { + labelledbyId: string; + numberArray: Array; + range?: string; + showValidation: boolean; + unitName: string; + validateAscendingOrder?: boolean; + onBlur?(): void; + onFocus?(): void; + onChange(list: Array): void; + setTouched(): void; + setValidity(isValid: boolean): void; +} + +function NumberList({ + labelledbyId, + numberArray, + range, + showValidation, + unitName, + validateAscendingOrder = true, + onBlur, + onFocus, + onChange, + setTouched, + setValidity, +}: NumberListProps) { + const numberRange = getRange(range); + const [models, setModels] = useState(getInitModelList(numberArray)); + const [ascendingError, setAscendingError] = useState(EMPTY_STRING); + + // responsible for discarding changes + useEffect( + () => { + const updatedModels = getUpdatedModels(numberArray, models, numberRange); + if (validateAscendingOrder) { + const isOrderValid = validateOrder(updatedModels); + setAscendingError( + isOrderValid + ? i18n.translate('common.ui.aggTypes.numberList.invalidAscOrderErrorMessage', { + defaultMessage: 'The values should be in ascending order.', + }) + : EMPTY_STRING + ); + } + setModels(updatedModels); + }, + [numberArray] + ); + + useEffect( + () => { + setValidity(!hasInvalidValues(models)); + }, + [models] + ); + + // resposible for setting up an initial value ([0]) when there is no default value + useEffect(() => { + onChange(models.map(({ value }) => (value === EMPTY_STRING ? undefined : value))); + }, []); + + const onChangeValue = ({ id, value }: { id: string; value: string }) => { + const parsedValue = parse(value); + const { isValid, errors } = validateValue(parsedValue, numberRange); + setValidity(isValid); + + const currentModel = models.find(model => model.id === id); + if (currentModel) { + currentModel.value = parsedValue; + currentModel.isInvalid = !isValid; + currentModel.errors = errors; + } + + onUpdate(models); + }; + + // Add an item to the end of the list + const onAdd = () => { + const newArray = [...models, getNextModel(models, numberRange)]; + onUpdate(newArray); + }; + + const onDelete = (id: string) => { + const newArray = models.filter(model => model.id !== id); + onUpdate(newArray); + }; + + const onBlurFn = (model: NumberRowModel) => { + if (model.value === EMPTY_STRING) { + model.isInvalid = true; + } + setTouched(); + if (onBlur) { + onBlur(); + } + }; + + const onUpdate = (modelList: NumberRowModel[]) => { + setModels(modelList); + onChange(modelList.map(({ value }) => (value === EMPTY_STRING ? undefined : value))); + }; + + return ( + <> + {models.map((model, arrayIndex) => ( + + onBlurFn(model)} + autoFocus={models.length !== 1 && arrayIndex === models.length - 1} + /> + {showValidation && model.isInvalid && model.errors && model.errors.length > 0 && ( + {model.errors.join('\n')} + )} + {models.length - 1 !== arrayIndex && } + + ))} + {showValidation && ascendingError && {ascendingError}} + + + + + + + + ); +} + +export { NumberList }; diff --git a/src/legacy/ui/public/agg_types/number_list/number_list_input.js b/src/legacy/ui/public/agg_types/number_list/number_list_input.js deleted file mode 100644 index a869da7fd3420..0000000000000 --- a/src/legacy/ui/public/agg_types/number_list/number_list_input.js +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { keyMap } from '../../utils/key_map'; -import '../../fancy_forms'; -import { uiModules } from '../../modules'; - -const INVALID = {}; // invalid flag -const FLOATABLE = /^[\d\.e\-\+]+$/i; - -const VALIDATION_ERROR = 'numberListRangeAndOrder'; -const DIRECTIVE_ATTR = 'kbn-number-list-input'; - -uiModules - .get('kibana') - .directive('kbnNumberListInput', function ($parse) { - return { - restrict: 'A', - require: ['ngModel', '^kbnNumberList'], - link: function ($scope, $el, attrs, controllers) { - const ngModelCntr = controllers[0]; - const numberListCntr = controllers[1]; - - const $setModel = $parse(attrs.ngModel).assign; - const $repeater = $el.closest('[ng-repeat]'); - - const handlers = { - up: change(add, 1), - 'shift-up': change(addTenth, 1), - - down: change(add, -1), - 'shift-down': change(addTenth, -1), - - tab: go('next'), - 'shift-tab': go('prev'), - - 'shift-enter': numberListCntr.add, - - backspace: removeIfEmpty, - delete: removeIfEmpty - }; - - function removeIfEmpty(event) { - if (!ngModelCntr.$viewValue) { - $get('prev').focus(); - numberListCntr.remove($scope.$index); - event.preventDefault(); - } - - return false; - } - - function $get(dir) { - return $repeater[dir]().find('[' + DIRECTIVE_ATTR + ']'); - } - - function go(dir) { - return function () { - const $to = $get(dir); - if ($to.length) $to.focus(); - else return false; - }; - } - - function idKey(event) { - const id = []; - if (event.ctrlKey) id.push('ctrl'); - if (event.shiftKey) id.push('shift'); - if (event.metaKey) id.push('meta'); - if (event.altKey) id.push('alt'); - id.push(keyMap[event.keyCode] || event.keyCode); - return id.join('-'); - } - - function add(n, val) { - return parse(val + n); - } - - function addTenth(n, val, str) { - let int = Math.floor(val); - let dec = parseInt(str.split('.')[1] || 0, 10); - dec = dec + parseInt(n, 10); - - if (dec < 0 || dec > 9) { - int += Math.floor(dec / 10); - if (dec < 0) { - dec = 10 + (dec % 10); - } else { - dec = dec % 10; - } - } - - return parse(int + '.' + dec); - } - - function change(using, mod) { - return function () { - const str = String(ngModelCntr.$viewValue); - const val = parse(str); - if (val === INVALID) return; - - const next = using(mod, val, str); - if (next === INVALID) return; - - $el.val(next); - ngModelCntr.$setViewValue(next); - }; - } - - function onKeydown(event) { - const handler = handlers[idKey(event)]; - if (!handler) return; - - if (handler(event) !== false) { - event.preventDefault(); - } - - $scope.$apply(); - } - - $el.on('keydown', onKeydown); - $scope.$on('$destroy', function () { - $el.off('keydown', onKeydown); - }); - - function parse(viewValue) { - let num = viewValue; - - if (typeof num !== 'number' || isNaN(num)) { - // parse non-numbers - num = String(viewValue || 0).trim(); - if (!FLOATABLE.test(num)) return INVALID; - - num = parseFloat(num); - if (isNaN(num)) return INVALID; - } - - const range = numberListCntr.range; - if (!range.within(num)) return INVALID; - - if (numberListCntr.validateAscOrder && $scope.$index > 0) { - const i = $scope.$index - 1; - const list = numberListCntr.getList(); - const prev = list[i]; - if (num <= prev) return INVALID; - } - - return num; - } - - $scope.$watchMulti([ - '$index', - { - fn: $scope.$watchCollection, - get: function () { - return numberListCntr.getList(); - } - } - ], function () { - const valid = parse(ngModelCntr.$viewValue) !== INVALID; - ngModelCntr.$setValidity(VALIDATION_ERROR, valid); - }); - - function validate(then) { - return function (input) { - let value = parse(input); - const valid = value !== INVALID; - value = valid ? value : input; - ngModelCntr.$setValidity(VALIDATION_ERROR, valid); - then && then(input, value); - return value; - }; - } - - ngModelCntr.$parsers.push(validate()); - ngModelCntr.$formatters.push(validate(function (input, value) { - if (input !== value) $setModel($scope, value); - })); - - if (parse(ngModelCntr.$viewValue) === INVALID) { - ngModelCntr.$setTouched(); - } - } - }; - }); - diff --git a/src/legacy/ui/public/agg_types/number_list/number_row.tsx b/src/legacy/ui/public/agg_types/number_list/number_row.tsx new file mode 100644 index 0000000000000..6b2f44acd9eb3 --- /dev/null +++ b/src/legacy/ui/public/agg_types/number_list/number_row.tsx @@ -0,0 +1,106 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { EuiFieldNumber, EuiFlexGroup, EuiFlexItem, EuiButtonIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { Range } from '../../utils/range'; + +interface NumberRowProps { + autoFocus: boolean; + disableDelete: boolean; + isInvalid: boolean; + labelledbyId: string; + model: NumberRowModel; + range: Range; + onBlur(): void; + onFocus?(): void; + onChange({ id, value }: { id: string; value: string }): void; + onDelete(index: string): void; +} + +export interface NumberRowModel { + id: string; + isInvalid: boolean; + value: number | ''; + errors?: string[]; +} + +function NumberRow({ + autoFocus, + disableDelete, + model, + isInvalid, + labelledbyId, + range, + onBlur, + onDelete, + onFocus, + onChange, +}: NumberRowProps) { + const deleteBtnAriaLabel = i18n.translate( + 'common.ui.aggTypes.numberList.removeUnitButtonAriaLabel', + { + defaultMessage: 'Remove the rank value of {value}', + values: { value: model.value }, + } + ); + + const onValueChanged = (event: React.ChangeEvent) => + onChange({ + value: event.target.value, + id: model.id, + }); + + return ( + + + + + + onDelete(model.id)} + disabled={disableDelete} + /> + + + ); +} + +export { NumberRow }; diff --git a/src/legacy/ui/public/agg_types/number_list/utils.ts b/src/legacy/ui/public/agg_types/number_list/utils.ts new file mode 100644 index 0000000000000..a4383c39dee31 --- /dev/null +++ b/src/legacy/ui/public/agg_types/number_list/utils.ts @@ -0,0 +1,145 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { last } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { htmlIdGenerator } from '@elastic/eui'; + +import { parseRange, Range } from '../../utils/range'; +import { NumberRowModel } from './number_row'; + +const EMPTY_STRING = ''; +const defaultRange = parseRange('[0,Infinity)'); +const generateId = htmlIdGenerator(); + +function parse(value: string) { + const parsedValue = parseFloat(value); + return isNaN(parsedValue) ? EMPTY_STRING : parsedValue; +} + +function getRange(range?: string): Range { + try { + return range ? parseRange(range) : defaultRange; + } catch (e) { + throw new TypeError('Unable to parse range: ' + e.message); + } +} + +function validateValue(value: number | '', numberRange: Range) { + const result = { + isValid: true, + errors: [] as string[], + }; + + if (value === EMPTY_STRING) { + result.isValid = false; + } else if (!numberRange.within(value)) { + result.isValid = false; + result.errors.push( + i18n.translate('common.ui.aggTypes.numberList.invalidRangeErrorMessage', { + defaultMessage: 'The value should be in the range of {min} to {max}.', + values: { min: numberRange.min, max: numberRange.max }, + }) + ); + } + + return result; +} + +function validateOrder(list: NumberRowModel[]) { + let isInvalidOrder = false; + list.forEach((model, index, array) => { + const previousModel = array[index - 1]; + if (previousModel && model.value !== EMPTY_STRING) { + const isInvalidOrderOfItem = model.value <= previousModel.value; + + if (!model.isInvalid && isInvalidOrderOfItem) { + model.isInvalid = true; + } + + if (isInvalidOrderOfItem) { + isInvalidOrder = true; + } + } + }); + + return isInvalidOrder; +} + +function getNextModel(list: NumberRowModel[], range: Range): NumberRowModel { + const lastValue = last(list).value; + let next = Number(lastValue) ? Number(lastValue) + 1 : 1; + + if (next >= range.max) { + next = range.max - 1; + } + + return { + id: generateId(), + value: next, + isInvalid: false, + }; +} + +function getInitModelList(list: Array): NumberRowModel[] { + return list.length + ? list.map(num => ({ + value: (num === undefined ? EMPTY_STRING : num) as NumberRowModel['value'], + id: generateId(), + isInvalid: false, + })) + : [{ value: 0, id: generateId(), isInvalid: false }]; +} + +function getUpdatedModels( + numberList: Array, + modelList: NumberRowModel[], + numberRange: Range +): NumberRowModel[] { + if (!numberList.length) { + return modelList; + } + return numberList.map((number, index) => { + const model = modelList[index] || { id: generateId() }; + const newValue: NumberRowModel['value'] = number === undefined ? EMPTY_STRING : number; + const { isValid, errors } = validateValue(newValue, numberRange); + return { + ...model, + value: newValue, + isInvalid: !isValid, + errors, + }; + }); +} + +function hasInvalidValues(modelList: NumberRowModel[]): boolean { + return !!modelList.find(({ isInvalid }) => isInvalid); +} + +export { + EMPTY_STRING, + parse, + getRange, + validateValue, + validateOrder, + getNextModel, + getInitModelList, + getUpdatedModels, + hasInvalidValues, +}; diff --git a/src/legacy/ui/public/agg_types/param_types/field.js b/src/legacy/ui/public/agg_types/param_types/field.js index 5c883c2c588b5..53a861da6a8cd 100644 --- a/src/legacy/ui/public/agg_types/param_types/field.js +++ b/src/legacy/ui/public/agg_types/param_types/field.js @@ -20,13 +20,11 @@ import { sortBy } from 'lodash'; import { SavedObjectNotFound } from '../../errors'; import { FieldParamEditor } from '../controls/field'; -import '../directives/scroll_bottom'; import { BaseParamType } from './base'; -import '../filters/sort_prefix_first'; import { IndexedArray } from '../../indexed_array'; import { toastNotifications } from '../../notify'; import { createLegacyClass } from '../../utils/legacy_class'; -import { propFilter } from '../../filters/_prop_filter'; +import { propFilter } from '../filter'; import { i18n } from '@kbn/i18n'; const filterByType = propFilter('type'); diff --git a/src/legacy/ui/public/agg_types/param_types/select.d.ts b/src/legacy/ui/public/agg_types/param_types/select.d.ts index 06aeb0b37e066..d2f26b34ade2e 100644 --- a/src/legacy/ui/public/agg_types/param_types/select.d.ts +++ b/src/legacy/ui/public/agg_types/param_types/select.d.ts @@ -24,15 +24,15 @@ interface SelectValueProp { text: string; } -interface SelectOptions extends IndexedArray { +interface SelectOptions extends IndexedArray { byValue: { - [key: string]: SelectValueProp; + [key: string]: T; }; } -interface SelectParamEditorProps { +interface SelectParamEditorProps { aggParam: { - options: SelectOptions; + options: SelectOptions; }; } diff --git a/src/legacy/ui/public/apply_filters/directive.js b/src/legacy/ui/public/apply_filters/directive.js deleted file mode 100644 index f69bde8dce8d4..0000000000000 --- a/src/legacy/ui/public/apply_filters/directive.js +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import 'ngreact'; -import { uiModules } from '../modules'; -import template from './directive.html'; -import { ApplyFiltersPopover } from './apply_filters_popover'; -import { mapAndFlattenFilters } from '../filter_bar/lib/map_and_flatten_filters'; -import { wrapInI18nContext } from 'ui/i18n'; - -const app = uiModules.get('app/kibana', ['react']); - -app.directive('applyFiltersPopoverComponent', (reactDirective) => { - return reactDirective(wrapInI18nContext(ApplyFiltersPopover)); -}); - -app.directive('applyFiltersPopover', (indexPatterns) => { - return { - template, - restrict: 'E', - scope: { - filters: '=', - onCancel: '=', - onSubmit: '=', - }, - link: function ($scope) { - $scope.state = {}; - - // Each time the new filters change we want to rebuild (not just re-render) the "apply filters" - // popover, because it has to reset its state whenever the new filters change. Setting a `key` - // property on the component accomplishes this due to how React handles the `key` property. - $scope.$watch('filters', filters => { - mapAndFlattenFilters(indexPatterns, filters).then(mappedFilters => { - $scope.state = { - filters: mappedFilters, - key: Date.now(), - }; - }); - }); - } - }; -}); diff --git a/src/legacy/ui/public/apply_filters/index.ts b/src/legacy/ui/public/apply_filters/index.ts deleted file mode 100644 index 4346316e62374..0000000000000 --- a/src/legacy/ui/public/apply_filters/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import './directive'; - -export { ApplyFiltersPopover } from './apply_filters_popover'; diff --git a/src/legacy/ui/public/capabilities/index.ts b/src/legacy/ui/public/capabilities/index.ts index 7bb0c114e3945..e16116f024a0f 100644 --- a/src/legacy/ui/public/capabilities/index.ts +++ b/src/legacy/ui/public/capabilities/index.ts @@ -17,27 +17,13 @@ * under the License. */ +import { npStart } from 'ui/new_platform'; import { Capabilities as UICapabilities } from '../../../../core/public'; export { UICapabilities }; -let uiCapabilities: UICapabilities; - -export function __newPlatformStart__(capabilities: UICapabilities) { - if (uiCapabilities) { - throw new Error('ui/capabilities already initialized with new platform apis'); - } - - uiCapabilities = capabilities; -} export const capabilities = { get() { - if (!uiCapabilities) { - throw new Error( - `UI Capabilities are only available in the legacy platform once Angular has booted.` - ); - } - - return uiCapabilities; + return npStart.core.application.capabilities; }, }; diff --git a/src/legacy/ui/public/chrome/api/__tests__/nav.js b/src/legacy/ui/public/chrome/api/__tests__/nav.js index ff20f69586180..62089cdefa8ee 100644 --- a/src/legacy/ui/public/chrome/api/__tests__/nav.js +++ b/src/legacy/ui/public/chrome/api/__tests__/nav.js @@ -22,7 +22,7 @@ import sinon from 'sinon'; import { initChromeNavApi } from '../nav'; import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; -import { getNewPlatform } from 'ui/new_platform'; +import { npStart } from 'ui/new_platform'; import { absoluteToParsedUrl } from '../../../url/absolute_to_parsed_url'; const basePath = '/someBasePath'; @@ -52,7 +52,7 @@ describe('chrome nav apis', function () { }()); beforeEach(() => { - coreNavLinks = getNewPlatform().start.core.chrome.navLinks; + coreNavLinks = npStart.core.chrome.navLinks; sinon.stub(coreNavLinks, 'update').callsFake((linkId, updateAttrs) => { const link = fakedLinks.find(({ id }) => id === linkId); for (const key of Object.keys(updateAttrs)) { diff --git a/src/legacy/ui/public/chrome/api/badge.test.mocks.ts b/src/legacy/ui/public/chrome/api/badge.test.mocks.ts new file mode 100644 index 0000000000000..879942a9aea82 --- /dev/null +++ b/src/legacy/ui/public/chrome/api/badge.test.mocks.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { chromeServiceMock } from '../../../../../core/public/mocks'; + +export const newPlatformChrome = chromeServiceMock.createSetupContract(); +jest.doMock('ui/new_platform', () => ({ + npSetup: { + core: { chrome: newPlatformChrome }, + }, +})); diff --git a/src/legacy/ui/public/chrome/api/badge.test.ts b/src/legacy/ui/public/chrome/api/badge.test.ts index 7f0de36031cc1..02fc1e39d397c 100644 --- a/src/legacy/ui/public/chrome/api/badge.test.ts +++ b/src/legacy/ui/public/chrome/api/badge.test.ts @@ -20,12 +20,8 @@ import * as Rx from 'rxjs'; import { ChromeBadge } from 'src/core/public/chrome'; -import { chromeServiceMock } from '../../../../../core/public/mocks'; -import { __newPlatformSetup__, initChromeBadgeApi } from './badge'; - -const newPlatformChrome = chromeServiceMock.createSetupContract(); - -__newPlatformSetup__(newPlatformChrome); +import { newPlatformChrome } from './badge.test.mocks'; +import { initChromeBadgeApi } from './badge'; function setup() { const getBadge$ = new Rx.BehaviorSubject(undefined); diff --git a/src/legacy/ui/public/chrome/api/badge.ts b/src/legacy/ui/public/chrome/api/badge.ts index c1c5d4c7eccd4..811f61bc6ca56 100644 --- a/src/legacy/ui/public/chrome/api/badge.ts +++ b/src/legacy/ui/public/chrome/api/badge.ts @@ -18,19 +18,13 @@ */ import { Chrome } from 'ui/chrome'; -import { ChromeBadge, ChromeSetup } from '../../../../../core/public'; +import { npSetup } from 'ui/new_platform'; +import { ChromeBadge } from '../../../../../core/public'; export type Badge = ChromeBadge; export type BadgeApi = ReturnType['badge']; -let newPlatformChrome: ChromeSetup; -export function __newPlatformSetup__(instance: ChromeSetup) { - if (newPlatformChrome) { - throw new Error('ui/chrome/api/badge is already initialized'); - } - - newPlatformChrome = instance; -} +const newPlatformChrome = npSetup.core.chrome; function createBadgeApi() { return { diff --git a/src/legacy/ui/public/chrome/api/base_path.test.mocks.ts b/src/legacy/ui/public/chrome/api/base_path.test.mocks.ts new file mode 100644 index 0000000000000..c362b1709fba6 --- /dev/null +++ b/src/legacy/ui/public/chrome/api/base_path.test.mocks.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { httpServiceMock } from '../../../../../core/public/mocks'; + +export const newPlatformHttp = httpServiceMock.createSetupContract(); +jest.doMock('ui/new_platform', () => ({ + npSetup: { + core: { http: newPlatformHttp }, + }, +})); diff --git a/src/legacy/ui/public/chrome/api/base_path.test.ts b/src/legacy/ui/public/chrome/api/base_path.test.ts index a69d1d3581164..448872e87e458 100644 --- a/src/legacy/ui/public/chrome/api/base_path.test.ts +++ b/src/legacy/ui/public/chrome/api/base_path.test.ts @@ -17,8 +17,8 @@ * under the License. */ -import { basePathServiceMock } from '../../../../../core/public/mocks'; -import { __newPlatformSetup__, initChromeBasePathApi } from './base_path'; +import { newPlatformHttp } from './base_path.test.mocks'; +import { initChromeBasePathApi } from './base_path'; function initChrome() { const chrome: any = {}; @@ -26,39 +26,40 @@ function initChrome() { return chrome; } -const newPlatformBasePath = basePathServiceMock.createSetupContract(); -__newPlatformSetup__(newPlatformBasePath); +newPlatformHttp.getBasePath.mockImplementation(() => 'gotBasePath'); +newPlatformHttp.prependBasePath.mockImplementation(() => 'addedToPath'); +newPlatformHttp.removeBasePath.mockImplementation(() => 'removedFromPath'); beforeEach(() => { jest.clearAllMocks(); }); describe('#getBasePath()', () => { - it('proxies to newPlatformBasePath.get()', () => { + it('proxies to newPlatformHttp.getBasePath()', () => { const chrome = initChrome(); - expect(newPlatformBasePath.get).not.toHaveBeenCalled(); - expect(chrome.getBasePath()).toBe('get'); - expect(newPlatformBasePath.get).toHaveBeenCalledTimes(1); - expect(newPlatformBasePath.get).toHaveBeenCalledWith(); + expect(newPlatformHttp.prependBasePath).not.toHaveBeenCalled(); + expect(chrome.getBasePath()).toBe('gotBasePath'); + expect(newPlatformHttp.getBasePath).toHaveBeenCalledTimes(1); + expect(newPlatformHttp.getBasePath).toHaveBeenCalledWith(); }); }); describe('#addBasePath()', () => { - it('proxies to newPlatformBasePath.addToPath(path)', () => { + it('proxies to newPlatformHttp.prependBasePath(path)', () => { const chrome = initChrome(); - expect(newPlatformBasePath.addToPath).not.toHaveBeenCalled(); - expect(chrome.addBasePath('foo/bar')).toBe('addToPath'); - expect(newPlatformBasePath.addToPath).toHaveBeenCalledTimes(1); - expect(newPlatformBasePath.addToPath).toHaveBeenCalledWith('foo/bar'); + expect(newPlatformHttp.prependBasePath).not.toHaveBeenCalled(); + expect(chrome.addBasePath('foo/bar')).toBe('addedToPath'); + expect(newPlatformHttp.prependBasePath).toHaveBeenCalledTimes(1); + expect(newPlatformHttp.prependBasePath).toHaveBeenCalledWith('foo/bar'); }); }); describe('#removeBasePath', () => { - it('proxies to newPlatformBasePath.removeFromPath(path)', () => { + it('proxies to newPlatformBasePath.removeBasePath(path)', () => { const chrome = initChrome(); - expect(newPlatformBasePath.removeFromPath).not.toHaveBeenCalled(); - expect(chrome.removeBasePath('foo/bar')).toBe('removeFromPath'); - expect(newPlatformBasePath.removeFromPath).toHaveBeenCalledTimes(1); - expect(newPlatformBasePath.removeFromPath).toHaveBeenCalledWith('foo/bar'); + expect(newPlatformHttp.removeBasePath).not.toHaveBeenCalled(); + expect(chrome.removeBasePath('foo/bar')).toBe('removedFromPath'); + expect(newPlatformHttp.removeBasePath).toHaveBeenCalledTimes(1); + expect(newPlatformHttp.removeBasePath).toHaveBeenCalledWith('foo/bar'); }); }); diff --git a/src/legacy/ui/public/chrome/api/base_path.ts b/src/legacy/ui/public/chrome/api/base_path.ts index 87d637513f55f..cb342627f09d5 100644 --- a/src/legacy/ui/public/chrome/api/base_path.ts +++ b/src/legacy/ui/public/chrome/api/base_path.ts @@ -17,19 +17,12 @@ * under the License. */ -import { BasePathSetup } from '../../../../../core/public'; -let newPlatformBasePath: BasePathSetup; +import { npSetup } from 'ui/new_platform'; -export function __newPlatformSetup__(instance: BasePathSetup) { - if (newPlatformBasePath) { - throw new Error('ui/chrome/api/base_path is already initialized'); - } - - newPlatformBasePath = instance; -} +const newPlatformHttp = npSetup.core.http; export function initChromeBasePathApi(chrome: { [key: string]: any }) { - chrome.getBasePath = () => newPlatformBasePath.get(); - chrome.addBasePath = (path: string) => newPlatformBasePath.addToPath(path); - chrome.removeBasePath = (path: string) => newPlatformBasePath.removeFromPath(path); + chrome.getBasePath = newPlatformHttp.getBasePath.bind(newPlatformHttp); + chrome.addBasePath = newPlatformHttp.prependBasePath.bind(newPlatformHttp); + chrome.removeBasePath = newPlatformHttp.removeBasePath.bind(newPlatformHttp); } diff --git a/src/legacy/ui/public/chrome/api/breadcrumbs.ts b/src/legacy/ui/public/chrome/api/breadcrumbs.ts index d65897d60d8f4..fe40a428e546b 100644 --- a/src/legacy/ui/public/chrome/api/breadcrumbs.ts +++ b/src/legacy/ui/public/chrome/api/breadcrumbs.ts @@ -17,19 +17,13 @@ * under the License. */ -import { ChromeBreadcrumb, ChromeSetup } from '../../../../../core/public'; +import { npSetup } from 'ui/new_platform'; +import { ChromeBreadcrumb } from '../../../../../core/public'; export type Breadcrumb = ChromeBreadcrumb; export type BreadcrumbsApi = ReturnType['breadcrumbs']; -let newPlatformChrome: ChromeSetup; -export function __newPlatformSetup__(instance: ChromeSetup) { - if (newPlatformChrome) { - throw new Error('ui/chrome/api/breadcrumbs is already initialized'); - } - - newPlatformChrome = instance; -} +const newPlatformChrome = npSetup.core.chrome; function createBreadcrumbsApi(chrome: { [key: string]: any }) { let currentBreadcrumbs: Breadcrumb[] = []; diff --git a/src/legacy/ui/public/chrome/api/controls.test.mocks.ts b/src/legacy/ui/public/chrome/api/controls.test.mocks.ts new file mode 100644 index 0000000000000..879942a9aea82 --- /dev/null +++ b/src/legacy/ui/public/chrome/api/controls.test.mocks.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { chromeServiceMock } from '../../../../../core/public/mocks'; + +export const newPlatformChrome = chromeServiceMock.createSetupContract(); +jest.doMock('ui/new_platform', () => ({ + npSetup: { + core: { chrome: newPlatformChrome }, + }, +})); diff --git a/src/legacy/ui/public/chrome/api/controls.test.ts b/src/legacy/ui/public/chrome/api/controls.test.ts index 4dcbe59f99e33..f41f2041993a3 100644 --- a/src/legacy/ui/public/chrome/api/controls.test.ts +++ b/src/legacy/ui/public/chrome/api/controls.test.ts @@ -19,12 +19,8 @@ import * as Rx from 'rxjs'; -import { chromeServiceMock } from '../../../../../core/public/mocks'; -import { __newPlatformSetup__, initChromeControlsApi } from './controls'; - -const newPlatformChrome = chromeServiceMock.createSetupContract(); - -__newPlatformSetup__(newPlatformChrome); +import { newPlatformChrome } from './controls.test.mocks'; +import { initChromeControlsApi } from './controls'; function setup() { const isVisible$ = new Rx.BehaviorSubject(true); diff --git a/src/legacy/ui/public/chrome/api/controls.ts b/src/legacy/ui/public/chrome/api/controls.ts index e36625b03a1ad..4a27130cfa5ba 100644 --- a/src/legacy/ui/public/chrome/api/controls.ts +++ b/src/legacy/ui/public/chrome/api/controls.ts @@ -18,17 +18,9 @@ */ import * as Rx from 'rxjs'; -import { ChromeSetup } from '../../../../../core/public'; +import { npSetup } from 'ui/new_platform'; -let newPlatformChrome: ChromeSetup; - -export function __newPlatformSetup__(instance: ChromeSetup) { - if (newPlatformChrome) { - throw new Error('ui/chrome/api/controls is already initialized'); - } - - newPlatformChrome = instance; -} +const newPlatformChrome = npSetup.core.chrome; export function initChromeControlsApi(chrome: { [key: string]: any }) { // cache of chrome visibility state diff --git a/src/legacy/ui/public/chrome/api/help_extension.ts b/src/legacy/ui/public/chrome/api/help_extension.ts index 5bfd1d2d37170..acac363f13d0f 100644 --- a/src/legacy/ui/public/chrome/api/help_extension.ts +++ b/src/legacy/ui/public/chrome/api/help_extension.ts @@ -17,16 +17,10 @@ * under the License. */ -import { ChromeHelpExtension, ChromeSetup } from '../../../../../core/public'; +import { npSetup } from 'ui/new_platform'; +import { ChromeHelpExtension } from '../../../../../core/public'; -let newPlatformChrome: ChromeSetup; -export function __newPlatformSetup__(instance: ChromeSetup) { - if (newPlatformChrome) { - throw new Error('ui/chrome/api/help_extension is already initialized'); - } - - newPlatformChrome = instance; -} +const newPlatformChrome = npSetup.core.chrome; export type HelpExtensionApi = ReturnType['helpExtension']; export type HelpExtension = ChromeHelpExtension; diff --git a/src/legacy/ui/public/chrome/api/injected_vars.test.mocks.ts b/src/legacy/ui/public/chrome/api/injected_vars.test.mocks.ts new file mode 100644 index 0000000000000..3ad713ccce9a4 --- /dev/null +++ b/src/legacy/ui/public/chrome/api/injected_vars.test.mocks.ts @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const newPlatformInjectedMetadata: any = { + getInjectedVars: jest.fn(), + getInjectedVar: jest.fn(), +}; +jest.doMock('ui/new_platform', () => ({ + npSetup: { + core: { injectedMetadata: newPlatformInjectedMetadata }, + }, +})); diff --git a/src/legacy/ui/public/chrome/api/injected_vars.test.ts b/src/legacy/ui/public/chrome/api/injected_vars.test.ts index 9fab84e6bc983..2147aed06320e 100644 --- a/src/legacy/ui/public/chrome/api/injected_vars.test.ts +++ b/src/legacy/ui/public/chrome/api/injected_vars.test.ts @@ -17,7 +17,8 @@ * under the License. */ -import { __newPlatformSetup__, initChromeInjectedVarsApi } from './injected_vars'; +import { newPlatformInjectedMetadata } from './injected_vars.test.mocks'; +import { initChromeInjectedVarsApi } from './injected_vars'; function initChrome() { const chrome: any = {}; @@ -25,12 +26,6 @@ function initChrome() { return chrome; } -const newPlatformInjectedMetadata: any = { - getInjectedVars: jest.fn(), - getInjectedVar: jest.fn(), -}; -__newPlatformSetup__(newPlatformInjectedMetadata); - beforeEach(() => { jest.resetAllMocks(); }); diff --git a/src/legacy/ui/public/chrome/api/injected_vars.ts b/src/legacy/ui/public/chrome/api/injected_vars.ts index 25d3982a0968d..a827c1bf65f51 100644 --- a/src/legacy/ui/public/chrome/api/injected_vars.ts +++ b/src/legacy/ui/public/chrome/api/injected_vars.ts @@ -18,17 +18,9 @@ */ import { cloneDeep } from 'lodash'; -import { InjectedMetadataSetup } from '../../../../../core/public'; +import { npSetup } from 'ui/new_platform'; -let newPlatformInjectedVars: InjectedMetadataSetup; - -export function __newPlatformSetup__(instance: InjectedMetadataSetup) { - if (newPlatformInjectedVars) { - throw new Error('ui/chrome/api/injected_vars is already initialized'); - } - - newPlatformInjectedVars = instance; -} +const newPlatformInjectedVars = npSetup.core.injectedMetadata; export function initChromeInjectedVarsApi(chrome: { [key: string]: any }) { chrome.getInjected = (name?: string, defaultValue?: any) => diff --git a/src/legacy/ui/public/chrome/api/loading_count.js b/src/legacy/ui/public/chrome/api/loading_count.js index 0aba5010771e0..6137c2c17ea51 100644 --- a/src/legacy/ui/public/chrome/api/loading_count.js +++ b/src/legacy/ui/public/chrome/api/loading_count.js @@ -18,15 +18,9 @@ */ import * as Rx from 'rxjs'; +import { npSetup } from 'ui/new_platform'; -let newPlatformHttp; - -export function __newPlatformSetup__(instance) { - if (newPlatformHttp) { - throw new Error('ui/chrome/api/loading_count already initialized with new platform apis'); - } - newPlatformHttp = instance; -} +const newPlatformHttp = npSetup.core.http; export function initLoadingCountApi(chrome) { const manualCount$ = new Rx.BehaviorSubject(0); diff --git a/src/legacy/ui/public/chrome/api/nav.ts b/src/legacy/ui/public/chrome/api/nav.ts index 8a4504125d82b..349d49f23eb5b 100644 --- a/src/legacy/ui/public/chrome/api/nav.ts +++ b/src/legacy/ui/public/chrome/api/nav.ts @@ -19,8 +19,8 @@ import { KibanaParsedUrl } from 'ui/url/kibana_parsed_url'; import { absoluteToParsedUrl } from '../../url/absolute_to_parsed_url'; -import { onStart } from '../../new_platform'; -import { ChromeStart, ChromeNavLink } from '../../../../../core/public'; +import { npStart } from '../../new_platform'; +import { ChromeNavLink } from '../../../../../core/public'; import { relativeToAbsolute } from '../../url/relative_to_absolute'; export interface ChromeNavLinks { @@ -34,8 +34,7 @@ interface NavInternals { } export function initChromeNavApi(chrome: any, internals: NavInternals) { - let coreNavLinks: ChromeStart['navLinks']; - onStart(({ core }) => (coreNavLinks = core.chrome.navLinks)); + const coreNavLinks = npStart.core.chrome.navLinks; /** * Clear last url for deleted saved objects to avoid loading pages with "Could not locate..." @@ -145,15 +144,14 @@ export function initChromeNavApi(chrome: any, internals: NavInternals) { // simulate a possible change in url to initialize the // link.active and link.lastUrl properties - onStart(({ core }) => { - core.chrome.navLinks - .getAll() - .filter(link => link.subUrlBase) - .forEach(link => { - core.chrome.navLinks.update(link.id, { - subUrlBase: relativeToAbsolute(chrome.addBasePath(link.subUrlBase)), - }); + coreNavLinks + .getAll() + .filter(link => link.subUrlBase) + .forEach(link => { + coreNavLinks.update(link.id, { + subUrlBase: relativeToAbsolute(chrome.addBasePath(link.subUrlBase)), }); - internals.trackPossibleSubUrl(document.location.href); - }); + }); + + internals.trackPossibleSubUrl(document.location.href); } diff --git a/src/legacy/ui/public/chrome/api/sub_url_hooks.js b/src/legacy/ui/public/chrome/api/sub_url_hooks.js index 5ed3af9b59ff6..142f11e029b35 100644 --- a/src/legacy/ui/public/chrome/api/sub_url_hooks.js +++ b/src/legacy/ui/public/chrome/api/sub_url_hooks.js @@ -23,7 +23,6 @@ import { getUnhashableStatesProvider, unhashUrl, } from '../../state_management/state_hashing'; -import { onStart } from '../../new_platform'; export function registerSubUrlHooks(angularModule, internals) { angularModule.run(($rootScope, Private, $location) => { @@ -61,7 +60,7 @@ export function registerSubUrlHooks(angularModule, internals) { $rootScope.$on('$routeChangeSuccess', onRouteChange); $rootScope.$on('$routeUpdate', onRouteChange); - onStart(updateSubUrls); // initialize sub urls + updateSubUrls(); }); } diff --git a/src/legacy/ui/public/chrome/api/theme.test.mocks.ts b/src/legacy/ui/public/chrome/api/theme.test.mocks.ts new file mode 100644 index 0000000000000..879942a9aea82 --- /dev/null +++ b/src/legacy/ui/public/chrome/api/theme.test.mocks.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { chromeServiceMock } from '../../../../../core/public/mocks'; + +export const newPlatformChrome = chromeServiceMock.createSetupContract(); +jest.doMock('ui/new_platform', () => ({ + npSetup: { + core: { chrome: newPlatformChrome }, + }, +})); diff --git a/src/legacy/ui/public/chrome/api/theme.test.ts b/src/legacy/ui/public/chrome/api/theme.test.ts index 7bee075809b80..1e829329f9680 100644 --- a/src/legacy/ui/public/chrome/api/theme.test.ts +++ b/src/legacy/ui/public/chrome/api/theme.test.ts @@ -19,12 +19,8 @@ import * as Rx from 'rxjs'; -import { chromeServiceMock } from '../../../../../core/public/mocks'; -import { __newPlatformSetup__, initChromeThemeApi } from './theme'; - -const newPlatformChrome = chromeServiceMock.createSetupContract(); - -__newPlatformSetup__(newPlatformChrome); +import { newPlatformChrome } from './theme.test.mocks'; +import { initChromeThemeApi } from './theme'; function setup() { const brand$ = new Rx.BehaviorSubject({ logo: 'foo', smallLogo: 'foo' }); diff --git a/src/legacy/ui/public/chrome/api/theme.ts b/src/legacy/ui/public/chrome/api/theme.ts index dbc12636caf81..9333acb90c269 100644 --- a/src/legacy/ui/public/chrome/api/theme.ts +++ b/src/legacy/ui/public/chrome/api/theme.ts @@ -19,17 +19,10 @@ import * as Rx from 'rxjs'; -import { ChromeBrand, ChromeSetup } from '../../../../../core/public'; +import { npSetup } from 'ui/new_platform'; +import { ChromeBrand } from '../../../../../core/public'; -let newPlatformChrome: ChromeSetup; - -export function __newPlatformSetup__(instance: ChromeSetup) { - if (newPlatformChrome) { - throw new Error('ui/chrome/api/theme is already initialized'); - } - - newPlatformChrome = instance; -} +const newPlatformChrome = npSetup.core.chrome; export function initChromeThemeApi(chrome: { [key: string]: any }) { const brandCache$ = new Rx.BehaviorSubject({}); diff --git a/src/legacy/ui/public/chrome/api/ui_settings.js b/src/legacy/ui/public/chrome/api/ui_settings.js index bce073a643daf..dbdd6a9c12653 100644 --- a/src/legacy/ui/public/chrome/api/ui_settings.js +++ b/src/legacy/ui/public/chrome/api/ui_settings.js @@ -17,15 +17,9 @@ * under the License. */ -let newPlatformUiSettingsClient; +import { npSetup } from 'ui/new_platform'; -export function __newPlatformSetup__(instance) { - if (newPlatformUiSettingsClient) { - throw new Error('ui/chrome/api/ui_settings already initialized'); - } - - newPlatformUiSettingsClient = instance; -} +const newPlatformUiSettingsClient = npSetup.core.uiSettings; export function initUiSettingsApi(chrome) { chrome.getUiSettingsClient = function () { diff --git a/src/legacy/ui/public/chrome/directives/header_global_nav/header_global_nav.js b/src/legacy/ui/public/chrome/directives/header_global_nav/header_global_nav.js index d2e16cf7dad44..b38049730946e 100644 --- a/src/legacy/ui/public/chrome/directives/header_global_nav/header_global_nav.js +++ b/src/legacy/ui/public/chrome/directives/header_global_nav/header_global_nav.js @@ -22,7 +22,7 @@ import { uiModules } from '../../../modules'; import { Header } from './components/header'; import { wrapInI18nContext } from 'ui/i18n'; import { chromeHeaderNavControlsRegistry } from 'ui/registry/chrome_header_nav_controls'; -import { getNewPlatform } from '../../../new_platform'; +import { npStart } from '../../../new_platform'; const module = uiModules.get('kibana'); @@ -30,8 +30,6 @@ module.directive('headerGlobalNav', (reactDirective, chrome, Private, uiCapabili const { recentlyAccessed } = require('ui/persisted_log'); const navControls = Private(chromeHeaderNavControlsRegistry); const homeHref = chrome.addBasePath('/app/kibana#/home'); - const newPlatform = getNewPlatform(); - const newPlatformStart = newPlatform.start.core; return reactDirective(wrapInI18nContext(Header), [ // scope accepted by directive, passed in as React props @@ -44,9 +42,9 @@ module.directive('headerGlobalNav', (reactDirective, chrome, Private, uiCapabili badge$: chrome.badge.get$(), breadcrumbs$: chrome.breadcrumbs.get$(), helpExtension$: chrome.helpExtension.get$(), - navLinks$: newPlatformStart.chrome.navLinks.getNavLinks$(), + navLinks$: npStart.core.chrome.navLinks.getNavLinks$(), recentlyAccessed$: recentlyAccessed.get$(), - forceAppSwitcherNavigation$: newPlatformStart.chrome.navLinks.getForceAppSwitcherNavigation$(), + forceAppSwitcherNavigation$: npStart.core.chrome.navLinks.getForceAppSwitcherNavigation$(), navControls, homeHref, uiCapabilities, diff --git a/src/legacy/ui/public/chrome/directives/kbn_chrome.js b/src/legacy/ui/public/chrome/directives/kbn_chrome.js index 5bc3d8a28bac8..0f762f1c2eb79 100644 --- a/src/legacy/ui/public/chrome/directives/kbn_chrome.js +++ b/src/legacy/ui/public/chrome/directives/kbn_chrome.js @@ -22,6 +22,7 @@ import ReactDOM from 'react-dom'; import $ from 'jquery'; import { uiModules } from '../../modules'; +import template from './kbn_chrome.html'; import { notify, @@ -38,7 +39,7 @@ export function kbnChromeProvider(chrome, internals) { .directive('kbnChrome', () => { return { template() { - const $content = $(require('./kbn_chrome.html')); + const $content = $(template); const $app = $content.find('.application'); if (internals.rootController) { diff --git a/src/legacy/ui/public/chrome/services/global_nav_state.js b/src/legacy/ui/public/chrome/services/global_nav_state.js index a3ee7492cbf1e..3bd44b6e58528 100644 --- a/src/legacy/ui/public/chrome/services/global_nav_state.js +++ b/src/legacy/ui/public/chrome/services/global_nav_state.js @@ -18,16 +18,10 @@ */ import { distinctUntilChanged } from 'rxjs/operators'; +import { npSetup } from 'ui/new_platform'; import { uiModules } from '../../modules'; -let newPlatformChrome; -export function __newPlatformSetup__(instance) { - if (newPlatformChrome) { - throw new Error('ui/chrome/global_nav_state is already initialized'); - } - - newPlatformChrome = instance; -} +const newPlatformChrome = npSetup.core.chrome; uiModules.get('kibana') .service('globalNavState', ($rootScope) => { diff --git a/src/legacy/ui/public/courier/fetch/call_response_handlers.js b/src/legacy/ui/public/courier/fetch/call_response_handlers.js index bc3f43fb8eddf..112de6c54ddf0 100644 --- a/src/legacy/ui/public/courier/fetch/call_response_handlers.js +++ b/src/legacy/ui/public/courier/fetch/call_response_handlers.js @@ -23,7 +23,7 @@ import { RequestStatus } from './req_status'; import { SearchError } from '../search_strategy/search_error'; import { i18n } from '@kbn/i18n'; -export function CallResponseHandlersProvider(Private, Promise) { +export function CallResponseHandlersProvider(Promise) { const ABORTED = RequestStatus.ABORTED; const INCOMPLETE = RequestStatus.INCOMPLETE; diff --git a/src/legacy/ui/public/directives/field_name.js b/src/legacy/ui/public/directives/field_name.js index bc8a2932ed181..6ca0c329b09e7 100644 --- a/src/legacy/ui/public/directives/field_name.js +++ b/src/legacy/ui/public/directives/field_name.js @@ -18,6 +18,7 @@ */ import $ from 'jquery'; +import { i18n } from '@kbn/i18n'; import { template } from 'lodash'; import { shortenDottedString } from '../../../core_plugins/kibana/common/utils/shorten_dotted_string'; import booleanFieldNameIcon from './field_name_icons/boolean_field_name_icon.html'; @@ -45,7 +46,7 @@ const compiledSourceFieldNameIcon = template(sourceFieldNameIcon); const compiledStringFieldNameIcon = template(stringFieldNameIcon); const compiledUnknownFieldNameIcon = template(unknownFieldNameIcon); -module.directive('fieldName', function ($compile, $rootScope, config, i18n) { +module.directive('fieldName', function ($rootScope, config) { return { restrict: 'AE', scope: { @@ -56,47 +57,47 @@ module.directive('fieldName', function ($compile, $rootScope, config, i18n) { link: function ($scope, $el) { const typeToIconMap = { boolean: compiledBooleanFieldNameIcon({ - booleanFieldAriaLabel: i18n('common.ui.directives.fieldNameIcons.booleanAriaLabel', { + booleanFieldAriaLabel: i18n.translate('common.ui.directives.fieldNameIcons.booleanAriaLabel', { defaultMessage: 'Boolean field' }), }), conflict: compiledConflictFieldNameIcon({ - conflictingFieldAriaLabel: i18n('common.ui.directives.fieldNameIcons.conflictFieldAriaLabel', { + conflictingFieldAriaLabel: i18n.translate('common.ui.directives.fieldNameIcons.conflictFieldAriaLabel', { defaultMessage: 'Conflicting field' }), }), date: compiledDateFieldNameIcon({ - dateFieldAriaLabel: i18n('common.ui.directives.fieldNameIcons.dateFieldAriaLabel', { + dateFieldAriaLabel: i18n.translate('common.ui.directives.fieldNameIcons.dateFieldAriaLabel', { defaultMessage: 'Date field' }), }), geo_point: compiledGeoPointFieldNameIcon({ - geoPointFieldAriaLabel: i18n('common.ui.directives.fieldNameIcons.geoPointFieldAriaLabel', { + geoPointFieldAriaLabel: i18n.translate('common.ui.directives.fieldNameIcons.geoPointFieldAriaLabel', { defaultMessage: 'Date field' }), }), ip: compiledIpFieldNameIcon({ - ipAddressFieldAriaLabel: i18n('common.ui.directives.fieldNameIcons.ipAddressFieldAriaLabel', { + ipAddressFieldAriaLabel: i18n.translate('common.ui.directives.fieldNameIcons.ipAddressFieldAriaLabel', { defaultMessage: 'IP address field' }), }), murmur3: compiledMurmur3FieldNameIcon({ - murmur3FieldAriaLabel: i18n('common.ui.directives.fieldNameIcons.murmur3FieldAriaLabel', { + murmur3FieldAriaLabel: i18n.translate('common.ui.directives.fieldNameIcons.murmur3FieldAriaLabel', { defaultMessage: 'Murmur3 field' }), }), number: compiledNumberFieldNameIcon({ - numberFieldAriaLabel: i18n('common.ui.directives.fieldNameIcons.numberFieldAriaLabel', { + numberFieldAriaLabel: i18n.translate('common.ui.directives.fieldNameIcons.numberFieldAriaLabel', { defaultMessage: 'Number field' }), }), source: compiledSourceFieldNameIcon({ - sourceFieldAriaLabel: i18n('common.ui.directives.fieldNameIcons.sourceFieldAriaLabel', { + sourceFieldAriaLabel: i18n.translate('common.ui.directives.fieldNameIcons.sourceFieldAriaLabel', { defaultMessage: 'Source field' }), }), string: compiledStringFieldNameIcon({ - stringFieldAriaLabel: i18n('common.ui.directives.fieldNameIcons.stringFieldAriaLabel', { + stringFieldAriaLabel: i18n.translate('common.ui.directives.fieldNameIcons.stringFieldAriaLabel', { defaultMessage: 'String field' }), }), @@ -108,7 +109,7 @@ module.directive('fieldName', function ($compile, $rootScope, config, i18n) { } return compiledUnknownFieldNameIcon({ - unknownFieldAriaLabel: i18n('common.ui.directives.fieldNameIcons.unknownFieldAriaLabel', { + unknownFieldAriaLabel: i18n.translate('common.ui.directives.fieldNameIcons.unknownFieldAriaLabel', { defaultMessage: 'Unknown field' }), }); diff --git a/src/legacy/ui/public/directives/paginate.js b/src/legacy/ui/public/directives/paginate.js index 77454eb604ca6..7ecd5fefe6710 100644 --- a/src/legacy/ui/public/directives/paginate.js +++ b/src/legacy/ui/public/directives/paginate.js @@ -18,11 +18,12 @@ */ import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; import { uiModules } from '../modules'; import paginateControlsTemplate from './partials/paginate_controls.html'; uiModules.get('kibana') - .directive('paginate', function ($parse, $compile, i18n) { + .directive('paginate', function ($parse, $compile) { return { restrict: 'E', scope: true, @@ -61,7 +62,7 @@ uiModules.get('kibana') controller: function ($scope, $document) { const self = this; const ALL = 0; - const allSizeTitle = i18n('common.ui.directives.paginate.size.allDropDownOptionLabel', { + const allSizeTitle = i18n.translate('common.ui.directives.paginate.size.allDropDownOptionLabel', { defaultMessage: 'All', }); diff --git a/src/legacy/ui/public/documentation_links/documentation_links.ts b/src/legacy/ui/public/documentation_links/documentation_links.ts index 88513c5746ab1..75cddd6526bbb 100644 --- a/src/legacy/ui/public/documentation_links/documentation_links.ts +++ b/src/legacy/ui/public/documentation_links/documentation_links.ts @@ -100,6 +100,7 @@ export const documentationLinks = { introduction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index-patterns.html`, }, kibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index.html`, + siem: `${ELASTIC_WEBSITE_URL}guide/en/siem/guide/${DOC_LINK_VERSION}/index.html`, query: { luceneQuerySyntax: `${ELASTIC_DOCS}query-dsl-query-string-query.html#query-string-syntax`, queryDsl: `${ELASTIC_DOCS}query-dsl.html`, diff --git a/src/legacy/ui/public/embeddable/embeddable.ts b/src/legacy/ui/public/embeddable/embeddable.ts index 678bd13aebfc8..55ef4f3a6b15a 100644 --- a/src/legacy/ui/public/embeddable/embeddable.ts +++ b/src/legacy/ui/public/embeddable/embeddable.ts @@ -18,6 +18,7 @@ */ import { Adapters } from 'ui/inspector'; +import { StaticIndexPattern } from 'ui/index_patterns'; import { ContainerState } from './types'; export interface EmbeddableMetadata { @@ -25,7 +26,7 @@ export interface EmbeddableMetadata { * Should specify any index pattern the embeddable uses. This will be used by the container to list out * available fields to filter on. */ - indexPatterns?: object[]; + indexPatterns?: StaticIndexPattern[]; /** * The title, or name, of the embeddable. diff --git a/src/legacy/ui/public/embeddable/index.ts b/src/legacy/ui/public/embeddable/index.ts index 9b6f010f893da..0596a08b21772 100644 --- a/src/legacy/ui/public/embeddable/index.ts +++ b/src/legacy/ui/public/embeddable/index.ts @@ -21,4 +21,12 @@ export { EmbeddableFactory, OnEmbeddableStateChanged } from './embeddable_factor export * from './embeddable'; export * from './context_menu_actions'; export { EmbeddableFactoriesRegistryProvider } from './embeddable_factories_registry'; -export { ContainerState, EmbeddableState, Query, Filters, TimeRange, RefreshConfig } from './types'; +export { + ContainerState, + EmbeddableState, + Query, + Filters, + Filter, + TimeRange, + RefreshConfig, +} from './types'; diff --git a/src/legacy/ui/public/embeddable/types.ts b/src/legacy/ui/public/embeddable/types.ts index c29e75c2211cb..2b06bea9d8537 100644 --- a/src/legacy/ui/public/embeddable/types.ts +++ b/src/legacy/ui/public/embeddable/types.ts @@ -17,6 +17,11 @@ * under the License. */ +import { Filter } from '@kbn/es-query'; + +// Should go away soon once everyone imports from kbn/es-query +export { Filter } from '@kbn/es-query'; + export interface TimeRange { to: string; from: string; @@ -31,12 +36,6 @@ export interface FilterMeta { disabled: boolean; } -// TODO: Filter object representation needs to be fleshed out. -export interface Filter { - meta: FilterMeta; - query: object; -} - export type Filters = Filter[]; export enum QueryLanguageType { @@ -44,10 +43,13 @@ export enum QueryLanguageType { LUCENE = 'lucene', } +// It's a string sometimes in old version formats, before Kuery came along and there +// was the language specifier. export interface Query { language: QueryLanguageType; query: string; } + export interface EmbeddableCustomization { [key: string]: object | string; } @@ -58,7 +60,7 @@ export interface ContainerState { timeRange: TimeRange; - filters: Filters; + filters: Filter[]; refreshConfig: RefreshConfig; diff --git a/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.test.js b/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.test.js index 6fcbacede2a65..a8f6318090b1d 100644 --- a/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.test.js +++ b/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.test.js @@ -18,17 +18,13 @@ */ // @ts-ignore +import './error_auto_create_index.test.mocks'; import fetchMock from 'fetch-mock/es5/client'; -import { __newPlatformSetup__, kfetch } from '../kfetch'; -import { setup } from '../../../../test_utils/public/kfetch_test_setup'; +import { kfetch } from '../kfetch'; import { isAutoCreateIndexError } from './error_auto_create_index'; describe('isAutoCreateIndexError correctly handles KFetchError thrown by kfetch', () => { - beforeAll(() => { - __newPlatformSetup__(setup().http); - }); - describe('404', () => { beforeEach(() => { fetchMock.post({ @@ -77,7 +73,9 @@ describe('isAutoCreateIndexError correctly handles KFetchError thrown by kfetch' matcher: '*', response: { body: { - code: 'ES_AUTO_CREATE_INDEX_ERROR', + attributes: { + code: 'ES_AUTO_CREATE_INDEX_ERROR', + }, }, status: 503, }, diff --git a/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.test.mocks.js b/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.test.mocks.js new file mode 100644 index 0000000000000..956fac78d5b0a --- /dev/null +++ b/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.test.mocks.js @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + + +import { setup } from '../../../../test_utils/public/http_test_setup'; + +jest.doMock('ui/new_platform', () => ({ npSetup: { core: setup() } })); diff --git a/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.ts b/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.ts index e5287f18e1f7a..09c6bfd93148f 100644 --- a/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.ts +++ b/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.ts @@ -37,7 +37,8 @@ uiRoutes.when('/error/action.auto_create_index', { export function isAutoCreateIndexError(error: object) { return ( - get(error, 'res.status') === 503 && get(error, 'body.code') === 'ES_AUTO_CREATE_INDEX_ERROR' + get(error, 'res.status') === 503 && + get(error, 'body.attributes.code') === 'ES_AUTO_CREATE_INDEX_ERROR' ); } diff --git a/src/legacy/ui/public/error_url_overflow/error_url_overflow.js b/src/legacy/ui/public/error_url_overflow/error_url_overflow.js index 46dad9b32570d..bce43e877f7be 100644 --- a/src/legacy/ui/public/error_url_overflow/error_url_overflow.js +++ b/src/legacy/ui/public/error_url_overflow/error_url_overflow.js @@ -22,7 +22,7 @@ import uiRoutes from '../routes'; import { KbnUrlProvider } from '../url'; import template from './error_url_overflow.html'; -import { UrlOverflowServiceProvider } from './url_overflow_service'; +import { UrlOverflowService } from './url_overflow_service'; export * from './url_overflow_service'; @@ -32,9 +32,9 @@ uiRoutes k7Breadcrumbs: () => [{ text: i18n.translate('common.ui.errorUrlOverflow.breadcrumbs.errorText', { defaultMessage: 'Error' }) }], controllerAs: 'controller', controller: class OverflowController { - constructor(Private, config, $scope) { + constructor(Private, $scope) { const kbnUrl = Private(KbnUrlProvider); - const urlOverflow = Private(UrlOverflowServiceProvider); + const urlOverflow = new UrlOverflowService(); if (!urlOverflow.get()) { kbnUrl.redirectPath('/'); diff --git a/src/legacy/ui/public/error_url_overflow/index.js b/src/legacy/ui/public/error_url_overflow/index.js index b3e08fd89c76e..c5b13182a8288 100644 --- a/src/legacy/ui/public/error_url_overflow/index.js +++ b/src/legacy/ui/public/error_url_overflow/index.js @@ -18,4 +18,4 @@ */ import './error_url_overflow'; -export { UrlOverflowServiceProvider } from './url_overflow_service'; +export { UrlOverflowService } from './url_overflow_service'; diff --git a/src/legacy/ui/public/error_url_overflow/url_overflow_service.js b/src/legacy/ui/public/error_url_overflow/url_overflow_service.js index 4e4b2ad634f50..bb0df4ee63f92 100644 --- a/src/legacy/ui/public/error_url_overflow/url_overflow_service.js +++ b/src/legacy/ui/public/error_url_overflow/url_overflow_service.js @@ -78,7 +78,3 @@ export class UrlOverflowService { this._sync(); } } - -export function UrlOverflowServiceProvider() { - return new UrlOverflowService(); -} diff --git a/src/legacy/ui/public/fancy_forms/kbn_form_controller.js b/src/legacy/ui/public/fancy_forms/kbn_form_controller.js index 8d51b0712c6cd..ee6a1cb877839 100644 --- a/src/legacy/ui/public/fancy_forms/kbn_form_controller.js +++ b/src/legacy/ui/public/fancy_forms/kbn_form_controller.js @@ -17,7 +17,9 @@ * under the License. */ -export function decorateFormController($delegate, $injector, i18n) { +import { i18n } from '@kbn/i18n'; + +export function decorateFormController($delegate, $injector) { const [directive] = $delegate; const FormController = directive.controller; @@ -52,7 +54,7 @@ export function decorateFormController($delegate, $injector, i18n) { describeErrors() { const count = this.softErrorCount(); - return i18n('common.ui.fancyForm.errorDescription', + return i18n.translate('common.ui.fancyForm.errorDescription', { defaultMessage: '{count, plural, one {# Error} other {# Errors}}', values: { count } @@ -99,3 +101,4 @@ export function decorateFormController($delegate, $injector, i18n) { return $delegate; } + diff --git a/src/legacy/ui/public/field_editor/field_editor.js b/src/legacy/ui/public/field_editor/field_editor.js index 93a1cee6a7ef7..648325def5e04 100644 --- a/src/legacy/ui/public/field_editor/field_editor.js +++ b/src/legacy/ui/public/field_editor/field_editor.js @@ -140,7 +140,7 @@ export class FieldEditorComponent extends PureComponent { const fieldTypes = get(FIELD_TYPES_BY_LANG, field.lang, DEFAULT_FIELD_TYPES); field.type = fieldTypes.includes(field.type) ? field.type : fieldTypes[0]; - const DefaultFieldFormat = fieldFormats.getDefaultType(field.type); + const DefaultFieldFormat = fieldFormats.getDefaultType(field.type, field.esTypes); const fieldTypeFormats = [ getDefaultFormat(DefaultFieldFormat), ...fieldFormats.byFieldType[field.type], diff --git a/src/legacy/ui/public/filter_bar/__tests__/query_filter.js b/src/legacy/ui/public/filter_bar/__tests__/query_filter.js deleted file mode 100644 index 9e5d3b8973c09..0000000000000 --- a/src/legacy/ui/public/filter_bar/__tests__/query_filter.js +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import './_get_filters'; -import './_add_filters'; -import './_remove_filters'; -import './_toggle_filters'; -import './_invert_filters'; -import './_pin_filters'; -import { FilterBarQueryFilterProvider } from '../query_filter'; -import { EventsProvider } from '../../events'; -let queryFilter; -let EventEmitter; - -describe('Query Filter', function () { - describe('Module', function () { - beforeEach(ngMock.module('kibana')); - beforeEach(ngMock.inject(function (_$rootScope_, Private) { - queryFilter = Private(FilterBarQueryFilterProvider); - EventEmitter = Private(EventsProvider); - })); - - describe('module instance', function () { - it('should be an event emitter', function () { - expect(queryFilter).to.be.an(EventEmitter); - }); - }); - - describe('module methods', function () { - it('should have methods for getting filters', function () { - expect(queryFilter.getFilters).to.be.a('function'); - expect(queryFilter.getAppFilters).to.be.a('function'); - expect(queryFilter.getGlobalFilters).to.be.a('function'); - }); - - it('should have methods for modifying filters', function () { - expect(queryFilter.addFilters).to.be.a('function'); - expect(queryFilter.toggleFilter).to.be.a('function'); - expect(queryFilter.toggleAll).to.be.a('function'); - expect(queryFilter.removeFilter).to.be.a('function'); - expect(queryFilter.removeAll).to.be.a('function'); - expect(queryFilter.invertFilter).to.be.a('function'); - expect(queryFilter.invertAll).to.be.a('function'); - expect(queryFilter.pinFilter).to.be.a('function'); - expect(queryFilter.pinAll).to.be.a('function'); - }); - - }); - - }); - - describe('Actions', function () { - }); -}); diff --git a/src/legacy/ui/public/filter_bar/directive.js b/src/legacy/ui/public/filter_bar/directive.js deleted file mode 100644 index 1178e69df2c34..0000000000000 --- a/src/legacy/ui/public/filter_bar/directive.js +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import 'ngreact'; -import { wrapInI18nContext } from 'ui/i18n'; -import { uiModules } from '../modules'; -import { FilterBar } from './filter_bar'; - -const app = uiModules.get('app/kibana', ['react']); - -app.directive('filterBar', reactDirective => { - return reactDirective(wrapInI18nContext(FilterBar)); -}); diff --git a/src/legacy/ui/public/filter_bar/filter_editor/index.tsx b/src/legacy/ui/public/filter_bar/filter_editor/index.tsx deleted file mode 100644 index 71c1f96a628c7..0000000000000 --- a/src/legacy/ui/public/filter_bar/filter_editor/index.tsx +++ /dev/null @@ -1,472 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { - EuiButton, - EuiButtonEmpty, - // @ts-ignore - EuiCodeEditor, - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiForm, - EuiFormRow, - EuiPopoverTitle, - EuiSpacer, - EuiSwitch, -} from '@elastic/eui'; -import { FieldFilter, Filter } from '@kbn/es-query'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import { get } from 'lodash'; -import React, { Component } from 'react'; -import { Field, IndexPattern } from 'ui/index_patterns'; -import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box'; -import { - buildCustomFilter, - buildFilter, - getFieldFromFilter, - getFilterableFields, - getFilterParams, - getIndexPatternFromFilter, - getOperatorFromFilter, - getOperatorOptions, - getQueryDslFromFilter, - isFilterValid, -} from './lib/filter_editor_utils'; -import { Operator } from './lib/filter_operators'; -import { PhraseValueInput } from './phrase_value_input'; -import { PhrasesValuesInput } from './phrases_values_input'; -import { RangeValueInput } from './range_value_input'; - -interface Props { - filter: Filter; - indexPatterns: IndexPattern[]; - onSubmit: (filter: Filter) => void; - onCancel: () => void; - intl: InjectedIntl; -} - -interface State { - selectedIndexPattern?: IndexPattern; - selectedField?: Field; - selectedOperator?: Operator; - params: any; - useCustomLabel: boolean; - customLabel: string | null; - queryDsl: string; - isCustomEditorOpen: boolean; -} - -class FilterEditorUI extends Component { - public constructor(props: Props) { - super(props); - this.state = { - selectedIndexPattern: this.getIndexPatternFromFilter(), - selectedField: this.getFieldFromFilter(), - selectedOperator: this.getSelectedOperator(), - params: getFilterParams(props.filter), - useCustomLabel: props.filter.meta.alias !== null, - customLabel: props.filter.meta.alias, - queryDsl: JSON.stringify(getQueryDslFromFilter(props.filter), null, 2), - isCustomEditorOpen: this.isUnknownFilterType(), - }; - } - - public render() { - return ( -
- - - - - - - - {this.state.isCustomEditorOpen ? ( - - ) : ( - - )} - - - - - -
- - {this.renderIndexPatternInput()} - - {this.state.isCustomEditorOpen ? this.renderCustomEditor() : this.renderRegularEditor()} - - - - - - {this.state.useCustomLabel && ( -
- - - - -
- )} - - - - - - - - - - - - - - - - -
-
-
- ); - } - - private renderIndexPatternInput() { - if (this.props.indexPatterns.length <= 1) { - return ''; - } - const { selectedIndexPattern } = this.state; - return ( - - - - indexPattern.title} - onChange={this.onIndexPatternChange} - singleSelection={{ asPlainText: true }} - isClearable={false} - data-test-subj="filterIndexPatternsSelect" - /> - - - - ); - } - - private renderRegularEditor() { - return ( -
- - {this.renderFieldInput()} - {this.renderOperatorInput()} - - -
{this.renderParamsEditor()}
-
- ); - } - - private renderFieldInput() { - const { selectedIndexPattern, selectedField } = this.state; - const fields = selectedIndexPattern ? getFilterableFields(selectedIndexPattern) : []; - return ( - - field.name} - onChange={this.onFieldChange} - singleSelection={{ asPlainText: true }} - isClearable={false} - data-test-subj="filterFieldSuggestionList" - /> - - ); - } - - private renderOperatorInput() { - const { selectedField, selectedOperator } = this.state; - const operators = selectedField ? getOperatorOptions(selectedField) : []; - return ( - - message} - onChange={this.onOperatorChange} - singleSelection={{ asPlainText: true }} - isClearable={false} - data-test-subj="filterOperatorList" - /> - - ); - } - - private renderCustomEditor() { - return ( - - - - ); - } - - private renderParamsEditor() { - const indexPattern = this.state.selectedIndexPattern; - if (!indexPattern || !this.state.selectedOperator) { - return ''; - } - - switch (this.state.selectedOperator.type) { - case 'exists': - return ''; - case 'phrase': - return ( - - ); - case 'phrases': - return ( - - ); - case 'range': - return ( - - ); - } - } - - private toggleCustomEditor = () => { - const isCustomEditorOpen = !this.state.isCustomEditorOpen; - this.setState({ isCustomEditorOpen }); - }; - - private isUnknownFilterType() { - const { type } = this.props.filter.meta; - return !!type && !['phrase', 'phrases', 'range', 'exists'].includes(type); - } - - private getIndexPatternFromFilter() { - return getIndexPatternFromFilter(this.props.filter, this.props.indexPatterns); - } - - private getFieldFromFilter() { - const indexPattern = this.getIndexPatternFromFilter(); - return indexPattern && getFieldFromFilter(this.props.filter as FieldFilter, indexPattern); - } - - private getSelectedOperator() { - return getOperatorFromFilter(this.props.filter); - } - - private isFilterValid() { - const { - isCustomEditorOpen, - queryDsl, - selectedIndexPattern: indexPattern, - selectedField: field, - selectedOperator: operator, - params, - } = this.state; - - if (isCustomEditorOpen) { - try { - return Boolean(JSON.parse(queryDsl)); - } catch (e) { - return false; - } - } - - return isFilterValid(indexPattern, field, operator, params); - } - - private onIndexPatternChange = ([selectedIndexPattern]: IndexPattern[]) => { - const selectedField = undefined; - const selectedOperator = undefined; - const params = undefined; - this.setState({ selectedIndexPattern, selectedField, selectedOperator, params }); - }; - - private onFieldChange = ([selectedField]: Field[]) => { - const selectedOperator = undefined; - const params = undefined; - this.setState({ selectedField, selectedOperator, params }); - }; - - private onOperatorChange = ([selectedOperator]: Operator[]) => { - // Only reset params when the operator type changes - const params = - get(this.state.selectedOperator, 'type') === get(selectedOperator, 'type') - ? this.state.params - : undefined; - this.setState({ selectedOperator, params }); - }; - - private onCustomLabelSwitchChange = (event: React.ChangeEvent) => { - const useCustomLabel = event.target.checked; - const customLabel = event.target.checked ? '' : null; - this.setState({ useCustomLabel, customLabel }); - }; - - private onCustomLabelChange = (event: React.ChangeEvent) => { - const customLabel = event.target.value; - this.setState({ customLabel }); - }; - - private onParamsChange = (params: any) => { - this.setState({ params }); - }; - - private onQueryDslChange = (queryDsl: string) => { - this.setState({ queryDsl }); - }; - - private onSubmit = () => { - const { - selectedIndexPattern: indexPattern, - selectedField: field, - selectedOperator: operator, - params, - useCustomLabel, - customLabel, - isCustomEditorOpen, - queryDsl, - } = this.state; - - const { store } = this.props.filter.$state; - const alias = useCustomLabel ? customLabel : null; - - if (isCustomEditorOpen) { - const { index, disabled, negate } = this.props.filter.meta; - const newIndex = index || this.props.indexPatterns[0].id; - const body = JSON.parse(queryDsl); - const filter = buildCustomFilter(newIndex, body, disabled, negate, alias, store); - this.props.onSubmit(filter); - } else if (indexPattern && field && operator) { - const filter = buildFilter(indexPattern, field, operator, params, alias, store); - this.props.onSubmit(filter); - } - }; -} - -function IndexPatternComboBox(props: GenericComboBoxProps) { - return GenericComboBox(props); -} - -function FieldComboBox(props: GenericComboBoxProps) { - return GenericComboBox(props); -} - -function OperatorComboBox(props: GenericComboBoxProps) { - return GenericComboBox(props); -} - -export const FilterEditor = injectI18n(FilterEditorUI); diff --git a/src/legacy/ui/public/filter_bar/filter_view/index.tsx b/src/legacy/ui/public/filter_bar/filter_view/index.tsx deleted file mode 100644 index f62ca8dab5a80..0000000000000 --- a/src/legacy/ui/public/filter_bar/filter_view/index.tsx +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { EuiBadge } from '@elastic/eui'; -import { Filter, isFilterPinned } from '@kbn/es-query'; -import { i18n } from '@kbn/i18n'; -import React, { SFC } from 'react'; -import { existsOperator, isOneOfOperator } from 'ui/filter_bar/filter_editor/lib/filter_operators'; - -interface Props { - filter: Filter; - [propName: string]: any; -} - -export const FilterView: SFC = ({ filter, ...rest }: Props) => { - let title = `Filter: ${getFilterDisplayText(filter)}. ${i18n.translate( - 'common.ui.filterBar.moreFilterActionsMessage', - { - defaultMessage: 'Select for more filter actions.', - } - )}`; - - if (isFilterPinned(filter)) { - title = `${i18n.translate('common.ui.filterBar.pinnedFilterPrefix', { - defaultMessage: 'Pinned', - })} ${title}`; - } - if (filter.meta.disabled) { - title = `${i18n.translate('common.ui.filterBar.disabledFilterPrefix', { - defaultMessage: 'Disabled', - })} ${title}`; - } - - return ( - - {getFilterDisplayText(filter)} - - ); -}; - -export function getFilterDisplayText(filter: Filter) { - const prefix = filter.meta.negate - ? ` ${i18n.translate('common.ui.filterBar.negatedFilterPrefix', { - defaultMessage: 'NOT ', - })}` - : ''; - - if (filter.meta.alias !== null) { - return `${prefix}${filter.meta.alias}`; - } - - switch (filter.meta.type) { - case 'exists': - return `${prefix}${filter.meta.key} ${existsOperator.message}`; - case 'geo_bounding_box': - return `${prefix}${filter.meta.key}: ${filter.meta.value}`; - case 'geo_polygon': - return `${prefix}${filter.meta.key}: ${filter.meta.value}`; - case 'phrase': - return `${prefix}${filter.meta.key}: ${filter.meta.value}`; - case 'phrases': - return `${prefix}${filter.meta.key} ${isOneOfOperator.message} ${filter.meta.value}`; - case 'query_string': - return `${prefix}${filter.meta.value}`; - case 'range': - return `${prefix}${filter.meta.key}: ${filter.meta.value}`; - default: - return `${prefix}${JSON.stringify(filter.query)}`; - } -} diff --git a/src/legacy/ui/public/filter_bar/index.ts b/src/legacy/ui/public/filter_bar/index.ts deleted file mode 100644 index cdf49a72e9554..0000000000000 --- a/src/legacy/ui/public/filter_bar/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import './directive'; - -export { FilterBar } from './filter_bar'; diff --git a/src/legacy/ui/public/filter_bar/query_filter.js b/src/legacy/ui/public/filter_bar/query_filter.js deleted file mode 100644 index 736e52d0b1417..0000000000000 --- a/src/legacy/ui/public/filter_bar/query_filter.js +++ /dev/null @@ -1,408 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import { onlyDisabled } from './lib/only_disabled'; -import { onlyStateChanged } from './lib/only_state_changed'; -import { uniqFilters } from './lib/uniq_filters'; -import { compareFilters } from './lib/compare_filters'; -import { EventsProvider } from '../events'; -import { mapAndFlattenFilters } from './lib/map_and_flatten_filters'; -import { extractTimeFilter } from './lib/extract_time_filter'; -import { changeTimeFilter } from './lib/change_time_filter'; - -export function FilterBarQueryFilterProvider(Private, Promise, indexPatterns, $rootScope, getAppState, globalState, config) { - const EventEmitter = Private(EventsProvider); - - const queryFilter = new EventEmitter(); - - queryFilter.getFilters = function () { - const compareOptions = { disabled: true, negate: true }; - const appFilters = queryFilter.getAppFilters(); - const globalFilters = queryFilter.getGlobalFilters(); - - return uniqFilters(globalFilters.concat(appFilters), compareOptions); - }; - - queryFilter.getAppFilters = function () { - const appState = getAppState(); - if (!appState || !appState.filters) return []; - - // Work around for https://github.com/elastic/kibana/issues/5896 - appState.filters = validateStateFilters(appState); - - return (appState.filters) ? _.map(appState.filters, appendStoreType('appState')) : []; - }; - - queryFilter.getGlobalFilters = function () { - if (!globalState.filters) return []; - - // Work around for https://github.com/elastic/kibana/issues/5896 - globalState.filters = validateStateFilters(globalState); - - return _.map(globalState.filters, appendStoreType('globalState')); - }; - - /** - * Adds new filters to the scope and state - * @param {object|array} filters Filter(s) to add - * @param {bool} global Whether the filter should be added to global state - * @returns {Promise} filter map promise - */ - queryFilter.addFilters = function (filters, global) { - - if (global === undefined) { - const configDefault = config.get('filters:pinnedByDefault'); - - if (configDefault === false || configDefault === true) { - global = configDefault; - } - } - - // Determine the state for the new filter (whether to pass the filter through other apps or not) - const appState = getAppState(); - const filterState = (global) ? globalState : appState; - - if (!Array.isArray(filters)) { - filters = [filters]; - } - - return Promise.resolve(mapAndFlattenFilters(indexPatterns, filters)) - .then(function (filters) { - if (!filterState.filters) { - filterState.filters = []; - } - - filterState.filters = filterState.filters.concat(filters); - }); - }; - - /** - * Removes the filter from the proper state - * @param {object} matchFilter The filter to remove - */ - queryFilter.removeFilter = function (matchFilter) { - const appState = getAppState(); - const filter = _.omit(matchFilter, ['$$hashKey']); - let state; - let index; - - // check for filter in appState - if (appState) { - index = _.findIndex(appState.filters, filter); - if (index !== -1) state = appState; - } - - // if not found, check for filter in globalState - if (!state) { - index = _.findIndex(globalState.filters, filter); - if (index !== -1) state = globalState; - else return; // not found in either state, do nothing - } - - state.filters.splice(index, 1); - }; - - /** - * Removes all filters - */ - queryFilter.removeAll = function () { - const appState = getAppState(); - appState.filters = []; - globalState.filters = []; - }; - - /** - * Toggles the filter between enabled/disabled. - * @param {object} filter The filter to toggle - & @param {boolean} force Disabled true/false - * @returns {object} updated filter - */ - queryFilter.toggleFilter = function (filter, force) { - // Toggle the disabled flag - const disabled = _.isUndefined(force) ? !filter.meta.disabled : !!force; - filter.meta.disabled = disabled; - return filter; - }; - - /** - * Disables all filters - * @params {boolean} force Disable/enable all filters - */ - queryFilter.toggleAll = function (force) { - function doToggle(filter) { - queryFilter.toggleFilter(filter, force); - } - - executeOnFilters(doToggle); - }; - - - /** - * Inverts the negate value on the filter - * @param {object} filter The filter to toggle - * @returns {object} updated filter - */ - queryFilter.invertFilter = function (filter) { - // Toggle the negate meta state - filter.meta.negate = !filter.meta.negate; - return filter; - }; - - /** - * Inverts all filters - * @returns {object} Resulting updated filter list - */ - queryFilter.invertAll = function () { - executeOnFilters(queryFilter.invertFilter); - }; - - - /** - * Pins the filter to the global state - * @param {object} filter The filter to pin - * @param {boolean} force pinned state - * @returns {object} updated filter - */ - queryFilter.pinFilter = function (filter, force) { - const appState = getAppState(); - if (!appState) return filter; - - // ensure that both states have a filters property - if (!Array.isArray(globalState.filters)) globalState.filters = []; - if (!Array.isArray(appState.filters)) appState.filters = []; - - const appIndex = _.findIndex(appState.filters, appFilter => _.isEqual(appFilter, filter)); - - if (appIndex !== -1 && force !== false) { - appState.filters.splice(appIndex, 1); - globalState.filters.push(filter); - } else { - const globalIndex = _.findIndex(globalState.filters, globalFilter => _.isEqual(globalFilter, filter)); - - if (globalIndex === -1 || force === true) return filter; - - globalState.filters.splice(globalIndex, 1); - appState.filters.push(filter); - } - - return filter; - }; - - /** - * Pins all filters - * @params {boolean} force Pin/Unpin all filters - */ - queryFilter.pinAll = function (force) { - function pin(filter) { - queryFilter.pinFilter(filter, force); - } - - executeOnFilters(pin); - }; - - queryFilter.setFilters = filters => { - return Promise.resolve(mapAndFlattenFilters(indexPatterns, filters)) - .then(mappedFilters => { - const appState = getAppState(); - const [globalFilters, appFilters] = _.partition(mappedFilters, filter => { - return filter.$state.store === 'globalState'; - }); - globalState.filters = globalFilters; - if (appState) appState.filters = appFilters; - }); - }; - - queryFilter.addFiltersAndChangeTimeFilter = async filters => { - const timeFilter = await extractTimeFilter(indexPatterns, filters); - if (timeFilter) changeTimeFilter(timeFilter); - queryFilter.addFilters(filters.filter(filter => filter !== timeFilter)); - }; - - initWatchers(); - - return queryFilter; - - /** - * Rids filter list of null values and replaces state if any nulls are found - */ - function validateStateFilters(state) { - const compacted = _.compact(state.filters); - if (state.filters.length !== compacted.length) { - state.filters = compacted; - state.replace(); - } - return state.filters; - } - - - /** - * Saves both app and global states, ensuring filters are persisted - * @returns {object} Resulting filter list, app and global combined - */ - function saveState() { - const appState = getAppState(); - if (appState) appState.save(); - globalState.save(); - } - - function appendStoreType(type) { - return function (filter) { - filter.$state = { - store: type - }; - return filter; - }; - } - - // helper to run a function on all filters in all states - function executeOnFilters(fn) { - const appState = getAppState(); - let globalFilters = []; - let appFilters = []; - - if (globalState.filters) globalFilters = globalState.filters; - if (appState && appState.filters) appFilters = appState.filters; - - globalFilters.concat(appFilters).forEach(fn); - } - - function mergeStateFilters(gFilters, aFilters, compareOptions) { - // ensure we don't mutate the filters passed in - const globalFilters = gFilters ? _.cloneDeep(gFilters) : []; - const appFilters = aFilters ? _.cloneDeep(aFilters) : []; - - // existing globalFilters should be mutated by appFilters - _.each(appFilters, function (filter, i) { - const match = _.find(globalFilters, function (globalFilter) { - return compareFilters(globalFilter, filter, compareOptions); - }); - - // no match, do nothing - if (!match) return; - - // matching filter in globalState, update global and remove from appState - _.assign(match.meta, filter.meta); - appFilters.splice(i, 1); - }); - - // Reverse the order of globalFilters and appFilters, since uniqFilters - // will throw out duplicates from the back of the array, but we want - // newer filters to overwrite previously created filters. - globalFilters.reverse(); - appFilters.reverse(); - - return [ - // Reverse filters after uniq again, so they are still in the order, they - // were before updating them - uniqFilters(globalFilters).reverse(), - uniqFilters(appFilters).reverse() - ]; - } - - /** - * Initializes state watchers that use the event emitter - * @returns {void} - */ - function initWatchers() { - let removeAppStateWatchers; - - $rootScope.$watch(getAppState, function () { - removeAppStateWatchers && removeAppStateWatchers(); - removeAppStateWatchers = initAppStateWatchers(); - }); - - function initAppStateWatchers() { - // multi watch on the app and global states - const stateWatchers = [{ - fn: $rootScope.$watch, - deep: true, - get: queryFilter.getGlobalFilters - }, { - fn: $rootScope.$watch, - deep: true, - get: queryFilter.getAppFilters - }]; - - // when states change, use event emitter to trigger updates and fetches - return $rootScope.$watchMulti(stateWatchers, function (next, prev) { - // prevent execution on watcher instantiation - if (_.isEqual(next, prev)) return; - - let doUpdate = false; - let doFetch = false; - - // reconcile filter in global and app states - const filters = mergeStateFilters(next[0], next[1]); - const [globalFilters, appFilters] = filters; - const appState = getAppState(); - - // save the state, as it may have updated - const globalChanged = !_.isEqual(next[0], globalFilters); - const appChanged = !_.isEqual(next[1], appFilters); - - // the filters were changed, apply to state (re-triggers this watcher) - if (globalChanged || appChanged) { - globalState.filters = globalFilters; - if (appState) appState.filters = appFilters; - return; - } - - // check for actions, bail if we're done - getActions(); - if (!doUpdate) return; - - // save states and emit the required events - saveState(); - queryFilter.emit('update') - .then(function () { - if (!doFetch) return; - queryFilter.emit('fetch'); - }); - - // iterate over each state type, checking for changes - function getActions() { - let newFilters = []; - let oldFilters = []; - - stateWatchers.forEach(function (watcher, i) { - const nextVal = next[i]; - const prevVal = prev[i]; - newFilters = newFilters.concat(nextVal); - oldFilters = oldFilters.concat(prevVal); - - // no update or fetch if there was no change - if (nextVal === prevVal) return; - - if (nextVal) doUpdate = true; - - // don't trigger fetch when only disabled filters - if (!onlyDisabled(nextVal, prevVal)) doFetch = true; - }); - - // make sure change wasn't only a state move - // checking length first is an optimization - if (doFetch && newFilters.length === oldFilters.length) { - if (onlyStateChanged(newFilters, oldFilters)) doFetch = false; - } - } - }); - } - } -} diff --git a/src/legacy/ui/public/filter_bar/__tests__/_add_filters.js b/src/legacy/ui/public/filter_manager/__tests__/_add_filters.js similarity index 94% rename from src/legacy/ui/public/filter_bar/__tests__/_add_filters.js rename to src/legacy/ui/public/filter_manager/__tests__/_add_filters.js index 0af048d5227eb..300f5d9520721 100644 --- a/src/legacy/ui/public/filter_bar/__tests__/_add_filters.js +++ b/src/legacy/ui/public/filter_manager/__tests__/_add_filters.js @@ -140,12 +140,15 @@ describe('add filters', function () { }); it('should fire the update and fetch events', async function () { - const emitSpy = sinon.spy(queryFilter, 'emit'); + const updateStub = sinon.stub(); + const fetchStub = sinon.stub(); - const awaitFetch = new Promise(resolve => { - queryFilter.on('fetch', () => { - resolve(); - }); + queryFilter.getUpdates$().subscribe({ + next: updateStub, + }); + + queryFilter.getFetches$().subscribe({ + next: fetchStub, }); // set up the watchers, add new filters, and crank the digest loop @@ -158,10 +161,8 @@ describe('add filters', function () { expect(globalState.save.callCount).to.be(1); // this time, events should be emitted - await awaitFetch; - expect(emitSpy.callCount).to.be(2); - expect(emitSpy.firstCall.args[0]).to.be('update'); - expect(emitSpy.secondCall.args[0]).to.be('fetch'); + expect(fetchStub.called); + expect(updateStub.called); }); }); diff --git a/src/legacy/ui/public/filter_bar/__tests__/_get_filters.js b/src/legacy/ui/public/filter_manager/__tests__/_get_filters.js similarity index 100% rename from src/legacy/ui/public/filter_bar/__tests__/_get_filters.js rename to src/legacy/ui/public/filter_manager/__tests__/_get_filters.js diff --git a/src/legacy/ui/public/filter_bar/__tests__/_invert_filters.js b/src/legacy/ui/public/filter_manager/__tests__/_invert_filters.js similarity index 93% rename from src/legacy/ui/public/filter_bar/__tests__/_invert_filters.js rename to src/legacy/ui/public/filter_manager/__tests__/_invert_filters.js index e705cf22cf7fe..de7222dca7daf 100644 --- a/src/legacy/ui/public/filter_bar/__tests__/_invert_filters.js +++ b/src/legacy/ui/public/filter_manager/__tests__/_invert_filters.js @@ -92,7 +92,16 @@ describe('invert filters', function () { }); it('should fire the update and fetch events', function () { - const emitSpy = sinon.spy(queryFilter, 'emit'); + const updateStub = sinon.stub(); + const fetchStub = sinon.stub(); + + queryFilter.getUpdates$().subscribe({ + next: updateStub, + }); + + queryFilter.getFetches$().subscribe({ + next: fetchStub, + }); appState.filters = filters; // set up the watchers @@ -101,9 +110,8 @@ describe('invert filters', function () { // trigger the digest loop to fire the watchers $rootScope.$digest(); - expect(emitSpy.callCount).to.be(2); - expect(emitSpy.firstCall.args[0]).to.be('update'); - expect(emitSpy.secondCall.args[0]).to.be('fetch'); + expect(fetchStub.called); + expect(updateStub.called); }); }); diff --git a/src/legacy/ui/public/filter_bar/__tests__/_pin_filters.js b/src/legacy/ui/public/filter_manager/__tests__/_pin_filters.js similarity index 94% rename from src/legacy/ui/public/filter_bar/__tests__/_pin_filters.js rename to src/legacy/ui/public/filter_manager/__tests__/_pin_filters.js index 2c9eb4f730ada..8d292bfb28d3a 100644 --- a/src/legacy/ui/public/filter_bar/__tests__/_pin_filters.js +++ b/src/legacy/ui/public/filter_manager/__tests__/_pin_filters.js @@ -122,15 +122,25 @@ describe('pin filters', function () { it('should only fire the update event', function () { - const emitSpy = sinon.spy(queryFilter, 'emit'); + const updateStub = sinon.stub(); + const fetchStub = sinon.stub(); + + queryFilter.getUpdates$().subscribe({ + next: updateStub, + }); + + queryFilter.getFetches$().subscribe({ + next: fetchStub, + }); + const filter = appState.filters[1]; $rootScope.$digest(); queryFilter.pinFilter(filter); $rootScope.$digest(); - expect(emitSpy.callCount).to.be(1); - expect(emitSpy.firstCall.args[0]).to.be('update'); + expect(!fetchStub.called); + expect(updateStub.called); }); }); diff --git a/src/legacy/ui/public/filter_bar/__tests__/_remove_filters.js b/src/legacy/ui/public/filter_manager/__tests__/_remove_filters.js similarity index 93% rename from src/legacy/ui/public/filter_bar/__tests__/_remove_filters.js rename to src/legacy/ui/public/filter_manager/__tests__/_remove_filters.js index a723031f67c4c..2b9e31aa794eb 100644 --- a/src/legacy/ui/public/filter_bar/__tests__/_remove_filters.js +++ b/src/legacy/ui/public/filter_manager/__tests__/_remove_filters.js @@ -85,16 +85,26 @@ describe('remove filters', function () { }); it('should fire the update and fetch events', function () { - const emitSpy = sinon.spy(queryFilter, 'emit'); + const updateStub = sinon.stub(); + const fetchStub = sinon.stub(); + + queryFilter.getUpdates$().subscribe({ + next: updateStub, + }); + + queryFilter.getFetches$().subscribe({ + next: fetchStub, + }); + appState.filters = filters; $rootScope.$digest(); queryFilter.removeFilter(filters[0]); $rootScope.$digest(); - expect(emitSpy.callCount).to.be(2); - expect(emitSpy.firstCall.args[0]).to.be('update'); - expect(emitSpy.secondCall.args[0]).to.be('fetch'); + // this time, events should be emitted + expect(fetchStub.called); + expect(updateStub.called); }); it('should remove matching filters', function () { diff --git a/src/legacy/ui/public/filter_bar/__tests__/_toggle_filters.js b/src/legacy/ui/public/filter_manager/__tests__/_toggle_filters.js similarity index 94% rename from src/legacy/ui/public/filter_bar/__tests__/_toggle_filters.js rename to src/legacy/ui/public/filter_manager/__tests__/_toggle_filters.js index d1eb26641a770..bd7781098259d 100644 --- a/src/legacy/ui/public/filter_bar/__tests__/_toggle_filters.js +++ b/src/legacy/ui/public/filter_manager/__tests__/_toggle_filters.js @@ -91,16 +91,26 @@ describe('toggle filters', function () { }); it('should fire the update and fetch events', function () { - const emitSpy = sinon.spy(queryFilter, 'emit'); + const updateStub = sinon.stub(); + const fetchStub = sinon.stub(); + + queryFilter.getUpdates$().subscribe({ + next: updateStub, + }); + + queryFilter.getFetches$().subscribe({ + next: fetchStub, + }); + appState.filters = filters; $rootScope.$digest(); queryFilter.toggleFilter(filters[1]); $rootScope.$digest(); - expect(emitSpy.callCount).to.be(2); - expect(emitSpy.firstCall.args[0]).to.be('update'); - expect(emitSpy.secondCall.args[0]).to.be('fetch'); + // this time, events should be emitted + expect(fetchStub.called); + expect(updateStub.called); }); it('should always enable the filter', function () { diff --git a/src/legacy/ui/public/filter_manager/__tests__/filter_manager.js b/src/legacy/ui/public/filter_manager/__tests__/filter_manager.js index f1f374e0a7c3e..d33a92c7a0ac3 100644 --- a/src/legacy/ui/public/filter_manager/__tests__/filter_manager.js +++ b/src/legacy/ui/public/filter_manager/__tests__/filter_manager.js @@ -23,7 +23,7 @@ import MockState from 'fixtures/mock_state'; import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import { FilterManagerProvider } from '..'; -import { FilterBarQueryFilterProvider } from '../../filter_bar/query_filter'; +import { FilterBarQueryFilterProvider } from '../../filter_manager/query_filter'; import { getPhraseScript } from '@kbn/es-query'; let queryFilter; let filterManager; diff --git a/src/legacy/ui/public/filter_manager/__tests__/query_filter.js b/src/legacy/ui/public/filter_manager/__tests__/query_filter.js new file mode 100644 index 0000000000000..8ae0f143ff0d1 --- /dev/null +++ b/src/legacy/ui/public/filter_manager/__tests__/query_filter.js @@ -0,0 +1,70 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import ngMock from 'ng_mock'; +import './_get_filters'; +import './_add_filters'; +import './_remove_filters'; +import './_toggle_filters'; +import './_invert_filters'; +import './_pin_filters'; +import { FilterBarQueryFilterProvider } from '../query_filter'; +let queryFilter; + +describe('Query Filter', function () { + describe('Module', function () { + beforeEach(ngMock.module('kibana')); + beforeEach(ngMock.inject(function (_$rootScope_, Private) { + queryFilter = Private(FilterBarQueryFilterProvider); + })); + + describe('module instance', function () { + it('should use observables', function () { + expect(queryFilter.getUpdates$).to.be.a('function'); + expect(queryFilter.getFetches$).to.be.a('function'); + }); + }); + + describe('module methods', function () { + it('should have methods for getting filters', function () { + expect(queryFilter.getFilters).to.be.a('function'); + expect(queryFilter.getAppFilters).to.be.a('function'); + expect(queryFilter.getGlobalFilters).to.be.a('function'); + }); + + it('should have methods for modifying filters', function () { + expect(queryFilter.addFilters).to.be.a('function'); + expect(queryFilter.toggleFilter).to.be.a('function'); + expect(queryFilter.toggleAll).to.be.a('function'); + expect(queryFilter.removeFilter).to.be.a('function'); + expect(queryFilter.removeAll).to.be.a('function'); + expect(queryFilter.invertFilter).to.be.a('function'); + expect(queryFilter.invertAll).to.be.a('function'); + expect(queryFilter.pinFilter).to.be.a('function'); + expect(queryFilter.pinAll).to.be.a('function'); + }); + + }); + + }); + + describe('Actions', function () { + }); +}); diff --git a/src/legacy/ui/public/filter_manager/filter_manager.js b/src/legacy/ui/public/filter_manager/filter_manager.js index 47b2359bb62bd..0699217d9dcd3 100644 --- a/src/legacy/ui/public/filter_manager/filter_manager.js +++ b/src/legacy/ui/public/filter_manager/filter_manager.js @@ -18,7 +18,7 @@ */ import _ from 'lodash'; -import { FilterBarQueryFilterProvider } from '../filter_bar/query_filter'; +import { FilterBarQueryFilterProvider } from '../filter_manager/query_filter'; import { getPhraseScript } from '@kbn/es-query'; // Adds a filter to a passed state diff --git a/src/legacy/ui/public/filter_bar/lib/__tests__/change_time_filter.test.js b/src/legacy/ui/public/filter_manager/lib/__tests__/change_time_filter.test.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/__tests__/change_time_filter.test.js rename to src/legacy/ui/public/filter_manager/lib/__tests__/change_time_filter.test.js diff --git a/src/legacy/ui/public/filter_bar/lib/__tests__/dedup_filters.js b/src/legacy/ui/public/filter_manager/lib/__tests__/dedup_filters.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/__tests__/dedup_filters.js rename to src/legacy/ui/public/filter_manager/lib/__tests__/dedup_filters.js diff --git a/src/legacy/ui/public/filter_bar/lib/__tests__/extract_time_filter.js b/src/legacy/ui/public/filter_manager/lib/__tests__/extract_time_filter.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/__tests__/extract_time_filter.js rename to src/legacy/ui/public/filter_manager/lib/__tests__/extract_time_filter.js diff --git a/src/legacy/ui/public/filter_bar/lib/__tests__/generate_mapping_chain.js b/src/legacy/ui/public/filter_manager/lib/__tests__/generate_mapping_chain.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/__tests__/generate_mapping_chain.js rename to src/legacy/ui/public/filter_manager/lib/__tests__/generate_mapping_chain.js diff --git a/src/legacy/ui/public/filter_bar/lib/__tests__/map_and_flatten_filters.js b/src/legacy/ui/public/filter_manager/lib/__tests__/map_and_flatten_filters.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/__tests__/map_and_flatten_filters.js rename to src/legacy/ui/public/filter_manager/lib/__tests__/map_and_flatten_filters.js diff --git a/src/legacy/ui/public/filter_bar/lib/__tests__/map_default.js b/src/legacy/ui/public/filter_manager/lib/__tests__/map_default.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/__tests__/map_default.js rename to src/legacy/ui/public/filter_manager/lib/__tests__/map_default.js diff --git a/src/legacy/ui/public/filter_bar/lib/__tests__/map_exists.js b/src/legacy/ui/public/filter_manager/lib/__tests__/map_exists.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/__tests__/map_exists.js rename to src/legacy/ui/public/filter_manager/lib/__tests__/map_exists.js diff --git a/src/legacy/ui/public/filter_bar/lib/__tests__/map_filter.js b/src/legacy/ui/public/filter_manager/lib/__tests__/map_filter.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/__tests__/map_filter.js rename to src/legacy/ui/public/filter_manager/lib/__tests__/map_filter.js diff --git a/src/legacy/ui/public/filter_bar/lib/__tests__/map_geo_bounding_box.js b/src/legacy/ui/public/filter_manager/lib/__tests__/map_geo_bounding_box.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/__tests__/map_geo_bounding_box.js rename to src/legacy/ui/public/filter_manager/lib/__tests__/map_geo_bounding_box.js diff --git a/src/legacy/ui/public/filter_bar/lib/__tests__/map_geo_polygon.js b/src/legacy/ui/public/filter_manager/lib/__tests__/map_geo_polygon.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/__tests__/map_geo_polygon.js rename to src/legacy/ui/public/filter_manager/lib/__tests__/map_geo_polygon.js diff --git a/src/legacy/ui/public/filter_bar/lib/__tests__/map_match_all.js b/src/legacy/ui/public/filter_manager/lib/__tests__/map_match_all.js similarity index 97% rename from src/legacy/ui/public/filter_bar/lib/__tests__/map_match_all.js rename to src/legacy/ui/public/filter_manager/lib/__tests__/map_match_all.js index d1b340611b898..8560784da98a9 100644 --- a/src/legacy/ui/public/filter_bar/lib/__tests__/map_match_all.js +++ b/src/legacy/ui/public/filter_manager/lib/__tests__/map_match_all.js @@ -21,7 +21,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import { mapMatchAll } from '../map_match_all'; -describe('ui/filter_bar/lib', function () { +describe('ui/filter_manager/lib', function () { describe('mapMatchAll()', function () { let filter; diff --git a/src/legacy/ui/public/filter_bar/lib/__tests__/map_missing.js b/src/legacy/ui/public/filter_manager/lib/__tests__/map_missing.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/__tests__/map_missing.js rename to src/legacy/ui/public/filter_manager/lib/__tests__/map_missing.js diff --git a/src/legacy/ui/public/filter_bar/lib/__tests__/map_phrase.js b/src/legacy/ui/public/filter_manager/lib/__tests__/map_phrase.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/__tests__/map_phrase.js rename to src/legacy/ui/public/filter_manager/lib/__tests__/map_phrase.js diff --git a/src/legacy/ui/public/filter_bar/lib/__tests__/map_query_string.js b/src/legacy/ui/public/filter_manager/lib/__tests__/map_query_string.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/__tests__/map_query_string.js rename to src/legacy/ui/public/filter_manager/lib/__tests__/map_query_string.js diff --git a/src/legacy/ui/public/filter_bar/lib/__tests__/map_range.js b/src/legacy/ui/public/filter_manager/lib/__tests__/map_range.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/__tests__/map_range.js rename to src/legacy/ui/public/filter_manager/lib/__tests__/map_range.js diff --git a/src/legacy/ui/public/filter_bar/lib/__tests__/only_disabled.js b/src/legacy/ui/public/filter_manager/lib/__tests__/only_disabled.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/__tests__/only_disabled.js rename to src/legacy/ui/public/filter_manager/lib/__tests__/only_disabled.js diff --git a/src/legacy/ui/public/filter_bar/lib/__tests__/uniq_filters.js b/src/legacy/ui/public/filter_manager/lib/__tests__/uniq_filters.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/__tests__/uniq_filters.js rename to src/legacy/ui/public/filter_manager/lib/__tests__/uniq_filters.js diff --git a/src/legacy/ui/public/filter_bar/lib/change_time_filter.js b/src/legacy/ui/public/filter_manager/lib/change_time_filter.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/change_time_filter.js rename to src/legacy/ui/public/filter_manager/lib/change_time_filter.js diff --git a/src/legacy/ui/public/filter_bar/lib/compare_filters.js b/src/legacy/ui/public/filter_manager/lib/compare_filters.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/compare_filters.js rename to src/legacy/ui/public/filter_manager/lib/compare_filters.js diff --git a/src/legacy/ui/public/filter_bar/lib/dedup_filters.js b/src/legacy/ui/public/filter_manager/lib/dedup_filters.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/dedup_filters.js rename to src/legacy/ui/public/filter_manager/lib/dedup_filters.js diff --git a/src/legacy/ui/public/filter_bar/lib/extract_time_filter.js b/src/legacy/ui/public/filter_manager/lib/extract_time_filter.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/extract_time_filter.js rename to src/legacy/ui/public/filter_manager/lib/extract_time_filter.js diff --git a/src/legacy/ui/public/filter_bar/lib/generate_mapping_chain.js b/src/legacy/ui/public/filter_manager/lib/generate_mapping_chain.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/generate_mapping_chain.js rename to src/legacy/ui/public/filter_manager/lib/generate_mapping_chain.js diff --git a/src/legacy/ui/public/filter_bar/lib/map_and_flatten_filters.js b/src/legacy/ui/public/filter_manager/lib/map_and_flatten_filters.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/map_and_flatten_filters.js rename to src/legacy/ui/public/filter_manager/lib/map_and_flatten_filters.js diff --git a/src/legacy/ui/public/filter_bar/lib/map_default.js b/src/legacy/ui/public/filter_manager/lib/map_default.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/map_default.js rename to src/legacy/ui/public/filter_manager/lib/map_default.js diff --git a/src/legacy/ui/public/filter_bar/lib/map_exists.js b/src/legacy/ui/public/filter_manager/lib/map_exists.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/map_exists.js rename to src/legacy/ui/public/filter_manager/lib/map_exists.js diff --git a/src/legacy/ui/public/filter_bar/lib/map_filter.js b/src/legacy/ui/public/filter_manager/lib/map_filter.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/map_filter.js rename to src/legacy/ui/public/filter_manager/lib/map_filter.js diff --git a/src/legacy/ui/public/filter_bar/lib/map_geo_bounding_box.js b/src/legacy/ui/public/filter_manager/lib/map_geo_bounding_box.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/map_geo_bounding_box.js rename to src/legacy/ui/public/filter_manager/lib/map_geo_bounding_box.js diff --git a/src/legacy/ui/public/filter_bar/lib/map_geo_polygon.js b/src/legacy/ui/public/filter_manager/lib/map_geo_polygon.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/map_geo_polygon.js rename to src/legacy/ui/public/filter_manager/lib/map_geo_polygon.js diff --git a/src/legacy/ui/public/filter_bar/lib/map_match_all.js b/src/legacy/ui/public/filter_manager/lib/map_match_all.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/map_match_all.js rename to src/legacy/ui/public/filter_manager/lib/map_match_all.js diff --git a/src/legacy/ui/public/filter_bar/lib/map_missing.js b/src/legacy/ui/public/filter_manager/lib/map_missing.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/map_missing.js rename to src/legacy/ui/public/filter_manager/lib/map_missing.js diff --git a/src/legacy/ui/public/filter_bar/lib/map_phrase.js b/src/legacy/ui/public/filter_manager/lib/map_phrase.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/map_phrase.js rename to src/legacy/ui/public/filter_manager/lib/map_phrase.js diff --git a/src/legacy/ui/public/filter_bar/lib/map_phrases.js b/src/legacy/ui/public/filter_manager/lib/map_phrases.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/map_phrases.js rename to src/legacy/ui/public/filter_manager/lib/map_phrases.js diff --git a/src/legacy/ui/public/filter_bar/lib/map_query_string.js b/src/legacy/ui/public/filter_manager/lib/map_query_string.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/map_query_string.js rename to src/legacy/ui/public/filter_manager/lib/map_query_string.js diff --git a/src/legacy/ui/public/filter_bar/lib/map_range.js b/src/legacy/ui/public/filter_manager/lib/map_range.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/map_range.js rename to src/legacy/ui/public/filter_manager/lib/map_range.js diff --git a/src/legacy/ui/public/filter_bar/lib/only_disabled.js b/src/legacy/ui/public/filter_manager/lib/only_disabled.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/only_disabled.js rename to src/legacy/ui/public/filter_manager/lib/only_disabled.js diff --git a/src/legacy/ui/public/filter_bar/lib/only_state_changed.js b/src/legacy/ui/public/filter_manager/lib/only_state_changed.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/only_state_changed.js rename to src/legacy/ui/public/filter_manager/lib/only_state_changed.js diff --git a/src/legacy/ui/public/filter_bar/lib/uniq_filters.js b/src/legacy/ui/public/filter_manager/lib/uniq_filters.js similarity index 100% rename from src/legacy/ui/public/filter_bar/lib/uniq_filters.js rename to src/legacy/ui/public/filter_manager/lib/uniq_filters.js diff --git a/src/legacy/ui/public/filter_bar/push_filters.js b/src/legacy/ui/public/filter_manager/push_filters.js similarity index 100% rename from src/legacy/ui/public/filter_bar/push_filters.js rename to src/legacy/ui/public/filter_manager/push_filters.js diff --git a/src/legacy/ui/public/filter_bar/query_filter.d.ts b/src/legacy/ui/public/filter_manager/query_filter.d.ts similarity index 100% rename from src/legacy/ui/public/filter_bar/query_filter.d.ts rename to src/legacy/ui/public/filter_manager/query_filter.d.ts diff --git a/src/legacy/ui/public/filter_manager/query_filter.js b/src/legacy/ui/public/filter_manager/query_filter.js new file mode 100644 index 0000000000000..c1c2993aa0394 --- /dev/null +++ b/src/legacy/ui/public/filter_manager/query_filter.js @@ -0,0 +1,416 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; +import { Subject } from 'rxjs'; + +import { onlyDisabled } from './lib/only_disabled'; +import { onlyStateChanged } from './lib/only_state_changed'; +import { uniqFilters } from './lib/uniq_filters'; +import { compareFilters } from './lib/compare_filters'; +import { mapAndFlattenFilters } from './lib/map_and_flatten_filters'; +import { extractTimeFilter } from './lib/extract_time_filter'; +import { changeTimeFilter } from './lib/change_time_filter'; + +import { npSetup } from 'ui/new_platform'; + +export function FilterBarQueryFilterProvider(Promise, indexPatterns, $rootScope, getAppState, globalState) { + const queryFilter = {}; + const { uiSettings } = npSetup.core; + + const update$ = new Subject(); + const fetch$ = new Subject(); + + queryFilter.getUpdates$ = function () { + return update$.asObservable(); + }; + + queryFilter.getFetches$ = function () { + return fetch$.asObservable(); + }; + + queryFilter.getFilters = function () { + const compareOptions = { disabled: true, negate: true }; + const appFilters = queryFilter.getAppFilters(); + const globalFilters = queryFilter.getGlobalFilters(); + + return uniqFilters(globalFilters.concat(appFilters), compareOptions); + }; + + queryFilter.getAppFilters = function () { + const appState = getAppState(); + if (!appState || !appState.filters) return []; + + // Work around for https://github.com/elastic/kibana/issues/5896 + appState.filters = validateStateFilters(appState); + + return (appState.filters) ? _.map(appState.filters, appendStoreType('appState')) : []; + }; + + queryFilter.getGlobalFilters = function () { + if (!globalState.filters) return []; + + // Work around for https://github.com/elastic/kibana/issues/5896 + globalState.filters = validateStateFilters(globalState); + + return _.map(globalState.filters, appendStoreType('globalState')); + }; + + /** + * Adds new filters to the scope and state + * @param {object|array} filters Filter(s) to add + * @param {bool} global Whether the filter should be added to global state + * @returns {Promise} filter map promise + */ + queryFilter.addFilters = function (filters, addToGlobalState) { + if (addToGlobalState === undefined) { + addToGlobalState = uiSettings.get('filters:pinnedByDefault'); + } + + // Determine the state for the new filter (whether to pass the filter through other apps or not) + const appState = getAppState(); + const filterState = addToGlobalState ? globalState : appState; + + if (!Array.isArray(filters)) { + filters = [filters]; + } + + return Promise.resolve(mapAndFlattenFilters(indexPatterns, filters)) + .then(function (filters) { + if (!filterState.filters) { + filterState.filters = []; + } + + filterState.filters = filterState.filters.concat(filters); + }); + }; + + /** + * Removes the filter from the proper state + * @param {object} matchFilter The filter to remove + */ + queryFilter.removeFilter = function (matchFilter) { + const appState = getAppState(); + const filter = _.omit(matchFilter, ['$$hashKey']); + let state; + let index; + + // check for filter in appState + if (appState) { + index = _.findIndex(appState.filters, filter); + if (index !== -1) state = appState; + } + + // if not found, check for filter in globalState + if (!state) { + index = _.findIndex(globalState.filters, filter); + if (index !== -1) state = globalState; + else return; // not found in either state, do nothing + } + + state.filters.splice(index, 1); + }; + + /** + * Removes all filters + */ + queryFilter.removeAll = function () { + const appState = getAppState(); + appState.filters = []; + globalState.filters = []; + }; + + /** + * Toggles the filter between enabled/disabled. + * @param {object} filter The filter to toggle + & @param {boolean} force Disabled true/false + * @returns {object} updated filter + */ + queryFilter.toggleFilter = function (filter, force) { + // Toggle the disabled flag + const disabled = _.isUndefined(force) ? !filter.meta.disabled : !!force; + filter.meta.disabled = disabled; + return filter; + }; + + /** + * Disables all filters + * @params {boolean} force Disable/enable all filters + */ + queryFilter.toggleAll = function (force) { + function doToggle(filter) { + queryFilter.toggleFilter(filter, force); + } + + executeOnFilters(doToggle); + }; + + + /** + * Inverts the negate value on the filter + * @param {object} filter The filter to toggle + * @returns {object} updated filter + */ + queryFilter.invertFilter = function (filter) { + // Toggle the negate meta state + filter.meta.negate = !filter.meta.negate; + return filter; + }; + + /** + * Inverts all filters + * @returns {object} Resulting updated filter list + */ + queryFilter.invertAll = function () { + executeOnFilters(queryFilter.invertFilter); + }; + + + /** + * Pins the filter to the global state + * @param {object} filter The filter to pin + * @param {boolean} force pinned state + * @returns {object} updated filter + */ + queryFilter.pinFilter = function (filter, force) { + const appState = getAppState(); + if (!appState) return filter; + + // ensure that both states have a filters property + if (!Array.isArray(globalState.filters)) globalState.filters = []; + if (!Array.isArray(appState.filters)) appState.filters = []; + + const appIndex = _.findIndex(appState.filters, appFilter => _.isEqual(appFilter, filter)); + + if (appIndex !== -1 && force !== false) { + appState.filters.splice(appIndex, 1); + globalState.filters.push(filter); + } else { + const globalIndex = _.findIndex(globalState.filters, globalFilter => _.isEqual(globalFilter, filter)); + + if (globalIndex === -1 || force === true) return filter; + + globalState.filters.splice(globalIndex, 1); + appState.filters.push(filter); + } + + return filter; + }; + + /** + * Pins all filters + * @params {boolean} force Pin/Unpin all filters + */ + queryFilter.pinAll = function (force) { + function pin(filter) { + queryFilter.pinFilter(filter, force); + } + + executeOnFilters(pin); + }; + + queryFilter.setFilters = filters => { + return Promise.resolve(mapAndFlattenFilters(indexPatterns, filters)) + .then(mappedFilters => { + const appState = getAppState(); + const [globalFilters, appFilters] = _.partition(mappedFilters, filter => { + return filter.$state.store === 'globalState'; + }); + globalState.filters = globalFilters; + if (appState) appState.filters = appFilters; + }); + }; + + queryFilter.addFiltersAndChangeTimeFilter = async filters => { + const timeFilter = await extractTimeFilter(indexPatterns, filters); + if (timeFilter) changeTimeFilter(timeFilter); + queryFilter.addFilters(filters.filter(filter => filter !== timeFilter)); + }; + + initWatchers(); + + return queryFilter; + + /** + * Rids filter list of null values and replaces state if any nulls are found + */ + function validateStateFilters(state) { + const compacted = _.compact(state.filters); + if (state.filters.length !== compacted.length) { + state.filters = compacted; + state.replace(); + } + return state.filters; + } + + + /** + * Saves both app and global states, ensuring filters are persisted + * @returns {object} Resulting filter list, app and global combined + */ + function saveState() { + const appState = getAppState(); + if (appState) appState.save(); + globalState.save(); + } + + function appendStoreType(type) { + return function (filter) { + filter.$state = { + store: type + }; + return filter; + }; + } + + // helper to run a function on all filters in all states + function executeOnFilters(fn) { + const appState = getAppState(); + let globalFilters = []; + let appFilters = []; + + if (globalState.filters) globalFilters = globalState.filters; + if (appState && appState.filters) appFilters = appState.filters; + + globalFilters.concat(appFilters).forEach(fn); + } + + function mergeStateFilters(gFilters, aFilters, compareOptions) { + // ensure we don't mutate the filters passed in + const globalFilters = gFilters ? _.cloneDeep(gFilters) : []; + const appFilters = aFilters ? _.cloneDeep(aFilters) : []; + + // existing globalFilters should be mutated by appFilters + _.each(appFilters, function (filter, i) { + const match = _.find(globalFilters, function (globalFilter) { + return compareFilters(globalFilter, filter, compareOptions); + }); + + // no match, do nothing + if (!match) return; + + // matching filter in globalState, update global and remove from appState + _.assign(match.meta, filter.meta); + appFilters.splice(i, 1); + }); + + // Reverse the order of globalFilters and appFilters, since uniqFilters + // will throw out duplicates from the back of the array, but we want + // newer filters to overwrite previously created filters. + globalFilters.reverse(); + appFilters.reverse(); + + return [ + // Reverse filters after uniq again, so they are still in the order, they + // were before updating them + uniqFilters(globalFilters).reverse(), + uniqFilters(appFilters).reverse() + ]; + } + + /** + * Initializes state watchers that use the event emitter + * @returns {void} + */ + function initWatchers() { + let removeAppStateWatchers; + + $rootScope.$watch(getAppState, function () { + removeAppStateWatchers && removeAppStateWatchers(); + removeAppStateWatchers = initAppStateWatchers(); + }); + + function initAppStateWatchers() { + // multi watch on the app and global states + const stateWatchers = [{ + fn: $rootScope.$watch, + deep: true, + get: queryFilter.getGlobalFilters + }, { + fn: $rootScope.$watch, + deep: true, + get: queryFilter.getAppFilters + }]; + + // when states change, use event emitter to trigger updates and fetches + return $rootScope.$watchMulti(stateWatchers, function (next, prev) { + // prevent execution on watcher instantiation + if (_.isEqual(next, prev)) return; + + let doUpdate = false; + let doFetch = false; + + // reconcile filter in global and app states + const filters = mergeStateFilters(next[0], next[1]); + const [globalFilters, appFilters] = filters; + const appState = getAppState(); + + // save the state, as it may have updated + const globalChanged = !_.isEqual(next[0], globalFilters); + const appChanged = !_.isEqual(next[1], appFilters); + + // the filters were changed, apply to state (re-triggers this watcher) + if (globalChanged || appChanged) { + globalState.filters = globalFilters; + if (appState) appState.filters = appFilters; + return; + } + + // check for actions, bail if we're done + getActions(); + if (doUpdate) { + // save states and emit the required events + saveState(); + update$.next(); + + if (doFetch) { + fetch$.next(); + } + } + + // iterate over each state type, checking for changes + function getActions() { + let newFilters = []; + let oldFilters = []; + + stateWatchers.forEach(function (watcher, i) { + const nextVal = next[i]; + const prevVal = prev[i]; + newFilters = newFilters.concat(nextVal); + oldFilters = oldFilters.concat(prevVal); + + // no update or fetch if there was no change + if (nextVal === prevVal) return; + + if (nextVal) doUpdate = true; + + // don't trigger fetch when only disabled filters + if (!onlyDisabled(nextVal, prevVal)) doFetch = true; + }); + + // make sure change wasn't only a state move + // checking length first is an optimization + if (doFetch && newFilters.length === oldFilters.length) { + if (onlyStateChanged(newFilters, oldFilters)) doFetch = false; + } + } + }); + } + } +} diff --git a/src/legacy/ui/public/filters/__tests__/prop_filter.js b/src/legacy/ui/public/filters/__tests__/prop_filter.js deleted file mode 100644 index ec60121960ecb..0000000000000 --- a/src/legacy/ui/public/filters/__tests__/prop_filter.js +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import { propFilter } from '../_prop_filter'; - -describe('prop filter', function () { - let nameFilter; - - beforeEach(function () { - nameFilter = propFilter('name'); - }); - - function getObjects(...names) { - const count = new Map(); - const objects = []; - - for (const name of names) { - if (!count.has(name)) { - count.set(name, 1); - } - objects.push({ - name: name, - title: `${name} ${count.get(name)}` - }); - count.set(name, count.get(name) + 1); - } - return objects; - } - - it('returns list when no filters are provided', function () { - const objects = getObjects('table', 'table', 'pie'); - expect(nameFilter(objects)).to.eql(objects); - }); - - it('returns list when empty list of filters is provided', function () { - const objects = getObjects('table', 'table', 'pie'); - expect(nameFilter(objects, [])).to.eql(objects); - }); - - it('should keep only the tables', function () { - const objects = getObjects('table', 'table', 'pie'); - expect(nameFilter(objects, 'table')).to.eql(getObjects('table', 'table')); - }); - - it('should support comma-separated values', function () { - const objects = getObjects('table', 'line', 'pie'); - expect(nameFilter(objects, 'table,line')).to.eql(getObjects('table', 'line')); - }); - - it('should support an array of values', function () { - const objects = getObjects('table', 'line', 'pie'); - expect(nameFilter(objects, [ 'table', 'line' ])).to.eql(getObjects('table', 'line')); - }); - - it('should return all objects', function () { - const objects = getObjects('table', 'line', 'pie'); - expect(nameFilter(objects, '*')).to.eql(objects); - }); - - it('should allow negation', function () { - const objects = getObjects('table', 'line', 'pie'); - expect(nameFilter(objects, [ '!line' ])).to.eql(getObjects('table', 'pie')); - }); - - it('should support a function for specifying what should be kept', function () { - const objects = getObjects('table', 'line', 'pie'); - const line = (value) => value === 'line'; - expect(nameFilter(objects, line)).to.eql(getObjects('line')); - }); - - it('gracefully handles a filter function with zero arity', function () { - const objects = getObjects('table', 'line', 'pie'); - const rejectEverything = () => false; - expect(nameFilter(objects, rejectEverything)).to.eql([]); - }); -}); diff --git a/src/legacy/ui/public/filters/_prop_filter.d.ts b/src/legacy/ui/public/filters/_prop_filter.d.ts deleted file mode 100644 index 284619b50fb60..0000000000000 --- a/src/legacy/ui/public/filters/_prop_filter.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -type FilterFunc = (item: I) => boolean; - -export const propFilter: ( - prop: string -) => (list: T[], filters: string[] | string | FilterFunc) => T[]; diff --git a/src/legacy/ui/public/filters/_prop_filter.js b/src/legacy/ui/public/filters/_prop_filter.js deleted file mode 100644 index ff0f48028da30..0000000000000 --- a/src/legacy/ui/public/filters/_prop_filter.js +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { isFunction } from 'lodash'; - -/** - * Filters out a list by a given filter. This is currently used to implement: - * - fieldType filters a list of fields by their type property - * - aggFilter filters a list of aggs by their name property - * - * @returns {function} - the filter function which can be registered with angular - */ -export function propFilter(prop) { - /** - * List filtering function which accepts an array or list of values that a property - * must contain - * - * @param {array} list - array of items to filter - * @param {function|array|string} filters - the values to match against the list - * - if a function, it is expected to take the field property as argument and returns true to keep it. - * - Can be also an array, a single value as a string, or a comma-separated list of items - * @return {array} - the filtered list - */ - return function (list, filters = []) { - if (isFunction(filters)) { - return list.filter((item) => filters(item[prop])); - } - - if (!Array.isArray(filters)) { - filters = filters.split(','); - } - - if (filters.length === 0) { - return list; - } - - if (filters.includes('*')) { - return list; - } - - const options = filters.reduce(function (options, filter) { - let type = 'include'; - let value = filter; - - if (filter.charAt(0) === '!') { - type = 'exclude'; - value = filter.substr(1); - } - - if (!options[type]) options[type] = []; - options[type].push(value); - return options; - }, {}); - - return list.filter(function (item) { - const value = item[prop]; - - const excluded = options.exclude && options.exclude.includes(value); - if (excluded) { - return false; - } - - const included = !options.include || options.include.includes(value); - if (included) { - return true; - } - - return false; - }); - }; -} diff --git a/src/legacy/ui/public/i18n/index.test.tsx b/src/legacy/ui/public/i18n/index.test.tsx index 284f9cf4784ed..afa64fc80221d 100644 --- a/src/legacy/ui/public/i18n/index.test.tsx +++ b/src/legacy/ui/public/i18n/index.test.tsx @@ -21,12 +21,18 @@ import { render } from 'enzyme'; import PropTypes from 'prop-types'; import React from 'react'; -import { __newPlatformSetup__, wrapInI18nContext } from '.'; +jest.mock('ui/new_platform', () => ({ + npSetup: { + core: { + i18n: { Context: ({ children }: any) =>
Context: {children}
}, + }, + }, +})); + +import { wrapInI18nContext } from '.'; describe('ui/i18n', () => { test('renders children and forwards properties', () => { - __newPlatformSetup__(({ children }) =>
Context: {children}
); - const mockPropTypes = { stringProp: PropTypes.string.isRequired, numberProp: PropTypes.number, diff --git a/src/legacy/ui/public/i18n/index.tsx b/src/legacy/ui/public/i18n/index.tsx index afe062a4d76f8..f5c0f01afad14 100644 --- a/src/legacy/ui/public/i18n/index.tsx +++ b/src/legacy/ui/public/i18n/index.tsx @@ -22,16 +22,9 @@ import React from 'react'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; // @ts-ignore import { uiModules } from 'ui/modules'; -import { I18nSetup } from '../../../../core/public'; +import { npSetup } from 'ui/new_platform'; -export let I18nContext: I18nSetup['Context'] = null!; -export function __newPlatformSetup__(context: typeof I18nContext) { - if (I18nContext) { - throw new Error('ui/i18n already initialized with new platform apis'); - } - - I18nContext = context; -} +export const I18nContext = npSetup.core.i18n.Context; export function wrapInI18nContext

(ComponentToWrap: React.ComponentType

) { const ContextWrapper: React.SFC

= props => { diff --git a/src/legacy/ui/public/index_patterns/__tests__/_index_pattern.js b/src/legacy/ui/public/index_patterns/__tests__/_index_pattern.js index 1f6adec7ae256..c525cbf5063a1 100644 --- a/src/legacy/ui/public/index_patterns/__tests__/_index_pattern.js +++ b/src/legacy/ui/public/index_patterns/__tests__/_index_pattern.js @@ -24,7 +24,7 @@ import expect from '@kbn/expect'; import Promise from 'bluebird'; import { DuplicateField } from '../../errors'; import { IndexedArray } from '../../indexed_array'; -import FixturesLogstashFieldsProvider from 'fixtures/logstash_fields'; +import stubbedLogstashFields from 'fixtures/logstash_fields'; import { FixturesStubbedSavedObjectIndexPatternProvider } from 'fixtures/stubbed_saved_object_index_pattern'; import { IndexPatternProvider } from '../_index_pattern'; import NoDigestPromises from 'test_utils/no_digest_promises'; @@ -49,7 +49,7 @@ describe('index pattern', function () { beforeEach(ngMock.module('kibana', StubIndexPatternsApiClientModule)); beforeEach(ngMock.inject(function (Private) { - mockLogstashFields = Private(FixturesLogstashFieldsProvider); + mockLogstashFields = stubbedLogstashFields(); savedObjectsResponse = Private(FixturesStubbedSavedObjectIndexPatternProvider); savedObjectsClient = Private(SavedObjectsClientProvider); diff --git a/src/legacy/ui/public/index_patterns/__tests__/_index_pattern.test.js b/src/legacy/ui/public/index_patterns/__tests__/_index_pattern.test.js index f06fbfca1c37b..3947a9c5fef1b 100644 --- a/src/legacy/ui/public/index_patterns/__tests__/_index_pattern.test.js +++ b/src/legacy/ui/public/index_patterns/__tests__/_index_pattern.test.js @@ -39,11 +39,9 @@ jest.mock('../../utils/mapping_setup', () => ({ })); jest.mock('../../notify', () => ({ - Notifier: jest.fn().mockImplementation(() => ({ - error: jest.fn(), - })), toastNotifications: { addDanger: jest.fn(), + addError: jest.fn(), } })); diff --git a/src/legacy/ui/public/index_patterns/_field.js b/src/legacy/ui/public/index_patterns/_field.js index 6f7ea5a2a66c1..e6d527683cf93 100644 --- a/src/legacy/ui/public/index_patterns/_field.js +++ b/src/legacy/ui/public/index_patterns/_field.js @@ -58,7 +58,7 @@ export function Field(indexPattern, spec) { let format = spec.format; if (!format || !(format instanceof FieldFormat)) { - format = indexPattern.fieldFormatMap[spec.name] || fieldFormats.getDefaultInstance(spec.type); + format = indexPattern.fieldFormatMap[spec.name] || fieldFormats.getDefaultInstance(spec.type, spec.esTypes); } const indexed = !!spec.indexed; diff --git a/src/legacy/ui/public/index_patterns/_format_hit.js b/src/legacy/ui/public/index_patterns/_format_hit.js index 7baf707f52601..a25ec8f2e01cc 100644 --- a/src/legacy/ui/public/index_patterns/_format_hit.js +++ b/src/legacy/ui/public/index_patterns/_format_hit.js @@ -73,7 +73,7 @@ export function formatHit(indexPattern, defaultFormat) { } const val = fieldName === '_source' ? hit._source : indexPattern.flattenHit(hit)[fieldName]; - return partials[fieldName] = convert(hit, val, fieldName); + return convert(hit, val, fieldName); }; return formatHit; diff --git a/src/legacy/ui/public/index_patterns/_get_computed_fields.js b/src/legacy/ui/public/index_patterns/_get_computed_fields.js index 62fa7cbe0b625..6874306286225 100644 --- a/src/legacy/ui/public/index_patterns/_get_computed_fields.js +++ b/src/legacy/ui/public/index_patterns/_get_computed_fields.js @@ -28,12 +28,10 @@ export function getComputedFields() { // Use a docvalue for each date field to ensure standardized formats when working with date fields // indexPattern.flattenHit will override "_source" values when the same field is also defined in "fields" docvalueFields = _.reject(self.fields.byType.date, 'scripted') - .map((dateField) => { - return { - field: dateField.name, - format: 'date_time', - }; - }); + .map((dateField) => ({ + field: dateField.name, + format: dateField.esTypes && dateField.esTypes.indexOf('date_nanos') !== -1 ? 'strict_date_time' : 'date_time', + })); _.each(self.getScriptedFields(), function (field) { scriptFields[field.name] = { diff --git a/src/legacy/ui/public/index_patterns/_index_pattern.js b/src/legacy/ui/public/index_patterns/_index_pattern.js index 18ee649fd3504..b588822cac693 100644 --- a/src/legacy/ui/public/index_patterns/_index_pattern.js +++ b/src/legacy/ui/public/index_patterns/_index_pattern.js @@ -22,7 +22,7 @@ import { SavedObjectNotFound, DuplicateField, IndexPatternMissingIndices } from import angular from 'angular'; import { fieldFormats } from '../registry/field_formats'; import UtilsMappingSetupProvider from '../utils/mapping_setup'; -import { Notifier, toastNotifications } from '../notify'; +import { toastNotifications } from '../notify'; import { getComputedFields } from './_get_computed_fields'; import { formatHit } from './_format_hit'; @@ -46,7 +46,7 @@ export function getRoutes() { const MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS = 3; -export function IndexPatternProvider(Private, config, Promise, confirmModalPromise, kbnUrl) { +export function IndexPatternProvider(Private, config, Promise) { const getConfig = (...args) => config.get(...args); const getIds = Private(IndexPatternsGetProvider)('id'); const fieldsFetcher = Private(FieldsFetcherProvider); @@ -57,7 +57,6 @@ export function IndexPatternProvider(Private, config, Promise, confirmModalPromi const fieldformats = fieldFormats; const type = 'index-pattern'; - const notify = new Notifier(); const configWatchers = new WeakMap(); const mapping = mappingSetup.expandShorthand({ @@ -308,6 +307,11 @@ export function IndexPatternProvider(Private, config, Promise, confirmModalPromi return !!this.timeFieldName && (!this.fields || !!this.getTimeField()); } + isTimeNanosBased() { + const timeField = this.getTimeField(); + return timeField && timeField.esTypes && timeField.esTypes.indexOf('date_nanos') !== -1; + } + isTimeBasedWildcard() { return this.isTimeBased() && this.isWildcard(); } @@ -338,7 +342,7 @@ export function IndexPatternProvider(Private, config, Promise, confirmModalPromi return body; } - async create(allowOverride = false, showOverridePrompt = false) { + async create(allowOverride = false) { const _create = async (duplicateId) => { if (duplicateId) { const duplicatePattern = new IndexPattern(duplicateId); @@ -358,40 +362,9 @@ export function IndexPatternProvider(Private, config, Promise, confirmModalPromi // We found a duplicate but we aren't allowing override, show the warn modal if (!allowOverride) { - const confirmMessage = i18n.translate('common.ui.indexPattern.titleExistsLabel', { values: { title: this.title }, - defaultMessage: 'An index pattern with the title \'{title}\' already exists.' }); - try { - await confirmModalPromise(confirmMessage, { confirmButtonText: 'Go to existing pattern' }); - return kbnUrl.redirect('/management/kibana/index_patterns/{{id}}', { id: potentialDuplicateByTitle.id }); - } catch (err) { - return false; - } - } - - // We can override, but we do not want to see a prompt, so just do it - if (!showOverridePrompt) { - return await _create(potentialDuplicateByTitle.id); - } - - // We can override and we want to prompt for confirmation - try { - await confirmModalPromise( - i18n.translate('common.ui.indexPattern.confirmOverwriteLabel', { values: { title: this.title }, - defaultMessage: 'Are you sure you want to overwrite \'{title}\'?' }), - { - title: i18n.translate('common.ui.indexPattern.confirmOverwriteTitle', { - defaultMessage: 'Overwrite {type}?', - values: { type }, - }), - confirmButtonText: i18n.translate('common.ui.indexPattern.confirmOverwriteButton', { defaultMessage: 'Overwrite' }), - } - ); - } catch (err) { - // They changed their mind return false; } - // Let's do it! return await _create(potentialDuplicateByTitle.id); } @@ -461,7 +434,6 @@ export function IndexPatternProvider(Private, config, Promise, confirmModalPromi return fetchFields(this) .then(() => this.save()) .catch((err) => { - notify.error(err); // https://github.com/elastic/kibana/issues/9224 // This call will attempt to remap fields from the matching // ES index which may not actually exist. In that scenario, @@ -469,9 +441,15 @@ export function IndexPatternProvider(Private, config, Promise, confirmModalPromi // but we do not want to potentially make any pages unusable // so do not rethrow the error here if (err instanceof IndexPatternMissingIndices) { + toastNotifications.addDanger(err.message); return []; } + toastNotifications.addError(err, { + title: i18n.translate('common.ui.indexPattern.fetchFieldErrorTitle', { + defaultMessage: 'Error fetching fields', + }), + }); throw err; }); } diff --git a/src/legacy/ui/public/index_patterns/index_patterns.js b/src/legacy/ui/public/index_patterns/index_patterns.js index 7c03c79b336a1..937f4be337c8d 100644 --- a/src/legacy/ui/public/index_patterns/index_patterns.js +++ b/src/legacy/ui/public/index_patterns/index_patterns.js @@ -69,7 +69,6 @@ export function IndexPatternsProvider(Private, config) { self.getFields = getProvider.multiple; self.fieldsFetcher = Private(FieldsFetcherProvider); self.fieldFormats = fieldFormats; - self.IndexPattern = IndexPattern; } module.service('indexPatterns', Private => Private(IndexPatternsProvider)); diff --git a/src/legacy/ui/public/index_patterns/route_setup/load_default.js b/src/legacy/ui/public/index_patterns/route_setup/load_default.js index 83c737332b8e9..482b975421873 100644 --- a/src/legacy/ui/public/index_patterns/route_setup/load_default.js +++ b/src/legacy/ui/public/index_patterns/route_setup/load_default.js @@ -65,7 +65,7 @@ export default function (opts) { const whenMissingRedirectTo = opts.whenMissingRedirectTo || null; uiRoutes - .addSetupWork(function loadDefaultIndexPattern(Private, Promise, $route, config) { + .addSetupWork(function loadDefaultIndexPattern(Private, $route, config) { const getIds = Private(IndexPatternsGetProvider)('id'); const route = _.get($route, 'current.$$route'); @@ -85,8 +85,8 @@ export default function (opts) { } if (!defined) { - // If there is only one index pattern, set it as default - if (patterns.length === 1) { + // If there is any index pattern created, set the first as default + if (patterns.length >= 1) { defaultId = patterns[0]; config.set('defaultIndex', defaultId); } else { diff --git a/src/legacy/ui/public/inspector/inspector.test.js b/src/legacy/ui/public/inspector/inspector.test.js index dbe84f9960bd7..75b7804d47ffb 100644 --- a/src/legacy/ui/public/inspector/inspector.test.js +++ b/src/legacy/ui/public/inspector/inspector.test.js @@ -29,15 +29,13 @@ jest.mock('./ui/inspector_panel', () => ({ jest.mock('ui/i18n', () => ({ I18nContext: ({ children }) => children })); jest.mock('ui/new_platform', () => ({ - getNewPlatform: () => ({ - start: { - core: { - overlay: { - openFlyout: jest.fn(), - }, - } + npStart: { + core: { + overlay: { + openFlyout: jest.fn(), + }, } - }), + }, })); import { viewRegistry } from './view_registry'; diff --git a/src/legacy/ui/public/inspector/inspector.tsx b/src/legacy/ui/public/inspector/inspector.tsx index 88501841cb868..84e03df440e41 100644 --- a/src/legacy/ui/public/inspector/inspector.tsx +++ b/src/legacy/ui/public/inspector/inspector.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { OverlayRef } from '../../../../core/public'; -import { getNewPlatform } from '../new_platform'; +import { npStart } from '../new_platform'; import { Adapters } from './types'; import { InspectorPanel } from './ui/inspector_panel'; import { viewRegistry } from './view_registry'; @@ -73,7 +73,7 @@ function open(adapters: Adapters, options: InspectorOptions = {}): InspectorSess if an inspector can be shown.`); } - return getNewPlatform().start.core.overlays.openFlyout( + return npStart.core.overlays.openFlyout( , { 'data-test-subj': 'inspectorPanel', diff --git a/src/legacy/ui/public/inspector/ui/__snapshots__/inspector_panel.test.js.snap b/src/legacy/ui/public/inspector/ui/__snapshots__/inspector_panel.test.js.snap index 18adad0d3b1e2..27911b2e86fd5 100644 --- a/src/legacy/ui/public/inspector/ui/__snapshots__/inspector_panel.test.js.snap +++ b/src/legacy/ui/public/inspector/ui/__snapshots__/inspector_panel.test.js.snap @@ -274,40 +274,23 @@ exports[`InspectorPanel should render as expected 1`] = ` size="m" type="arrowDown" > - + /> + any; - -export function __newPlatformSetup__(httpSetup: HttpSetup) { - if (http) { - throw new Error('ui/kfetch already initialized with New Platform APIs'); - } - - http = httpSetup; - kfetchInstance = createKfetch(http); -} +const kfetchInstance = createKfetch(npSetup.core.http); export const kfetch = (options: KFetchOptions, kfetchOptions?: KFetchKibanaOptions) => { return kfetchInstance(options, kfetchOptions); diff --git a/src/legacy/ui/public/kfetch/kfetch.test.mocks.ts b/src/legacy/ui/public/kfetch/kfetch.test.mocks.ts new file mode 100644 index 0000000000000..ea066b3623f13 --- /dev/null +++ b/src/legacy/ui/public/kfetch/kfetch.test.mocks.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { setup } from '../../../../test_utils/public/http_test_setup'; + +jest.doMock('ui/new_platform', () => ({ + npSetup: { + core: setup(), + }, +})); diff --git a/src/legacy/ui/public/kfetch/kfetch.test.ts b/src/legacy/ui/public/kfetch/kfetch.test.ts index 79bca9da1d273..3452892a4b23a 100644 --- a/src/legacy/ui/public/kfetch/kfetch.test.ts +++ b/src/legacy/ui/public/kfetch/kfetch.test.ts @@ -19,18 +19,14 @@ // @ts-ignore import fetchMock from 'fetch-mock/es5/client'; +import './kfetch.test.mocks'; import { readFileSync } from 'fs'; import { join } from 'path'; -import { __newPlatformSetup__, addInterceptor, kfetch, KFetchOptions } from '.'; +import { addInterceptor, kfetch, KFetchOptions } from '.'; import { Interceptor, resetInterceptors, withDefaultOptions } from './kfetch'; import { KFetchError } from './kfetch_error'; -import { setup } from '../../../../test_utils/public/kfetch_test_setup'; describe('kfetch', () => { - beforeAll(() => { - __newPlatformSetup__(setup().http); - }); - afterEach(() => { fetchMock.restore(); resetInterceptors(); @@ -46,7 +42,7 @@ describe('kfetch', () => { fetchMock.get('*', {}); await kfetch({ pathname: '/my/path', headers: { 'Content-Type': 'CustomContentType' } }); expect(fetchMock.lastOptions()!.headers).toMatchObject({ - 'Content-Type': 'CustomContentType', + 'content-type': 'CustomContentType', }); }); @@ -64,9 +60,9 @@ describe('kfetch', () => { }); expect(fetchMock.lastOptions()!.headers).toEqual({ - 'Content-Type': 'application/json', + 'content-type': 'application/json', 'kbn-version': 'kibanaVersion', - myHeader: 'foo', + myheader: 'foo', }); }); @@ -92,11 +88,11 @@ describe('kfetch', () => { fetchMock.get('*', {}); await kfetch({ pathname: '/my/path' }); + expect(fetchMock.lastCall()!.request.credentials).toBe('same-origin'); expect(fetchMock.lastOptions()!).toMatchObject({ method: 'GET', - credentials: 'same-origin', headers: { - 'Content-Type': 'application/json', + 'content-type': 'application/json', 'kbn-version': 'kibanaVersion', }, }); @@ -359,7 +355,7 @@ describe('kfetch', () => { addInterceptor({ request: config => ({ ...config, - addedByRequestInterceptor: true, + pathname: '/my/intercepted-route', }), response: res => ({ ...res, @@ -371,8 +367,8 @@ describe('kfetch', () => { }); it('should modify request', () => { + expect(fetchMock.lastUrl()).toContain('/my/intercepted-route'); expect(fetchMock.lastOptions()!).toMatchObject({ - addedByRequestInterceptor: true, method: 'GET', }); }); @@ -393,7 +389,7 @@ describe('kfetch', () => { request: config => Promise.resolve({ ...config, - addedByRequestInterceptor: true, + pathname: '/my/intercepted-route', }), response: res => Promise.resolve({ @@ -406,8 +402,8 @@ describe('kfetch', () => { }); it('should modify request', () => { + expect(fetchMock.lastUrl()).toContain('/my/intercepted-route'); expect(fetchMock.lastOptions()!).toMatchObject({ - addedByRequestInterceptor: true, method: 'GET', }); }); diff --git a/src/legacy/ui/public/legacy_compat/angular_config.tsx b/src/legacy/ui/public/legacy_compat/angular_config.tsx index 7819593055ef0..7b0d5e2bd2176 100644 --- a/src/legacy/ui/public/legacy_compat/angular_config.tsx +++ b/src/legacy/ui/public/legacy_compat/angular_config.tsx @@ -33,15 +33,15 @@ import * as Rx from 'rxjs'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { CoreSetup } from 'kibana/public'; +import { InternalCoreSetup } from 'kibana/public'; import { fatalError } from 'ui/notify'; import { capabilities } from 'ui/capabilities'; // @ts-ignore import { modifyUrl } from 'ui/url'; // @ts-ignore -import { UrlOverflowServiceProvider } from '../error_url_overflow'; -import { getNewPlatform } from '../new_platform'; +import { UrlOverflowService } from '../error_url_overflow'; +import { npSetup } from '../new_platform'; import { toastNotifications } from '../notify'; // @ts-ignore import { isSystemApiRequest } from '../system_api'; @@ -49,7 +49,7 @@ import { isSystemApiRequest } from '../system_api'; const URL_LIMIT_WARN_WITHIN = 1000; export const configureAppAngularModule = (angularModule: IModule) => { - const newPlatform = getNewPlatform().setup.core; + const newPlatform = npSetup.core; const legacyMetadata = newPlatform.injectedMetadata.getLegacyMetadata(); forOwn(newPlatform.injectedMetadata.getInjectedVars(), (val, name) => { @@ -77,9 +77,9 @@ export const configureAppAngularModule = (angularModule: IModule) => { .run($setupUrlOverflowHandling(newPlatform)); }; -const getEsUrl = (newPlatform: CoreSetup) => { +const getEsUrl = (newPlatform: InternalCoreSetup) => { const a = document.createElement('a'); - a.href = newPlatform.basePath.addToPath('/elasticsearch'); + a.href = newPlatform.http.prependBasePath('/elasticsearch'); const protocolPort = /https/.test(a.protocol) ? 443 : 80; const port = a.port || protocolPort; return { @@ -90,13 +90,15 @@ const getEsUrl = (newPlatform: CoreSetup) => { }; }; -const setupCompileProvider = (newPlatform: CoreSetup) => ($compileProvider: ICompileProvider) => { +const setupCompileProvider = (newPlatform: InternalCoreSetup) => ( + $compileProvider: ICompileProvider +) => { if (!newPlatform.injectedMetadata.getLegacyMetadata().devMode) { $compileProvider.debugInfoEnabled(false); } }; -const setupLocationProvider = (newPlatform: CoreSetup) => ( +const setupLocationProvider = (newPlatform: InternalCoreSetup) => ( $locationProvider: ILocationProvider ) => { $locationProvider.html5Mode({ @@ -108,7 +110,7 @@ const setupLocationProvider = (newPlatform: CoreSetup) => ( $locationProvider.hashPrefix(''); }; -export const $setupXsrfRequestInterceptor = (newPlatform: CoreSetup) => { +export const $setupXsrfRequestInterceptor = (newPlatform: InternalCoreSetup) => { const version = newPlatform.injectedMetadata.getLegacyMetadata().version; // Configure jQuery prefilter @@ -143,7 +145,7 @@ export const $setupXsrfRequestInterceptor = (newPlatform: CoreSetup) => { * @param {HttpService} $http * @return {undefined} */ -const capture$httpLoadingCount = (newPlatform: CoreSetup) => ( +const capture$httpLoadingCount = (newPlatform: InternalCoreSetup) => ( $rootScope: IRootScopeService, $http: IHttpService ) => { @@ -164,7 +166,7 @@ const capture$httpLoadingCount = (newPlatform: CoreSetup) => ( * lets us integrate with the angular router so that we can automatically clear * the breadcrumbs if we switch to a Kibana app that does not use breadcrumbs correctly */ -const $setupBreadcrumbsAutoClear = (newPlatform: CoreSetup) => ( +const $setupBreadcrumbsAutoClear = (newPlatform: InternalCoreSetup) => ( $rootScope: IRootScopeService, $injector: any ) => { @@ -211,7 +213,7 @@ const $setupBreadcrumbsAutoClear = (newPlatform: CoreSetup) => ( * lets us integrate with the angular router so that we can automatically clear * the badge if we switch to a Kibana app that does not use the badge correctly */ -const $setupBadgeAutoClear = (newPlatform: CoreSetup) => ( +const $setupBadgeAutoClear = (newPlatform: InternalCoreSetup) => ( $rootScope: IRootScopeService, $injector: any ) => { @@ -251,7 +253,7 @@ const $setupBadgeAutoClear = (newPlatform: CoreSetup) => ( * the helpExtension if we switch to a Kibana app that does not set its own * helpExtension */ -const $setupHelpExtensionAutoClear = (newPlatform: CoreSetup) => ( +const $setupHelpExtensionAutoClear = (newPlatform: InternalCoreSetup) => ( $rootScope: IRootScopeService, $injector: any ) => { @@ -283,13 +285,13 @@ const $setupHelpExtensionAutoClear = (newPlatform: CoreSetup) => ( }); }; -const $setupUrlOverflowHandling = (newPlatform: CoreSetup) => ( +const $setupUrlOverflowHandling = (newPlatform: InternalCoreSetup) => ( $location: ILocationService, $rootScope: IRootScopeService, Private: any, config: any ) => { - const urlOverflow = Private(UrlOverflowServiceProvider); + const urlOverflow = new UrlOverflowService(); const check = () => { // disable long url checks when storing state in session storage if (config.get('state:storeInSessionStorage')) { diff --git a/src/legacy/ui/public/markdown/__snapshots__/markdown.test.js.snap b/src/legacy/ui/public/markdown/__snapshots__/markdown.test.tsx.snap similarity index 100% rename from src/legacy/ui/public/markdown/__snapshots__/markdown.test.js.snap rename to src/legacy/ui/public/markdown/__snapshots__/markdown.test.tsx.snap diff --git a/src/legacy/ui/public/markdown/index.js b/src/legacy/ui/public/markdown/index.ts similarity index 100% rename from src/legacy/ui/public/markdown/index.js rename to src/legacy/ui/public/markdown/index.ts diff --git a/src/legacy/ui/public/markdown/markdown.js b/src/legacy/ui/public/markdown/markdown.js deleted file mode 100644 index be817fa4ff1b5..0000000000000 --- a/src/legacy/ui/public/markdown/markdown.js +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import classNames from 'classnames'; -import React, { PureComponent } from 'react'; -import PropTypes from 'prop-types'; -import MarkdownIt from 'markdown-it'; -import { memoize } from 'lodash'; -import { getSecureRelForTarget } from '@elastic/eui'; - -/** - * Return a memoized markdown rendering function that use the specified - * whiteListedRules and openLinksInNewTab configurations. - * @param {Array of Strings} whiteListedRules - white list of markdown rules - * list of rules can be found at https://github.com/markdown-it/markdown-it/issues/361 - * @param {Boolean} openLinksInNewTab - * @return {Function} Returns an Object to use with dangerouslySetInnerHTML - * with the rendered markdown HTML - */ -export const markdownFactory = memoize((whiteListedRules = [], openLinksInNewTab = false) => { - let markdownIt; - - // It is imperative that the html config property be set to false, to mitigate XSS: the output of markdown-it is - // fed directly to the DOM via React's dangerouslySetInnerHTML below. - - if (whiteListedRules && whiteListedRules.length > 0) { - markdownIt = new MarkdownIt('zero', { html: false, linkify: true }); - markdownIt.enable(whiteListedRules); - } else { - markdownIt = new MarkdownIt({ html: false, linkify: true }); - } - - if (openLinksInNewTab) { - // All links should open in new browser tab. - // Define custom renderer to add 'target' attribute - // https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#renderer - const originalLinkRender = markdownIt.renderer.rules.link_open || function (tokens, idx, options, env, self) { - return self.renderToken(tokens, idx, options); - }; - markdownIt.renderer.rules.link_open = function (tokens, idx, options, env, self) { - const href = tokens[idx].attrGet('href'); - const target = '_blank'; - const rel = getSecureRelForTarget({ href, target }); - - // https://www.jitbit.com/alexblog/256-targetblank---the-most-underestimated-vulnerability-ever/ - tokens[idx].attrPush(['target', target]); - tokens[idx].attrPush(['rel', rel]); - return originalLinkRender(tokens, idx, options, env, self); - }; - } - /** - * This method is used to render markdown from the passed parameter - * into HTML. It will just return an empty string when the markdown is empty. - * @param {String} markdown - The markdown String - * @return {String} - Returns the rendered HTML as string. - */ - return (markdown) => { - return markdown ? markdownIt.render(markdown) : ''; - }; -}, (whiteListedRules = [], openLinksInNewTab = false) => { - return whiteListedRules.join('_').concat(openLinksInNewTab); -}); - -export class Markdown extends PureComponent { - render() { - const { - className, - markdown, - openLinksInNewTab, - whiteListedRules, - ...rest - } = this.props; - - const classes = classNames('kbnMarkdown__body', className); - const markdownRenderer = markdownFactory(whiteListedRules, openLinksInNewTab); - const renderedMarkdown = markdownRenderer(markdown); - return ( -

- ); - } -} - -Markdown.propTypes = { - className: PropTypes.string, - markdown: PropTypes.string, - openLinksInNewTab: PropTypes.bool, - whiteListedRules: PropTypes.arrayOf(PropTypes.string), -}; diff --git a/src/legacy/ui/public/markdown/markdown.test.js b/src/legacy/ui/public/markdown/markdown.test.js deleted file mode 100644 index 314b5b1d60c10..0000000000000 --- a/src/legacy/ui/public/markdown/markdown.test.js +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { shallow } from 'enzyme'; - -import { - Markdown, -} from './markdown'; - -test('render', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); // eslint-disable-line -}); - -test('should never render html tags', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); // eslint-disable-line -}); - -test('should render links with parentheses correctly', () => { - const component = shallow( - - ); - expect(component.render().find('a').prop('href')).toBe('https://example.com/foo/bar?group=(()filters:!t)'); -}); - -test('should add `noreferrer` and `nooopener` to unknown links in new tabs', () => { - const component = shallow( - - ); - expect(component.render().find('a').prop('rel')).toBe('noopener noreferrer'); -}); - -test('should only add `nooopener` to known links in new tabs', () => { - const component = shallow( - - ); - expect(component.render().find('a').prop('rel')).toBe('noopener'); -}); - -describe('props', () => { - - const markdown = 'I am *some* [content](https://en.wikipedia.org/wiki/Content) with `markdown`'; - - test('markdown', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); // eslint-disable-line - }); - - test('openLinksInNewTab', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); // eslint-disable-line - }); - - test('whiteListedRules', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); // eslint-disable-line - }); - - test('should update markdown when openLinksInNewTab prop change', () => { - const component = shallow(); - expect(component.render().find('a').prop('target')).not.toBe('_blank'); - component.setProps({ openLinksInNewTab: true }); - expect(component.render().find('a').prop('target')).toBe('_blank'); - }); - - test('should update markdown when whiteListedRules prop change', () => { - const markdown = '*emphasis* `backticks`'; - const component = shallow(); - expect(component.render().find('em')).toHaveLength(1); - expect(component.render().find('code')).toHaveLength(1); - component.setProps({ whiteListedRules: ['backticks'] }); - expect(component.render().find('code')).toHaveLength(1); - expect(component.render().find('em')).toHaveLength(0); - }); -}); diff --git a/src/legacy/ui/public/markdown/markdown.test.tsx b/src/legacy/ui/public/markdown/markdown.test.tsx new file mode 100644 index 0000000000000..0036c4f6377be --- /dev/null +++ b/src/legacy/ui/public/markdown/markdown.test.tsx @@ -0,0 +1,124 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { Markdown } from './markdown'; + +test('render', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); // eslint-disable-line +}); + +test('should never render html tags', () => { + const component = shallow( + + ); + expect(component).toMatchSnapshot(); // eslint-disable-line +}); + +test('should render links with parentheses correctly', () => { + const component = shallow( + + ); + expect( + component + .render() + .find('a') + .prop('href') + ).toBe('https://example.com/foo/bar?group=(()filters:!t)'); +}); + +test('should add `noreferrer` and `nooopener` to unknown links in new tabs', () => { + const component = shallow( + + ); + expect( + component + .render() + .find('a') + .prop('rel') + ).toBe('noopener noreferrer'); +}); + +test('should only add `nooopener` to known links in new tabs', () => { + const component = shallow( + + ); + expect( + component + .render() + .find('a') + .prop('rel') + ).toBe('noopener'); +}); + +describe('props', () => { + const markdown = 'I am *some* [content](https://en.wikipedia.org/wiki/Content) with `markdown`'; + + test('markdown', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); // eslint-disable-line + }); + + test('openLinksInNewTab', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); // eslint-disable-line + }); + + test('whiteListedRules', () => { + const component = shallow( + + ); + expect(component).toMatchSnapshot(); // eslint-disable-line + }); + + test('should update markdown when openLinksInNewTab prop change', () => { + const component = shallow(); + expect( + component + .render() + .find('a') + .prop('target') + ).not.toBe('_blank'); + component.setProps({ openLinksInNewTab: true }); + expect( + component + .render() + .find('a') + .prop('target') + ).toBe('_blank'); + }); + + test('should update markdown when whiteListedRules prop change', () => { + const md = '*emphasis* `backticks`'; + const component = shallow( + + ); + expect(component.render().find('em')).toHaveLength(1); + expect(component.render().find('code')).toHaveLength(1); + component.setProps({ whiteListedRules: ['backticks'] }); + expect(component.render().find('code')).toHaveLength(1); + expect(component.render().find('em')).toHaveLength(0); + }); +}); diff --git a/src/legacy/ui/public/markdown/markdown.tsx b/src/legacy/ui/public/markdown/markdown.tsx new file mode 100644 index 0000000000000..ba81b5e111cbd --- /dev/null +++ b/src/legacy/ui/public/markdown/markdown.tsx @@ -0,0 +1,113 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import classNames from 'classnames'; +import React, { PureComponent } from 'react'; +import MarkdownIt from 'markdown-it'; +import { memoize } from 'lodash'; +import { getSecureRelForTarget } from '@elastic/eui'; + +/** + * Return a memoized markdown rendering function that use the specified + * whiteListedRules and openLinksInNewTab configurations. + * @param {Array of Strings} whiteListedRules - white list of markdown rules + * list of rules can be found at https://github.com/markdown-it/markdown-it/issues/361 + * @param {Boolean} openLinksInNewTab + * @return {Function} Returns an Object to use with dangerouslySetInnerHTML + * with the rendered markdown HTML + */ +export const markdownFactory = memoize( + (whiteListedRules: string[] = [], openLinksInNewTab: boolean = false) => { + let markdownIt: MarkdownIt; + + // It is imperative that the html config property be set to false, to mitigate XSS: the output of markdown-it is + // fed directly to the DOM via React's dangerouslySetInnerHTML below. + + if (whiteListedRules && whiteListedRules.length > 0) { + markdownIt = new MarkdownIt('zero', { html: false, linkify: true }); + markdownIt.enable(whiteListedRules); + } else { + markdownIt = new MarkdownIt({ html: false, linkify: true }); + } + + if (openLinksInNewTab) { + // All links should open in new browser tab. + // Define custom renderer to add 'target' attribute + // https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#renderer + const originalLinkRender = + markdownIt.renderer.rules.link_open || + function(tokens, idx, options, env, self) { + return self.renderToken(tokens, idx, options); + }; + markdownIt.renderer.rules.link_open = function(tokens, idx, options, env, self) { + const href = tokens[idx].attrGet('href'); + const target = '_blank'; + const rel = getSecureRelForTarget({ href: href === null ? undefined : href, target }); + + // https://www.jitbit.com/alexblog/256-targetblank---the-most-underestimated-vulnerability-ever/ + tokens[idx].attrPush(['target', target]); + if (rel) { + tokens[idx].attrPush(['rel', rel]); + } + return originalLinkRender(tokens, idx, options, env, self); + }; + } + /** + * This method is used to render markdown from the passed parameter + * into HTML. It will just return an empty string when the markdown is empty. + * @param {String} markdown - The markdown String + * @return {String} - Returns the rendered HTML as string. + */ + return (markdown: string) => { + return markdown ? markdownIt.render(markdown) : ''; + }; + }, + (whiteListedRules: string[] = [], openLinksInNewTab: boolean = false) => { + return `${whiteListedRules.join('_')}${openLinksInNewTab}`; + } +); + +interface MarkdownProps extends React.HTMLAttributes { + className?: string; + markdown?: string; + openLinksInNewTab?: boolean; + whiteListedRules?: string[]; +} + +export class Markdown extends PureComponent { + render() { + const { className, markdown = '', openLinksInNewTab, whiteListedRules, ...rest } = this.props; + + const classes = classNames('kbnMarkdown__body', className); + const markdownRenderer = markdownFactory(whiteListedRules, openLinksInNewTab); + const renderedMarkdown = markdownRenderer(markdown); + return ( +
+ ); + } +} diff --git a/src/legacy/ui/public/markdown/markdown_simple.js b/src/legacy/ui/public/markdown/markdown_simple.js deleted file mode 100644 index 30c96558ac6a5..0000000000000 --- a/src/legacy/ui/public/markdown/markdown_simple.js +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React, { Fragment } from 'react'; -import PropTypes from 'prop-types'; -import ReactMarkdown from 'react-markdown'; - -const markdownRenderers = { - root: Fragment, -}; - -// Render markdown string into JSX inside of a Fragment. -export const MarkdownSimple = ({ children }) => ( - {children} -); - -MarkdownSimple.propTypes = { - children: PropTypes.string, -}; diff --git a/src/legacy/ui/public/markdown/markdown_simple.tsx b/src/legacy/ui/public/markdown/markdown_simple.tsx new file mode 100644 index 0000000000000..a5465fd1c6fc9 --- /dev/null +++ b/src/legacy/ui/public/markdown/markdown_simple.tsx @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { Fragment } from 'react'; +import ReactMarkdown from 'react-markdown'; + +const markdownRenderers = { + root: Fragment, +}; + +interface MarkdownSimpleProps { + children: string; +} + +// Render markdown string into JSX inside of a Fragment. +export const MarkdownSimple = ({ children }: MarkdownSimpleProps) => ( + {children} +); diff --git a/src/legacy/ui/public/metadata.d.ts b/src/legacy/ui/public/metadata.d.ts deleted file mode 100644 index d604838bd046b..0000000000000 --- a/src/legacy/ui/public/metadata.d.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -declare class Metadata { - public branch: string; - public version: string; -} - -declare const metadata: Metadata; - -export { metadata }; diff --git a/src/legacy/ui/public/metadata.js b/src/legacy/ui/public/metadata.js deleted file mode 100644 index 3d04e02f79cb7..0000000000000 --- a/src/legacy/ui/public/metadata.js +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export let metadata = null; - -export function __newPlatformSetup__(legacyMetadata) { - if (metadata === null) { - metadata = legacyMetadata; - } else { - throw new Error('ui/metadata can only be initialized once'); - } -} diff --git a/src/legacy/ui/public/metadata.ts b/src/legacy/ui/public/metadata.ts new file mode 100644 index 0000000000000..fade0f0d8629a --- /dev/null +++ b/src/legacy/ui/public/metadata.ts @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { npSetup } from 'ui/new_platform'; + +export const metadata: { + branch: string; + version: string; +} = npSetup.core.injectedMetadata.getLegacyMetadata(); diff --git a/src/legacy/ui/public/modals/confirm_modal.js b/src/legacy/ui/public/modals/confirm_modal.js index 2880b2f083ef6..6d5abfca64aaf 100644 --- a/src/legacy/ui/public/modals/confirm_modal.js +++ b/src/legacy/ui/public/modals/confirm_modal.js @@ -18,6 +18,7 @@ */ import angular from 'angular'; +import { i18n } from '@kbn/i18n'; import { noop } from 'lodash'; import { uiModules } from '../modules'; import template from './confirm_modal.html'; @@ -44,7 +45,7 @@ export const ConfirmationButtonTypes = { * @property {String=} title - If given, shows a title on the confirm modal. */ -module.factory('confirmModal', function ($rootScope, $compile, i18n) { +module.factory('confirmModal', function ($rootScope, $compile) { let modalPopover; const confirmQueue = []; @@ -55,7 +56,7 @@ module.factory('confirmModal', function ($rootScope, $compile, i18n) { return function confirmModal(message, customOptions) { const defaultOptions = { onCancel: noop, - cancelButtonText: i18n('common.ui.modals.cancelButtonLabel', { + cancelButtonText: i18n.translate('common.ui.modals.cancelButtonLabel', { defaultMessage: 'Cancel' }), defaultFocusedButton: ConfirmationButtonTypes.CONFIRM diff --git a/src/legacy/ui/public/new_platform/index.ts b/src/legacy/ui/public/new_platform/index.ts index d205e817fb9d4..ca5890854f3aa 100644 --- a/src/legacy/ui/public/new_platform/index.ts +++ b/src/legacy/ui/public/new_platform/index.ts @@ -16,10 +16,4 @@ * specific language governing permissions and limitations * under the License. */ -export { - __newPlatformSetup__, - __newPlatformStart__, - getNewPlatform, - onSetup, - onStart, -} from './new_platform'; +export { __setup__, __start__, npSetup, npStart } from './new_platform'; diff --git a/src/legacy/ui/public/new_platform/new_platform.test.ts b/src/legacy/ui/public/new_platform/new_platform.test.ts deleted file mode 100644 index c283a12c8dfca..0000000000000 --- a/src/legacy/ui/public/new_platform/new_platform.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { - __newPlatformSetup__, - __newPlatformStart__, - __reset__, - onSetup, - onStart, -} from './new_platform'; - -describe('onSetup', () => { - afterEach(() => __reset__()); - - it('resolves callbacks registered before setup', async () => { - const aCallback = jest.fn(() => 1); - const bCallback = jest.fn(() => 2); - const a = onSetup(aCallback); - const b = onSetup(bCallback); - const coreSetup = { fake: true } as any; - - __newPlatformSetup__(coreSetup); - - expect(await Promise.all([a, b])).toEqual([1, 2]); - expect(aCallback).toHaveBeenCalledWith({ core: { fake: true }, plugins: {} }); - expect(bCallback).toHaveBeenCalledWith({ core: { fake: true }, plugins: {} }); - }); - - it('resolves callbacks registered after setup', async () => { - const callback = jest.fn(() => 3); - const coreSetup = { fake: true } as any; - - __newPlatformSetup__(coreSetup); - - expect(await onSetup(callback)).toEqual(3); - expect(callback).toHaveBeenCalledWith({ core: { fake: true }, plugins: {} }); - }); - - it('rejects errors in callbacks registered before setup', async () => { - const aCallback = jest.fn(() => { - throw new Error('a error!'); - }); - const bCallback = jest.fn(() => { - throw new Error('b error!'); - }); - const cCallback = jest.fn(() => 3); - const a = onSetup(aCallback); - const b = onSetup(bCallback); - const c = onSetup(cCallback); - const coreSetup = { fake: true } as any; - - __newPlatformSetup__(coreSetup); - - await expect(a).rejects.toThrowError('a error!'); - await expect(b).rejects.toThrowError('b error!'); - // make sure one exception doesn't stop other callbacks from running - await expect(c).resolves.toEqual(3); - }); - - it('rejects errors in callbacks registered after setup', async () => { - const callback = jest.fn(() => { - throw new Error('a error!'); - }); - const coreSetup = { fake: true } as any; - - __newPlatformSetup__(coreSetup); - - await expect(onSetup(callback)).rejects.toThrowError('a error!'); - }); -}); - -describe('onStart', () => { - afterEach(() => __reset__()); - - it('resolves callbacks registered before start', async () => { - const aCallback = jest.fn(() => 1); - const bCallback = jest.fn(() => 2); - const a = onStart(aCallback); - const b = onStart(bCallback); - const coreStart = { fake: true } as any; - - __newPlatformStart__(coreStart); - - expect(await Promise.all([a, b])).toEqual([1, 2]); - expect(aCallback).toHaveBeenCalledWith({ core: { fake: true }, plugins: {} }); - expect(bCallback).toHaveBeenCalledWith({ core: { fake: true }, plugins: {} }); - }); - - it('resolves callbacks registered after start', async () => { - const callback = jest.fn(() => 3); - const coreStart = { fake: true } as any; - - __newPlatformStart__(coreStart); - - expect(await onStart(callback)).toEqual(3); - expect(callback).toHaveBeenCalledWith({ core: { fake: true }, plugins: {} }); - }); - - it('rejects errors in callbacks registered before start', async () => { - const aCallback = jest.fn(() => { - throw new Error('a error!'); - }); - const bCallback = jest.fn(() => { - throw new Error('b error!'); - }); - const cCallback = jest.fn(() => 3); - const a = onStart(aCallback); - const b = onStart(bCallback); - const c = onStart(cCallback); - const coreStart = { fake: true } as any; - - __newPlatformStart__(coreStart); - - await expect(a).rejects.toThrowError('a error!'); - await expect(b).rejects.toThrowError('b error!'); - // make sure one exception doesn't stop other callbacks from running - await expect(c).resolves.toEqual(3); - }); - - it('rejects errors in callbacks registered after start', async () => { - const callback = jest.fn(() => { - throw new Error('a error!'); - }); - const coreStart = { fake: true } as any; - - __newPlatformStart__(coreStart); - - await expect(onStart(callback)).rejects.toThrowError('a error!'); - }); -}); diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index 26b291693f7f1..2880cbd60bcbd 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -16,17 +16,16 @@ * specific language governing permissions and limitations * under the License. */ -import { CoreSetup, CoreStart } from '../../../../core/public'; +import { InternalCoreSetup, InternalCoreStart } from '../../../../core/public'; -const runtimeContext = { - setup: { - core: (null as unknown) as CoreSetup, - plugins: {}, - }, - start: { - core: (null as unknown) as CoreStart, - plugins: {}, - }, +export const npSetup = { + core: (null as unknown) as InternalCoreSetup, + plugins: {} as Record, +}; + +export const npStart = { + core: (null as unknown) as InternalCoreStart, + plugins: {} as Record, }; /** @@ -34,88 +33,14 @@ const runtimeContext = { * @internal */ export function __reset__() { - runtimeContext.setup.core = (null as unknown) as CoreSetup; - runtimeContext.start.core = (null as unknown) as CoreStart; -} - -export async function __newPlatformSetup__(core: CoreSetup) { - if (runtimeContext.setup.core) { - throw new Error('New platform core api was already set up'); - } - - runtimeContext.setup.core = core; - - // Process any pending onSetup callbacks - while (onSetupCallbacks.length) { - const cb = onSetupCallbacks.shift()!; - await cb(runtimeContext.setup); - } + npSetup.core = (null as unknown) as InternalCoreSetup; + npStart.core = (null as unknown) as InternalCoreStart; } -export async function __newPlatformStart__(core: CoreStart) { - if (runtimeContext.start.core) { - throw new Error('New platform core api was already started'); - } - - runtimeContext.start.core = core; - - // Process any pending onStart callbacks - while (onStartCallbacks.length) { - const cb = onStartCallbacks.shift()!; - await cb(runtimeContext.start); - } -} - -export function getNewPlatform() { - if (runtimeContext.setup.core === null || runtimeContext.start.core === null) { - throw new Error('runtimeContext is not initialized yet'); - } - - return runtimeContext; +export function __setup__(coreSetup: InternalCoreSetup) { + npSetup.core = coreSetup; } -type SetupCallback = (startContext: typeof runtimeContext['setup']) => T; -type StartCallback = (startContext: typeof runtimeContext['start']) => T; - -const onSetupCallbacks: Array>> = []; -const onStartCallbacks: Array>> = []; - -/** - * Register a callback to be called once the new platform is in the - * `setup` lifecycle event. Resolves to the return value of the callback. - */ -export async function onSetup(callback: SetupCallback): Promise { - if (runtimeContext.setup.core !== null) { - return callback(runtimeContext.setup); - } - - return new Promise((resolve, reject) => { - onSetupCallbacks.push(async (setupContext: typeof runtimeContext['setup']) => { - try { - resolve(await callback(setupContext)); - } catch (e) { - reject(e); - } - }); - }); -} - -/** - * Register a callback to be called once the new platform is in the - * `start` lifecycle event. Resolves to the return value of the callback. - */ -export async function onStart(callback: StartCallback): Promise { - if (runtimeContext.start.core !== null) { - return callback(runtimeContext.start); - } - - return new Promise((resolve, reject) => { - onStartCallbacks.push(async (startContext: typeof runtimeContext['start']) => { - try { - resolve(await callback(startContext)); - } catch (e) { - reject(e); - } - }); - }); +export function __start__(coreStart: InternalCoreStart) { + npStart.core = coreStart; } diff --git a/src/legacy/ui/public/notify/directives/truncated.js b/src/legacy/ui/public/notify/directives/truncated.js index b17567a301701..6d72e8780d4a1 100644 --- a/src/legacy/ui/public/notify/directives/truncated.js +++ b/src/legacy/ui/public/notify/directives/truncated.js @@ -18,6 +18,7 @@ */ import truncText from 'trunc-text'; +import { i18n } from '@kbn/i18n'; import truncHTML from 'trunc-html'; import { uiModules } from '../../modules'; import truncatedTemplate from '../partials/truncated.html'; @@ -25,7 +26,7 @@ import 'angular-sanitize'; const module = uiModules.get('kibana', ['ngSanitize']); -module.directive('kbnTruncated', function (i18n) { +module.directive('kbnTruncated', function () { return { restrict: 'E', scope: { @@ -48,10 +49,10 @@ module.directive('kbnTruncated', function (i18n) { } $scope.truncated = true; $scope.expanded = false; - const showLessText = i18n('common.ui.directives.truncated.showLessLinkText', { + const showLessText = i18n.translate('common.ui.directives.truncated.showLessLinkText', { defaultMessage: 'less' }); - const showMoreText = i18n('common.ui.directives.truncated.showMoreLinkText', { + const showMoreText = i18n.translate('common.ui.directives.truncated.showMoreLinkText', { defaultMessage: 'more' }); $scope.action = showMoreText; diff --git a/src/legacy/ui/public/notify/fatal_error.ts b/src/legacy/ui/public/notify/fatal_error.ts index a837e33b16be0..f3f05bdf793a2 100644 --- a/src/legacy/ui/public/notify/fatal_error.ts +++ b/src/legacy/ui/public/notify/fatal_error.ts @@ -17,22 +17,14 @@ * under the License. */ -import { FatalErrorsSetup } from '../../../../core/public'; +import { npSetup } from 'ui/new_platform'; import { AngularHttpError, formatAngularHttpError, isAngularHttpError, } from './lib/format_angular_http_error'; -let newPlatformFatalErrors: FatalErrorsSetup; - -export function __newPlatformSetup__(instance: FatalErrorsSetup) { - if (newPlatformFatalErrors) { - throw new Error('ui/notify/fatal_error already initialized with new platform apis'); - } - - newPlatformFatalErrors = instance; -} +const newPlatformFatalErrors = npSetup.core.fatalErrors; export function addFatalErrorCallback(callback: () => void) { newPlatformFatalErrors.get$().subscribe(() => { diff --git a/src/legacy/ui/public/notify/toasts/index.ts b/src/legacy/ui/public/notify/toasts/index.ts index c9054cba86d74..d305b176e1128 100644 --- a/src/legacy/ui/public/notify/toasts/index.ts +++ b/src/legacy/ui/public/notify/toasts/index.ts @@ -17,5 +17,5 @@ * under the License. */ -export { toastNotifications, __newPlatformSetup__ } from './toasts'; +export { toastNotifications } from './toasts'; export { Toast, ToastInput } from './toast_notifications'; diff --git a/src/legacy/ui/public/notify/toasts/toast_notifications.test.ts b/src/legacy/ui/public/notify/toasts/toast_notifications.test.ts index 0d49e08d9c616..484dde73b1130 100644 --- a/src/legacy/ui/public/notify/toasts/toast_notifications.test.ts +++ b/src/legacy/ui/public/notify/toasts/toast_notifications.test.ts @@ -19,13 +19,35 @@ import sinon from 'sinon'; import { ToastsApi } from '../../../../../core/public'; - +import { uiSettingsServiceMock, i18nServiceMock } from '../../../../../core/public/mocks'; import { ToastNotifications } from './toast_notifications'; +function toastDeps() { + const uiSettingsMock = uiSettingsServiceMock.createSetupContract(); + (uiSettingsMock.get as jest.Mock).mockImplementation( + () => (config: string) => { + switch (config) { + case 'notifications:lifetime:info': + return 5000; + case 'notifications:lifetime:warning': + return 10000; + case 'notification:lifetime:error': + return 30000; + default: + throw new Error(`Accessing ${config} is not supported in the mock.`); + } + } + ); + return { + uiSettings: uiSettingsMock, + i18n: i18nServiceMock.createSetupContract(), + }; +} + describe('ToastNotifications', () => { describe('interface', () => { function setup() { - return { toastNotifications: new ToastNotifications(new ToastsApi()) }; + return { toastNotifications: new ToastNotifications(new ToastsApi(toastDeps())) }; } describe('add method', () => { diff --git a/src/legacy/ui/public/notify/toasts/toast_notifications.ts b/src/legacy/ui/public/notify/toasts/toast_notifications.ts index 6171cf2c0e985..987717ab17d39 100644 --- a/src/legacy/ui/public/notify/toasts/toast_notifications.ts +++ b/src/legacy/ui/public/notify/toasts/toast_notifications.ts @@ -17,7 +17,12 @@ * under the License. */ -import { Toast, ToastInput, ToastsApi } from '../../../../../core/public'; +import { + ErrorToastOptions, + NotificationsSetup, + Toast, + ToastInput, +} from '../../../../../core/public'; export { Toast, ToastInput }; @@ -26,7 +31,7 @@ export class ToastNotifications { private onChangeCallback?: () => void; - constructor(private readonly toasts: ToastsApi) { + constructor(private readonly toasts: NotificationsSetup['toasts']) { toasts.get$().subscribe(list => { this.list = list; @@ -45,4 +50,6 @@ export class ToastNotifications { public addSuccess = (toastOrTitle: ToastInput) => this.toasts.addSuccess(toastOrTitle); public addWarning = (toastOrTitle: ToastInput) => this.toasts.addWarning(toastOrTitle); public addDanger = (toastOrTitle: ToastInput) => this.toasts.addDanger(toastOrTitle); + public addError = (error: Error, options: ErrorToastOptions) => + this.toasts.addError(error, options); } diff --git a/src/legacy/ui/public/notify/toasts/toasts.ts b/src/legacy/ui/public/notify/toasts/toasts.ts index 0574bf228cf28..8889ee4a6d981 100644 --- a/src/legacy/ui/public/notify/toasts/toasts.ts +++ b/src/legacy/ui/public/notify/toasts/toasts.ts @@ -17,15 +17,7 @@ * under the License. */ -import { ToastsApi } from '../../../../../core/public'; +import { npSetup } from 'ui/new_platform'; import { ToastNotifications } from './toast_notifications'; -export let toastNotifications: ToastNotifications; - -export function __newPlatformSetup__(toasts: ToastsApi) { - if (toastNotifications) { - throw new Error('ui/notify/toasts already initialized with new platform apis'); - } - - toastNotifications = new ToastNotifications(toasts); -} +export const toastNotifications = new ToastNotifications(npSetup.core.notifications.toasts); diff --git a/src/legacy/ui/public/query_manager/index.js b/src/legacy/ui/public/query_manager/index.js deleted file mode 100644 index bcedda808e1ce..0000000000000 --- a/src/legacy/ui/public/query_manager/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { queryManagerFactory } from './query_manager'; diff --git a/src/legacy/ui/public/query_manager/query_manager.js b/src/legacy/ui/public/query_manager/query_manager.js deleted file mode 100644 index 751e6af94b71b..0000000000000 --- a/src/legacy/ui/public/query_manager/query_manager.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; - -export function queryManagerFactory(getState) { - - function getQuery() { - return { - ...getState().query - }; - } - - function setQuery(newQuery) { - const state = getState(); - state.query = newQuery; - - if (_.isFunction(state.save)) { - state.save(); - } - } - - return { - getQuery, - setQuery, - }; - -} diff --git a/src/legacy/ui/public/registry/field_formats.js b/src/legacy/ui/public/registry/field_formats.js index 464b8dbc5386f..0f134dcdd2877 100644 --- a/src/legacy/ui/public/registry/field_formats.js +++ b/src/legacy/ui/public/registry/field_formats.js @@ -51,10 +51,12 @@ class FieldFormatRegistry extends IndexedArray { * using the format:defaultTypeMap config map * * @param {String} fieldType - the field type - * @return {String} + * @param {String[]} esTypes - Array of ES data types + * @return {Object} */ - getDefaultConfig = (fieldType) => { - return this._defaultMap[fieldType] || this._defaultMap._default_; + getDefaultConfig = (fieldType, esTypes) => { + const type = this.getDefaultTypeName(fieldType, esTypes); + return this._defaultMap[type] || this._defaultMap._default_; }; /** @@ -66,16 +68,43 @@ class FieldFormatRegistry extends IndexedArray { getType = (formatId) => { return this.byId[formatId]; }; - /** * Get the default FieldFormat type (class) for * a field type, using the format:defaultTypeMap. + * used by the field editor * * @param {String} fieldType + * @param {String} esTypes - Array of ES data types * @return {Function} */ - getDefaultType = (fieldType) => { - return this.byId[this.getDefaultConfig(fieldType).id]; + getDefaultType = (fieldType, esTypes) => { + const config = this.getDefaultConfig(fieldType, esTypes); + return this.byId[config.id]; + }; + + /** + * Get the name of the default type for ES types like date_nanos + * using the format:defaultTypeMap config map + * + * @param {String[]} esTypes - Array of ES data types + * @return {String|undefined} + */ + getTypeNameByEsTypes = (esTypes) => { + if(!Array.isArray(esTypes)) { + return; + } + return esTypes.find(type => this._defaultMap[type] && this._defaultMap[type].es); + }; + /** + * Get the default FieldFormat type name for + * a field type, using the format:defaultTypeMap. + * + * @param {String} fieldType + * @param {String[]} esTypes + * @return {string} + */ + getDefaultTypeName = (fieldType, esTypes) => { + return this.getTypeNameByEsTypes(esTypes) || fieldType; }; /** @@ -96,13 +125,41 @@ class FieldFormatRegistry extends IndexedArray { * Get the default fieldFormat instance for a field format. * * @param {String} fieldType + * @param {String[]} esTypes * @return {FieldFormat} */ - getDefaultInstance = _.memoize(function (fieldType) { - const conf = this.getDefaultConfig(fieldType); + getDefaultInstancePlain(fieldType, esTypes) { + const conf = this.getDefaultConfig(fieldType, esTypes); + const FieldFormat = this.byId[conf.id]; return new FieldFormat(conf.params, this.getConfig); - }); + } + /** + * Returns a cache key built by the given variables for caching in memoized + * Where esType contains fieldType, fieldType is returned + * -> kibana types have a higher priority in that case + * -> would lead to failing tests that match e.g. date format with/without esTypes + * https://lodash.com/docs#memoize + * + * @param {String} fieldType + * @param {String[]} esTypes + * @return {string} + */ + getDefaultInstanceCacheResolver(fieldType, esTypes) { + return Array.isArray(esTypes) && esTypes.indexOf(fieldType) === -1 + ? [fieldType, ...esTypes].join('-') + : fieldType; + } + + /** + * Get the default fieldFormat instance for a field format. + * It's a memoized function that builds and reads a cache + * + * @param {String} fieldType + * @param {String[]} esTypes + * @return {FieldFormat} + */ + getDefaultInstance = _.memoize(this.getDefaultInstancePlain, this.getDefaultInstanceCacheResolver); parseDefaultTypeMap(value) { diff --git a/src/legacy/ui/public/routes/__tests__/_wrap_route_with_prep.js b/src/legacy/ui/public/routes/__tests__/_wrap_route_with_prep.js index 44b99a72b402c..63c24c336b2e3 100644 --- a/src/legacy/ui/public/routes/__tests__/_wrap_route_with_prep.js +++ b/src/legacy/ui/public/routes/__tests__/_wrap_route_with_prep.js @@ -48,7 +48,7 @@ describe('wrapRouteWithPrep fn', function () { let Promise; let $injector; - ngMock.inject(function ($rootScope, _Private_, _Promise_, _$injector_) { + ngMock.inject(function (_Promise_, _$injector_) { Promise = _Promise_; $injector = _$injector_; }); diff --git a/src/legacy/ui/public/saved_objects/saved_object.d.ts b/src/legacy/ui/public/saved_objects/saved_object.d.ts new file mode 100644 index 0000000000000..762cb9b6ee9c7 --- /dev/null +++ b/src/legacy/ui/public/saved_objects/saved_object.d.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export interface SaveOptions { + confirmOverwrite: boolean; + isTitleDuplicateConfirmed: boolean; + onTitleDuplicate: () => void; +} + +export interface SavedObject { + save: (saveOptions: SaveOptions) => Promise; + copyOnSave: boolean; +} diff --git a/src/legacy/ui/public/saved_objects/saved_object.js b/src/legacy/ui/public/saved_objects/saved_object.js index d4cd17fa6c859..3345ddefce8f3 100644 --- a/src/legacy/ui/public/saved_objects/saved_object.js +++ b/src/legacy/ui/public/saved_objects/saved_object.js @@ -207,7 +207,7 @@ export function SavedObjectProvider(Promise, Private, confirmModalPromise, index } // If index is not an IndexPattern object at this point, then it's a string id of an index. - if (!(index instanceof indexPatterns.IndexPattern)) { + if (typeof index === 'string') { index = indexPatterns.get(index); } diff --git a/src/legacy/ui/public/saved_objects/saved_objects_client.ts b/src/legacy/ui/public/saved_objects/saved_objects_client.ts index fe94afc7f14f2..6fee8826d1638 100644 --- a/src/legacy/ui/public/saved_objects/saved_objects_client.ts +++ b/src/legacy/ui/public/saved_objects/saved_objects_client.ts @@ -22,12 +22,12 @@ import { resolve as resolveUrl } from 'url'; import { MigrationVersion, - SavedObject as PlainSavedObject, + SavedObject, SavedObjectAttributes, SavedObjectReference, SavedObjectsClient as SavedObjectsApi, } from '../../../server/saved_objects'; -import { CreateResponse, FindOptions, UpdateResponse } from '../../../server/saved_objects/service'; +import { FindOptions } from '../../../server/saved_objects/service'; import { isAutoCreateIndexError, showAutoCreateIndexErrorPage } from '../error_auto_create_index'; import { kfetch, KFetchQuery } from '../kfetch'; import { keysToCamelCaseShallow, keysToSnakeCaseShallow } from '../utils/case_conversion'; @@ -73,9 +73,7 @@ interface FindResults interface BatchQueueEntry { type: string; id: string; - resolve: ( - value: SimpleSavedObject | PlainSavedObject - ) => void; + resolve: (value: SimpleSavedObject | SavedObject) => void; reject: (reason?: any) => void; } @@ -165,7 +163,7 @@ export class SavedObjectsClient { overwrite: options.overwrite, }; - const createRequest: Promise> = this.request({ + const createRequest: Promise> = this.request({ method: 'POST', path, query, @@ -334,18 +332,17 @@ export class SavedObjectsClient { version, }; - const request: Promise> = this.request({ + return this.request({ method: 'PUT', path, body, - }); - return request.then(resp => { + }).then((resp: SavedObject) => { return this.createSavedObject(resp); }); } private createSavedObject( - options: PlainSavedObject + options: SavedObject ): SimpleSavedObject { return new SimpleSavedObject(this, options); } diff --git a/src/legacy/ui/public/saved_objects/simple_saved_object.ts b/src/legacy/ui/public/saved_objects/simple_saved_object.ts index 4bb20efdf43fa..d742b103afdd0 100644 --- a/src/legacy/ui/public/saved_objects/simple_saved_object.ts +++ b/src/legacy/ui/public/saved_objects/simple_saved_object.ts @@ -70,7 +70,7 @@ export class SimpleSavedObject { return has(this.attributes, key); } - public save() { + public save(): Promise> { if (this.id) { return this.client.update(this.type, this.id, this.attributes, { migrationVersion: this.migrationVersion, diff --git a/src/legacy/ui/public/state_management/app_state.d.ts b/src/legacy/ui/public/state_management/app_state.d.ts index 3a1dbc8d9d70e..ddd0b90710766 100644 --- a/src/legacy/ui/public/state_management/app_state.d.ts +++ b/src/legacy/ui/public/state_management/app_state.d.ts @@ -18,4 +18,9 @@ */ import { State } from './state'; -export type AppState = State; +export class AppState extends State {} + +export type AppStateClass< + TAppState extends AppState = AppState, + TDefaults = { [key: string]: any } +> = new (defaults: TDefaults) => TAppState; diff --git a/src/legacy/ui/public/state_management/app_state.js b/src/legacy/ui/public/state_management/app_state.js index 73dd33905eaba..8ec0a355cfc34 100644 --- a/src/legacy/ui/public/state_management/app_state.js +++ b/src/legacy/ui/public/state_management/app_state.js @@ -35,7 +35,7 @@ import { callEach } from '../utils/function'; const urlParam = '_a'; -export function AppStateProvider(Private, $rootScope, $location, $injector) { +export function AppStateProvider(Private, $location, $injector) { const State = Private(StateProvider); const PersistedState = $injector.get('PersistedState'); let persistedStates; diff --git a/src/legacy/ui/public/state_management/state.d.ts b/src/legacy/ui/public/state_management/state.d.ts index b96b2303e94f2..b31862b681f55 100644 --- a/src/legacy/ui/public/state_management/state.d.ts +++ b/src/legacy/ui/public/state_management/state.d.ts @@ -17,7 +17,7 @@ * under the License. */ -export interface State { +export class State { [key: string]: any; translateHashToRison: (stateHashOrRison: string | string[]) => string | string[]; getQueryParamName: () => string; diff --git a/src/legacy/ui/public/state_management/state.js b/src/legacy/ui/public/state_management/state.js index 19a09fc1d90ca..b7623ab0fc5a5 100644 --- a/src/legacy/ui/public/state_management/state.js +++ b/src/legacy/ui/public/state_management/state.js @@ -26,6 +26,7 @@ */ import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; import angular from 'angular'; import rison from 'rison-node'; import { applyDiff } from '../utils/diff_object'; @@ -41,7 +42,7 @@ import { isStateHash, } from './state_storage'; -export function StateProvider(Private, $rootScope, $location, stateManagementConfig, config, kbnUrl, i18n) { +export function StateProvider(Private, $rootScope, $location, stateManagementConfig, config, kbnUrl) { const Events = Private(EventsProvider); createLegacyClass(State).inherits(Events); @@ -103,7 +104,7 @@ export function StateProvider(Private, $rootScope, $location, stateManagementCon } if (unableToParse) { - toastNotifications.addDanger(i18n('common.ui.stateManagement.unableToParseUrlErrorMessage', { + toastNotifications.addDanger(i18n.translate('common.ui.stateManagement.unableToParseUrlErrorMessage', { defaultMessage: 'Unable to parse URL' })); search[this._urlParam] = this.toQueryParam(this._defaults); @@ -244,7 +245,7 @@ export function StateProvider(Private, $rootScope, $location, stateManagementCon State.prototype._parseStateHash = function (stateHash) { const json = this._hashedItemStore.getItem(stateHash); if (json === null) { - toastNotifications.addDanger(i18n('common.ui.stateManagement.unableToRestoreUrlErrorMessage', { + toastNotifications.addDanger(i18n.translate('common.ui.stateManagement.unableToRestoreUrlErrorMessage', { defaultMessage: 'Unable to completely restore the URL, be sure to use the share functionality.' })); } @@ -293,7 +294,7 @@ export function StateProvider(Private, $rootScope, $location, stateManagementCon } // If we ran out of space trying to persist the state, notify the user. - const message = i18n('common.ui.stateManagement.unableToStoreHistoryInSessionErrorMessage', { + const message = i18n.translate('common.ui.stateManagement.unableToStoreHistoryInSessionErrorMessage', { defaultMessage: 'Kibana is unable to store history items in your session ' + `because it is full and there don't seem to be items any items safe ` + 'to delete.\n\n' + diff --git a/src/legacy/ui/public/state_management/state_monitor_factory.ts b/src/legacy/ui/public/state_management/state_monitor_factory.ts index 2ed138d24bf14..27f3e59852569 100644 --- a/src/legacy/ui/public/state_management/state_monitor_factory.ts +++ b/src/legacy/ui/public/state_management/state_monitor_factory.ts @@ -20,7 +20,10 @@ import { cloneDeep, isEqual, isPlainObject, set } from 'lodash'; import { State } from './state'; export const stateMonitorFactory = { - create: (state: State, customInitialState: State) => stateMonitor(state, customInitialState), + create: ( + state: State, + customInitialState: TStateDefault + ) => stateMonitor(state, customInitialState), }; interface StateStatus { @@ -28,22 +31,32 @@ interface StateStatus { dirty: boolean; } +export interface StateMonitor { + setInitialState: (innerCustomInitialState: TStateDefault) => void; + ignoreProps: (props: string[] | string) => void; + onChange: (callback: ChangeHandlerFn) => StateMonitor; + destroy: () => void; +} + type ChangeHandlerFn = (status: StateStatus, type: string | null, keys: string[]) => void; -function stateMonitor(state: State, customInitialState: State) { +function stateMonitor( + state: State, + customInitialState: TStateDefault +): StateMonitor { let destroyed = false; let ignoredProps: string[] = []; let changeHandlers: ChangeHandlerFn[] | undefined = []; - let initialState: State; + let initialState: TStateDefault; setInitialState(customInitialState); - function setInitialState(innerCustomInitialState: State) { + function setInitialState(innerCustomInitialState: TStateDefault) { // state.toJSON returns a reference, clone so we can mutate it safely initialState = cloneDeep(innerCustomInitialState) || cloneDeep(state.toJSON()); } - function removeIgnoredProps(innerState: State) { + function removeIgnoredProps(innerState: TStateDefault) { ignoredProps.forEach(path => { set(innerState, path, true); }); @@ -84,7 +97,7 @@ function stateMonitor(state: State, customInitialState: State) { } return { - setInitialState(innerCustomInitialState: State) { + setInitialState(innerCustomInitialState: TStateDefault) { if (!isPlainObject(innerCustomInitialState)) { throw new TypeError('The default state must be an object'); } @@ -102,7 +115,7 @@ function stateMonitor(state: State, customInitialState: State) { } }, - ignoreProps(props: string[]) { + ignoreProps(props: string[] | string) { ignoredProps = ignoredProps.concat(props); removeIgnoredProps(initialState); return this; diff --git a/src/legacy/ui/public/storage/storage_service.ts b/src/legacy/ui/public/storage/storage_service.ts new file mode 100644 index 0000000000000..eb697be0ac07a --- /dev/null +++ b/src/legacy/ui/public/storage/storage_service.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Storage } from './storage'; + +export const localStorage = new Storage(window.localStorage); +export const sessionStorage = new Storage(window.sessionStorage); diff --git a/src/legacy/ui/public/style_compile/style_compile.js b/src/legacy/ui/public/style_compile/style_compile.js index fb9c47d87182a..8e3e052a0dd06 100644 --- a/src/legacy/ui/public/style_compile/style_compile.js +++ b/src/legacy/ui/public/style_compile/style_compile.js @@ -21,14 +21,15 @@ import _ from 'lodash'; import $ from 'jquery'; import '../config'; import { uiModules } from '../modules'; -const $style = $(' + + + mylayer + my_layer + + + + mylayer + my_layer + + + + + `; + } + }; + }; + const capabilities = await wmsClient.getCapabilities(); + expect(capabilities.layers).toEqual([ + { label: 'mylayer (my_layer)', value: 'my_layer' }, + { label: 'mylayer (my_layer):1', value: 'my_layer' }, + { label: 'mylayer (my_layer):2', value: 'my_layer' }, + ]); + }); + + it('Should not create group common hierarchy when there is only a single layer', async () => { const wmsClient = new WmsClient({ serviceUrl: 'myWMSUrl' }); wmsClient._fetch = () => { return { @@ -192,10 +239,12 @@ describe('getCapabilities', () => { }; const capabilities = await wmsClient.getCapabilities(); expect(capabilities.layers).toEqual([ - { label: 'layer1', value: '1' }, + { label: 'layer1 (1)', value: '1' }, ]); }); +}); +describe('getUrlTemplate', () => { it('Should not overwrite specific query parameters when defined in the url', async () => { const urlWithQuery = 'http://example.com/wms?map=MyMap&format=image/jpeg&service=NotWMS&version=0&request=GetNull&srs=Invalid&transparent=false&width=1024&height=640'; const wmsClient = new WmsClient({ serviceUrl: urlWithQuery }); diff --git a/x-pack/plugins/maps/public/shared/layers/styles/_index.scss b/x-pack/plugins/maps/public/shared/layers/styles/_index.scss index d3a07b8da5ae1..4115b1d44b9f6 100644 --- a/x-pack/plugins/maps/public/shared/layers/styles/_index.scss +++ b/x-pack/plugins/maps/public/shared/layers/styles/_index.scss @@ -1,2 +1,3 @@ +@import './components/color_gradient'; @import './components/static_dynamic_style_row'; @import './components/vector/color/static_color_selection'; diff --git a/x-pack/plugins/maps/public/shared/layers/styles/color_utils.js b/x-pack/plugins/maps/public/shared/layers/styles/color_utils.js new file mode 100644 index 0000000000000..9238564d44760 --- /dev/null +++ b/x-pack/plugins/maps/public/shared/layers/styles/color_utils.js @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { vislibColorMaps } from 'ui/vislib/components/color/colormaps'; +import { getLegendColors, getColor } from 'ui/vis/map/color_util'; +import { ColorGradient } from './components/color_gradient'; +import chroma from 'chroma-js'; + +const GRADIENT_INTERVALS = 8; + +function getColorRamp(colorRampName) { + const colorRamp = vislibColorMaps[colorRampName]; + if (!colorRamp) { + throw new Error(`${colorRampName} not found. Expected one of following values: ${Object.keys(vislibColorMaps)}`); + } + return colorRamp; +} + +export function getRGBColorRangeStrings(colorRampName, numberColors = GRADIENT_INTERVALS) { + const colorRamp = getColorRamp(colorRampName); + return getLegendColors(colorRamp.value, numberColors); +} + +export function getHexColorRangeStrings(colorRampName, numberColors = GRADIENT_INTERVALS) { + return getRGBColorRangeStrings(colorRampName, numberColors) + .map(rgbColor => chroma(rgbColor).hex()); +} + +export function getColorRampCenterColor(colorRampName) { + const colorRamp = getColorRamp(colorRampName); + const centerIndex = Math.floor(colorRamp.value.length / 2); + return getColor(colorRamp.value, centerIndex); +} + +// Returns an array of color stops +// [ stop_input_1: number, stop_output_1: color, stop_input_n: number, stop_output_n: color ] +export function getColorRampStops(colorRampName, numberColors = GRADIENT_INTERVALS) { + return getHexColorRangeStrings(colorRampName, numberColors) + .reduce((accu, stopColor, idx, srcArr) => { + const stopNumber = idx / srcArr.length; // number between 0 and 1, increasing as index increases + return [ ...accu, stopNumber, stopColor ]; + }, []); +} + +export const COLOR_GRADIENTS = Object.keys(vislibColorMaps).map(colorRampName => ({ + value: colorRampName, + text: colorRampName, + inputDisplay: +})); + +export const COLOR_RAMP_NAMES = Object.keys(vislibColorMaps); + +export function getLinearGradient(colorStrings) { + const intervals = colorStrings.length; + let linearGradient = `linear-gradient(to right, ${colorStrings[0]} 0%,`; + for (let i = 1; i < intervals - 1; i++) { + linearGradient = `${linearGradient} ${colorStrings[i]} \ + ${Math.floor(100 * i / (intervals - 1))}%,`; + } + return `${linearGradient} ${colorStrings[colorStrings.length - 1]} 100%)`; +} diff --git a/x-pack/plugins/maps/public/shared/layers/styles/color_utils.test.js b/x-pack/plugins/maps/public/shared/layers/styles/color_utils.test.js new file mode 100644 index 0000000000000..91264c666a6cd --- /dev/null +++ b/x-pack/plugins/maps/public/shared/layers/styles/color_utils.test.js @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + COLOR_GRADIENTS, + getColorRampCenterColor, + getColorRampStops, + getHexColorRangeStrings, + getLinearGradient, + getRGBColorRangeStrings, +} from './color_utils'; + +describe('COLOR_GRADIENTS', () => { + it('Should contain EuiSuperSelect options list of color ramps', () => { + expect(COLOR_GRADIENTS.length).toBe(6); + const colorGradientOption = COLOR_GRADIENTS[0]; + expect(colorGradientOption.text).toBe('Blues'); + expect(colorGradientOption.value).toBe('Blues'); + }); +}); + +describe('getRGBColorRangeStrings', () => { + it('Should create RGB color ramp', () => { + expect(getRGBColorRangeStrings('Blues')).toEqual([ + 'rgb(247,250,255)', + 'rgb(221,234,247)', + 'rgb(197,218,238)', + 'rgb(157,201,224)', + 'rgb(106,173,213)', + 'rgb(65,145,197)', + 'rgb(32,112,180)', + 'rgb(7,47,107)' + ]); + }); +}); + +describe('getHexColorRangeStrings', () => { + it('Should create HEX color ramp', () => { + expect(getHexColorRangeStrings('Blues')).toEqual([ + '#f7faff', + '#ddeaf7', + '#c5daee', + '#9dc9e0', + '#6aadd5', + '#4191c5', + '#2070b4', + '#072f6b' + ]); + }); +}); + +describe('getColorRampCenterColor', () => { + it('Should get center color from color ramp', () => { + expect(getColorRampCenterColor('Blues')).toBe('rgb(106,173,213)'); + }); +}); + +describe('getColorRampStops', () => { + it('Should create color stops for color ramp', () => { + expect(getColorRampStops('Blues')).toEqual([ + 0, '#f7faff', + 0.125, '#ddeaf7', + 0.25, '#c5daee', + 0.375, '#9dc9e0', + 0.5, '#6aadd5', + 0.625, '#4191c5', + 0.75, '#2070b4', + 0.875, '#072f6b' + ]); + }); +}); + +describe('getLinearGradient', () => { + it('Should create linear gradient from color ramp', () => { + const colorRamp = [ + 'rgb(247,250,255)', + 'rgb(221,234,247)', + 'rgb(197,218,238)', + 'rgb(157,201,224)', + 'rgb(106,173,213)', + 'rgb(65,145,197)', + 'rgb(32,112,180)', + 'rgb(7,47,107)' + ]; + // eslint-disable-next-line max-len + expect(getLinearGradient(colorRamp)).toBe('linear-gradient(to right, rgb(247,250,255) 0%, rgb(221,234,247) 14%, rgb(197,218,238) 28%, rgb(157,201,224) 42%, rgb(106,173,213) 57%, rgb(65,145,197) 71%, rgb(32,112,180) 85%, rgb(7,47,107) 100%)'); + }); +}); diff --git a/x-pack/plugins/maps/public/shared/layers/styles/components/_color_gradient.scss b/x-pack/plugins/maps/public/shared/layers/styles/components/_color_gradient.scss new file mode 100644 index 0000000000000..dbe9575ce5698 --- /dev/null +++ b/x-pack/plugins/maps/public/shared/layers/styles/components/_color_gradient.scss @@ -0,0 +1,13 @@ +.mapColorGradient { + width: 100%; + height: $euiSizeXS; + position: relative; + top: 0; + right: 0; + bottom: 0; + left: 0; +} + +.mapFillableCircle { + overflow: visible; +} diff --git a/x-pack/plugins/maps/public/shared/layers/styles/components/color_gradient.js b/x-pack/plugins/maps/public/shared/layers/styles/components/color_gradient.js new file mode 100644 index 0000000000000..b416d869e592b --- /dev/null +++ b/x-pack/plugins/maps/public/shared/layers/styles/components/color_gradient.js @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { COLOR_RAMP_NAMES, getRGBColorRangeStrings, getLinearGradient } from '../color_utils'; +import classNames from 'classnames'; + +export const ColorGradient = ({ colorRamp, colorRampName, className }) => { + if (!colorRamp && (!colorRampName || !COLOR_RAMP_NAMES.includes(colorRampName))) { + return null; + } + + const classes = classNames('mapColorGradient', className); + const rgbColorStrings = colorRampName + ? getRGBColorRangeStrings(colorRampName) + : colorRamp; + const background = getLinearGradient(rgbColorStrings); + return ( +
+ ); +}; diff --git a/x-pack/plugins/maps/public/shared/layers/styles/components/heatmap/__snapshots__/heatmap_style_editor.test.js.snap b/x-pack/plugins/maps/public/shared/layers/styles/components/heatmap/__snapshots__/heatmap_style_editor.test.js.snap new file mode 100644 index 0000000000000..a358fc7a31a33 --- /dev/null +++ b/x-pack/plugins/maps/public/shared/layers/styles/components/heatmap/__snapshots__/heatmap_style_editor.test.js.snap @@ -0,0 +1,81 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`HeatmapStyleEditor is rendered 1`] = ` + + , + "text": "theclassic", + "value": "theclassic", + }, + Object { + "inputDisplay": , + "text": "Blues", + "value": "Blues", + }, + Object { + "inputDisplay": , + "text": "Greens", + "value": "Greens", + }, + Object { + "inputDisplay": , + "text": "Greys", + "value": "Greys", + }, + Object { + "inputDisplay": , + "text": "Reds", + "value": "Reds", + }, + Object { + "inputDisplay": , + "text": "Yellow to Red", + "value": "Yellow to Red", + }, + Object { + "inputDisplay": , + "text": "Green to Red", + "value": "Green to Red", + }, + ] + } + valueOfSelected="Blues" + /> + +`; diff --git a/x-pack/plugins/maps/public/shared/layers/styles/components/heatmap/heatmap_constants.js b/x-pack/plugins/maps/public/shared/layers/styles/components/heatmap/heatmap_constants.js new file mode 100644 index 0000000000000..be2f55f373f5c --- /dev/null +++ b/x-pack/plugins/maps/public/shared/layers/styles/components/heatmap/heatmap_constants.js @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +// Color stops from default Mapbox heatmap-color +export const DEFAULT_RGB_HEATMAP_COLOR_RAMP = [ + 'rgb(65, 105, 225)', // royalblue + 'rgb(0, 256, 256)', // cyan + 'rgb(0, 256, 0)', // lime + 'rgb(256, 256, 0)', // yellow + 'rgb(256, 0, 0)', // red +]; + +export const DEFAULT_HEATMAP_COLOR_RAMP_NAME = 'theclassic'; + +export const HEATMAP_COLOR_RAMP_LABEL = i18n.translate('xpack.maps.heatmap.colorRampLabel', { + defaultMessage: 'Color range' +}); diff --git a/x-pack/plugins/maps/public/shared/layers/styles/components/heatmap/heatmap_style_editor.js b/x-pack/plugins/maps/public/shared/layers/styles/components/heatmap/heatmap_style_editor.js new file mode 100644 index 0000000000000..d14d5baefb01e --- /dev/null +++ b/x-pack/plugins/maps/public/shared/layers/styles/components/heatmap/heatmap_style_editor.js @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { EuiFormRow, EuiSuperSelect } from '@elastic/eui'; +import { COLOR_GRADIENTS } from '../../color_utils'; +import { ColorGradient } from '../color_gradient'; +import { + DEFAULT_RGB_HEATMAP_COLOR_RAMP, + DEFAULT_HEATMAP_COLOR_RAMP_NAME, + HEATMAP_COLOR_RAMP_LABEL +} from './heatmap_constants'; + +export function HeatmapStyleEditor({ colorRampName, onHeatmapColorChange }) { + + const onColorRampChange = (selectedColorRampName) => { + onHeatmapColorChange({ + colorRampName: selectedColorRampName + }); + }; + + const colorRampOptions = [ + { + value: DEFAULT_HEATMAP_COLOR_RAMP_NAME, + text: DEFAULT_HEATMAP_COLOR_RAMP_NAME, + inputDisplay: + }, + ...COLOR_GRADIENTS + ]; + + return ( + + + + ); +} diff --git a/x-pack/plugins/maps/public/shared/layers/styles/components/heatmap/heatmap_style_editor.test.js b/x-pack/plugins/maps/public/shared/layers/styles/components/heatmap/heatmap_style_editor.test.js new file mode 100644 index 0000000000000..ae2aae5c5930f --- /dev/null +++ b/x-pack/plugins/maps/public/shared/layers/styles/components/heatmap/heatmap_style_editor.test.js @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { HeatmapStyleEditor } from './heatmap_style_editor'; + +describe('HeatmapStyleEditor', () => { + test('is rendered', () => { + const component = shallow( + {}} + /> + ); + + expect(component) + .toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/maps/public/shared/layers/styles/components/heatmap/legend/heatmap_legend.js b/x-pack/plugins/maps/public/shared/layers/styles/components/heatmap/legend/heatmap_legend.js new file mode 100644 index 0000000000000..74fce11abf0a6 --- /dev/null +++ b/x-pack/plugins/maps/public/shared/layers/styles/components/heatmap/legend/heatmap_legend.js @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { i18n } from '@kbn/i18n'; +import { ColorGradient } from '../../color_gradient'; +import { StyleLegendRow } from '../../style_legend_row'; +import { + DEFAULT_RGB_HEATMAP_COLOR_RAMP, + DEFAULT_HEATMAP_COLOR_RAMP_NAME, + HEATMAP_COLOR_RAMP_LABEL +} from '../heatmap_constants'; + +export function HeatmapLegend({ colorRampName, label }) { + const header = colorRampName === DEFAULT_HEATMAP_COLOR_RAMP_NAME + ? + : ; + + return ( + + ); +} diff --git a/x-pack/plugins/maps/public/shared/layers/styles/components/style_legend_row.js b/x-pack/plugins/maps/public/shared/layers/styles/components/style_legend_row.js new file mode 100644 index 0000000000000..3691f18cbeba6 --- /dev/null +++ b/x-pack/plugins/maps/public/shared/layers/styles/components/style_legend_row.js @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiSpacer, + EuiToolTip, +} from '@elastic/eui'; + +export function StyleLegendRow({ header, minLabel, maxLabel, propertyLabel, fieldLabel }) { + return ( +
+ + {header} + + + + {minLabel} + + + + + + {fieldLabel} + + + + + + {maxLabel} + + + +
+ ); +} + +StyleLegendRow.propTypes = { + header: PropTypes.node.isRequired, + minLabel: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + ]).isRequired, + maxLabel: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + ]).isRequired, + propertyLabel: PropTypes.string.isRequired, + fieldLabel: PropTypes.string.isRequired, +}; diff --git a/x-pack/plugins/maps/public/shared/layers/styles/components/vector/color/color_ramp_select.js b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/color/color_ramp_select.js index feff3b1835922..67d0e5e10f7ce 100644 --- a/x-pack/plugins/maps/public/shared/layers/styles/components/vector/color/color_ramp_select.js +++ b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/color/color_ramp_select.js @@ -8,14 +8,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { EuiSuperSelect } from '@elastic/eui'; -import { vislibColorMaps } from 'ui/vislib/components/color/colormaps'; -import { ColorGradient } from '../../../../../icons/color_gradient'; - -export const COLOR_GRADIENTS = Object.keys(vislibColorMaps).map(colorKey => ({ - value: colorKey, - text: colorKey, - inputDisplay: -})); +import { COLOR_GRADIENTS } from '../../../color_utils'; export function ColorRampSelect({ color, onChange }) { const onColorRampChange = (selectedColorRampString) => { diff --git a/x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/line_icon.js b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/line_icon.js new file mode 100644 index 0000000000000..0f5b6e4b470bf --- /dev/null +++ b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/line_icon.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +export const LineIcon = ({ style }) => ( + + + +); diff --git a/x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/point_icon.js b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/point_icon.js new file mode 100644 index 0000000000000..7a208e1f46093 --- /dev/null +++ b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/point_icon.js @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +export const PointIcon = ({ style }) => ( + + + + + + + + +); diff --git a/x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/polygon_icon.js b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/polygon_icon.js new file mode 100644 index 0000000000000..4907a853e2cda --- /dev/null +++ b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/polygon_icon.js @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +export const PolygonIcon = ({ style }) => ( + + + +); diff --git a/x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/style_property_legend_row.js b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/style_property_legend_row.js index 7aff9372a3977..5a86a1e723c17 100644 --- a/x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/style_property_legend_row.js +++ b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/style_property_legend_row.js @@ -10,17 +10,11 @@ import PropTypes from 'prop-types'; import { styleOptionShapes, rangeShape } from '../style_option_shapes'; import { VectorStyle } from '../../../vector_style'; -import { ColorGradient } from '../../../../../icons/color_gradient'; -import { FillableCircle } from '../../../../../icons/additional_layer_icons'; +import { ColorGradient } from '../../color_gradient'; +import { PointIcon } from './point_icon'; import { getVectorStyleLabel } from '../get_vector_style_label'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiText, - EuiSpacer, - EuiToolTip, - EuiHorizontalRule, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; +import { StyleLegendRow } from '../../style_legend_row'; function getLineWidthIcons() { const defaultStyle = { @@ -29,9 +23,9 @@ function getLineWidthIcons() { width: '12px', }; return [ - , - , - , + , + , + , ]; } @@ -42,9 +36,9 @@ function getSymbolSizeIcons() { fill: 'grey', }; return [ - , - , - , + , + , + , ]; } @@ -84,7 +78,7 @@ export function StylePropertyLegendRow({ name, type, options, range }) { let header; if (options.color) { - header = ; + header = ; } else if (name === 'lineWidth') { header = renderHeaderWithIcons(getLineWidthIcons()); } else if (name === 'iconSize') { @@ -92,37 +86,13 @@ export function StylePropertyLegendRow({ name, type, options, range }) { } return ( -
- - {header} - - - - {_.get(range, 'min', '')} - - - - - - {options.field.label} - - - - - - {_.get(range, 'max', '')} - - - -
+ ); } diff --git a/x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/vector_icon.js b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/vector_icon.js index 31a07d94a439a..3a15ea3908ac0 100644 --- a/x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/vector_icon.js +++ b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/vector_icon.js @@ -8,9 +8,11 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { dynamicColorShape, staticColorShape } from '../style_option_shapes'; -import { ColorableLine, FillableCircle, FillableRectangle } from '../../../../../icons/additional_layer_icons'; +import { PointIcon } from './point_icon'; +import { LineIcon } from './line_icon'; +import { PolygonIcon } from './polygon_icon'; import { VectorStyle } from '../../../vector_style'; -import { getColorRampCenterColor } from '../../../../../utils/color_utils'; +import { getColorRampCenterColor } from '../../../color_utils'; export class VectorIcon extends Component { @@ -50,7 +52,7 @@ export class VectorIcon extends Component { strokeWidth: '4px', }; return ( - + ); } @@ -61,8 +63,8 @@ export class VectorIcon extends Component { }; return this.state.isPointsOnly - ? - : ; + ? + : ; } } diff --git a/x-pack/plugins/maps/public/shared/layers/styles/heatmap_style.js b/x-pack/plugins/maps/public/shared/layers/styles/heatmap_style.js index 2e41430779971..af96fc49b1276 100644 --- a/x-pack/plugins/maps/public/shared/layers/styles/heatmap_style.js +++ b/x-pack/plugins/maps/public/shared/layers/styles/heatmap_style.js @@ -4,22 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; import { GRID_RESOLUTION } from '../grid_resolution'; import { AbstractStyle } from './abstract_style'; +import { HeatmapStyleEditor } from './components/heatmap/heatmap_style_editor'; +import { HeatmapLegend } from './components/heatmap/legend/heatmap_legend'; +import { DEFAULT_HEATMAP_COLOR_RAMP_NAME } from './components/heatmap/heatmap_constants'; +import { getColorRampStops } from './color_utils'; import { i18n } from '@kbn/i18n'; export class HeatmapStyle extends AbstractStyle { static type = 'HEATMAP'; - constructor() { + constructor(descriptor = {}) { super(); - this._descriptor = HeatmapStyle.createDescriptor(); + this._descriptor = HeatmapStyle.createDescriptor(descriptor.colorRampName); } - static createDescriptor() { + static createDescriptor(colorRampName) { return { type: HeatmapStyle.type, + colorRampName: colorRampName ? colorRampName : DEFAULT_HEATMAP_COLOR_RAMP_NAME, }; } @@ -29,6 +35,29 @@ export class HeatmapStyle extends AbstractStyle { }); } + renderEditor({ onStyleDescriptorChange }) { + const onHeatmapColorChange = ({ colorRampName }) => { + const styleDescriptor = HeatmapStyle.createDescriptor(colorRampName); + onStyleDescriptorChange(styleDescriptor); + }; + + return ( + + ); + } + + getLegendDetails(label) { + return ( + + ); + } + setMBPaintProperties({ mbMap, layerId, propertyName, resolution }) { let radius; if (resolution === GRID_RESOLUTION.COARSE) { @@ -49,7 +78,29 @@ export class HeatmapStyle extends AbstractStyle { type: 'identity', property: propertyName }); - } + const { colorRampName } = this._descriptor; + if (colorRampName && colorRampName !== DEFAULT_HEATMAP_COLOR_RAMP_NAME) { + const colorStops = getColorRampStops(colorRampName); + mbMap.setPaintProperty(layerId, 'heatmap-color', [ + 'interpolate', + ['linear'], + ['heatmap-density'], + 0, 'rgba(0, 0, 255, 0)', + ...colorStops.slice(2) // remove first stop from colorStops to avoid conflict with transparent stop at zero + ]); + } else { + mbMap.setPaintProperty(layerId, 'heatmap-color', [ + 'interpolate', + ['linear'], + ['heatmap-density'], + 0, 'rgba(0, 0, 255, 0)', + 0.1, 'royalblue', + 0.3, 'cyan', + 0.5, 'lime', + 0.7, 'yellow', + 1, 'red' + ]); + } + } } - diff --git a/x-pack/plugins/maps/public/shared/layers/styles/vector_style.js b/x-pack/plugins/maps/public/shared/layers/styles/vector_style.js index 06ef80c9672b8..0d705a06202ad 100644 --- a/x-pack/plugins/maps/public/shared/layers/styles/vector_style.js +++ b/x-pack/plugins/maps/public/shared/layers/styles/vector_style.js @@ -7,7 +7,7 @@ import _ from 'lodash'; import React from 'react'; import { i18n } from '@kbn/i18n'; -import { getHexColorRangeStrings } from '../../utils/color_utils'; +import { getColorRampStops } from './color_utils'; import { VectorStyleEditor } from './components/vector/vector_style_editor'; import { getDefaultStaticProperties } from './vector_style_defaults'; import { AbstractStyle } from './abstract_style'; @@ -303,12 +303,8 @@ export class VectorStyle extends AbstractStyle { return (); } - addScaledPropertiesBasedOnStyle(featureCollection) { - if (!featureCollection || featureCollection.length === 0) { - return false; - } - - const scaledFields = this.getDynamicPropertiesArray() + _getScaledFields() { + return this.getDynamicPropertiesArray() .map(({ options }) => { const name = options.field.name; return { @@ -316,55 +312,80 @@ export class VectorStyle extends AbstractStyle { range: this._getFieldRange(name), computedName: VectorStyle.getComputedFieldName(name), }; - }) - .filter(({ range }) => { - return range; }); + } + + clearFeatureState(featureCollection, mbMap, sourceId) { + const tmpFeatureIdentifier = { + source: null, + id: null + }; + for (let i = 0; i < featureCollection.features.length; i++) { + const feature = featureCollection.features[i]; + tmpFeatureIdentifier.source = sourceId; + tmpFeatureIdentifier.id = feature.id; + mbMap.removeFeatureState(tmpFeatureIdentifier); + } + } + + setFeatureState(featureCollection, mbMap, sourceId) { + + if (!featureCollection) { + return; + } + const scaledFields = this._getScaledFields(); if (scaledFields.length === 0) { - return false; + return; } + const tmpFeatureIdentifier = { + source: null, + id: null + }; + const tmpFeatureState = {}; + //scale to [0,1] domain - featureCollection.features.forEach(feature => { - scaledFields.forEach(({ name, range, computedName }) => { + for (let i = 0; i < featureCollection.features.length; i++) { + const feature = featureCollection.features[i]; + + for (let j = 0; j < scaledFields.length; j++) { + const { name, range, computedName } = scaledFields[j]; const unscaledValue = parseFloat(feature.properties[name]); let scaledValue; - if (isNaN(unscaledValue)) {//cannot scale + if (isNaN(unscaledValue) || !range) {//cannot scale scaledValue = -1;//put outside range } else if (range.delta === 0) {//values are identical scaledValue = 1;//snap to end of color range } else { scaledValue = (feature.properties[name] - range.min) / range.delta; } - feature.properties[computedName] = scaledValue; - }); - }); - - return true; + tmpFeatureState[computedName] = scaledValue; + } + tmpFeatureIdentifier.source = sourceId; + tmpFeatureIdentifier.id = feature.id; + mbMap.setFeatureState(tmpFeatureIdentifier, tmpFeatureState); + } } _getMBDataDrivenColor({ fieldName, color }) { - const colorRange = getHexColorRangeStrings(color, 8) - .reduce((accu, curColor, idx, srcArr) => { - accu = [ ...accu, idx / srcArr.length, curColor ]; - return accu; - }, []); + const colorStops = getColorRampStops(color); const targetName = VectorStyle.getComputedFieldName(fieldName); return [ 'interpolate', ['linear'], - ['coalesce', ['get', targetName], -1], + ['coalesce', ['feature-state', targetName], -1], -1, 'rgba(0,0,0,0)', - ...colorRange + ...colorStops ]; } _getMbDataDrivenSize({ fieldName, minSize, maxSize }) { const targetName = VectorStyle.getComputedFieldName(fieldName); - return ['interpolate', + return [ + 'interpolate', ['linear'], - ['get', targetName], + ['feature-state', targetName], 0, minSize, 1, maxSize ]; diff --git a/x-pack/plugins/maps/public/shared/layers/styles/vector_style_defaults.js b/x-pack/plugins/maps/public/shared/layers/styles/vector_style_defaults.js index fa45178d66764..674e9b66249be 100644 --- a/x-pack/plugins/maps/public/shared/layers/styles/vector_style_defaults.js +++ b/x-pack/plugins/maps/public/shared/layers/styles/vector_style_defaults.js @@ -5,7 +5,7 @@ */ import { VectorStyle } from './vector_style'; -import { COLOR_GRADIENTS } from './components/vector/color/color_ramp_select'; +import { COLOR_GRADIENTS } from './color_utils'; const DEFAULT_COLORS = ['#e6194b', '#3cb44b', '#ffe119', '#f58231', '#911eb4']; diff --git a/x-pack/plugins/maps/public/shared/layers/vector_layer.js b/x-pack/plugins/maps/public/shared/layers/vector_layer.js index 5e3014839a53a..d59996e8ef0d6 100644 --- a/x-pack/plugins/maps/public/shared/layers/vector_layer.js +++ b/x-pack/plugins/maps/public/shared/layers/vector_layer.js @@ -21,7 +21,6 @@ const EMPTY_FEATURE_COLLECTION = { features: [] }; - const CLOSED_SHAPE_MB_FILTER = [ 'any', ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], @@ -36,6 +35,15 @@ const ALL_SHAPE_MB_FILTER = [ ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_LINE_STRING] ]; + +let idCounter = 0; +function generateNumericalId() { + const newId = idCounter < Number.MAX_SAFE_INTEGER ? idCounter : 0; + idCounter = newId + 1; + return newId; +} + + export class VectorLayer extends AbstractLayer { static type = 'VECTOR'; @@ -364,11 +372,12 @@ export class VectorLayer extends AbstractLayer { } } - _assignIdsToFeatures(featureCollection) { for (let i = 0; i < featureCollection.features.length; i++) { const feature = featureCollection.features[i]; - feature.properties[FEATURE_ID_PROPERTY_NAME] = (typeof feature.id === 'string' || typeof feature.id === 'number') ? feature.id : i; + const id = generateNumericalId(); + feature.properties[FEATURE_ID_PROPERTY_NAME] = id; + feature.id = id; } } @@ -406,23 +415,23 @@ export class VectorLayer extends AbstractLayer { } _syncFeatureCollectionWithMb(mbMap) { - const mbGeoJSONSource = mbMap.getSource(this.getId()); + const mbGeoJSONSource = mbMap.getSource(this.getId()); const featureCollection = this._getSourceFeatureCollection(); + const featureCollectionOnMap = AbstractLayer.getBoundDataForSource(mbMap, this.getId()); + if (!featureCollection) { + if (featureCollectionOnMap) { + this._style.clearFeatureState(featureCollectionOnMap, mbMap, this.getId()); + } mbGeoJSONSource.setData(EMPTY_FEATURE_COLLECTION); return; } - const dataBoundToMap = AbstractLayer.getBoundDataForSource(mbMap, this.getId()); - if (featureCollection !== dataBoundToMap) { - mbGeoJSONSource.setData(featureCollection); - } - - const shouldRefresh = this._style.addScaledPropertiesBasedOnStyle(featureCollection); - if (shouldRefresh) { + if (featureCollection !== featureCollectionOnMap) { mbGeoJSONSource.setData(featureCollection); } + this._style.setFeatureState(featureCollection, mbMap, this.getId()); } _setMbPointsProperties(mbMap) { diff --git a/x-pack/plugins/maps/public/shared/utils/color_utils.js b/x-pack/plugins/maps/public/shared/utils/color_utils.js deleted file mode 100644 index 8f7ca601f8b9f..0000000000000 --- a/x-pack/plugins/maps/public/shared/utils/color_utils.js +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { vislibColorMaps } from 'ui/vislib/components/color/colormaps'; -import { getLegendColors, getColor } from 'ui/vis/map/color_util'; -import chroma from 'chroma-js'; - -function getColorRamp(colorRampName) { - const colorRamp = vislibColorMaps[colorRampName]; - if (!colorRamp) { - throw new Error(`${colorRampName} not found. Expected one of following values: ${Object.keys(vislibColorMaps)}`); - } - return colorRamp; -} - -export function getRGBColorRangeStrings(colorRampName, numberColors) { - const colorRamp = getColorRamp(colorRampName); - return getLegendColors(colorRamp.value, numberColors); -} - -export function getHexColorRangeStrings(colorRampName, numberColors) { - return getRGBColorRangeStrings(colorRampName, numberColors) - .map(rgbColor => chroma(rgbColor).hex()); -} - -export function getColorRampCenterColor(colorRampName) { - const colorRamp = getColorRamp(colorRampName); - const centerIndex = Math.floor(colorRamp.value.length / 2); - return getColor(colorRamp.value, centerIndex); -} diff --git a/x-pack/plugins/maps/server/maps_telemetry/maps_usage_collector.js b/x-pack/plugins/maps/server/maps_telemetry/maps_usage_collector.js index 88713a4471123..9c76be739fbe2 100644 --- a/x-pack/plugins/maps/server/maps_telemetry/maps_usage_collector.js +++ b/x-pack/plugins/maps/server/maps_telemetry/maps_usage_collector.js @@ -13,37 +13,63 @@ export function initTelemetryCollection(server) { registerMapsUsageCollector(server); } -export function buildCollectorObj(server) { - return { - type: 'maps', - fetch: async () => { - let docs; - try { - ({ docs } = await server.taskManager.fetch({ - query: { - bool: { - filter: { - term: { - _id: TASK_ID - } - } +async function isTaskManagerReady(server) { + const result = await fetch(server); + return result !== null; +} + +async function fetch(server) { + let docs; + try { + ({ docs } = await server.taskManager.fetch({ + query: { + bool: { + filter: { + term: { + _id: TASK_ID } } - })); - } catch (err) { - const errMessage = err && err.message ? err.message : err.toString(); - /* - * The usage service WILL to try to fetch from this collector before the task manager has been initialized, because the task manager - * has to wait for all plugins to initialize first. - * It's fine to ignore it as next time around it will be initialized (or it will throw a different type of error) - */ - if (errMessage.indexOf('NotInitialized') >= 0) { - docs = {}; - } else { - throw err; } } + })); + } catch (err) { + const errMessage = err && err.message ? err.message : err.toString(); + /* + * The usage service WILL to try to fetch from this collector before the task manager has been initialized, because the task manager + * has to wait for all plugins to initialize first. + * It's fine to ignore it as next time around it will be initialized (or it will throw a different type of error) + */ + if (errMessage.indexOf('NotInitialized') >= 0) { + return null; + } else { + throw err; + } + } + + return docs; +} + +export function buildCollectorObj(server) { + let isCollectorReady = false; + async function determineIfTaskManagerIsReady() { + let isReady = false; + try { + isReady = await isTaskManagerReady(server); + } catch (err) {} // eslint-disable-line + + if (isReady) { + isCollectorReady = true; + } else { + setTimeout(determineIfTaskManagerIsReady, 500); + } + } + determineIfTaskManagerIsReady(); + return { + type: 'maps', + isReady: () => isCollectorReady, + fetch: async () => { + const docs = await fetch(server); return _.get(docs, '[0].state.stats'); }, }; diff --git a/x-pack/plugins/maps/server/routes.js b/x-pack/plugins/maps/server/routes.js index 6a97fc21cca30..b111f3a4f9342 100644 --- a/x-pack/plugins/maps/server/routes.js +++ b/x-pack/plugins/maps/server/routes.js @@ -5,10 +5,11 @@ */ -import { GIS_API_PATH } from '../common/constants'; +import { EMS_DATA_FILE_PATH, EMS_DATA_TMS_PATH, EMS_META_PATH, GIS_API_PATH } from '../common/constants'; import fetch from 'node-fetch'; -import _ from 'lodash'; import { i18n } from '@kbn/i18n'; +import { getEMSResources } from '../common/ems_util'; +import Boom from 'boom'; const ROOT = `/${GIS_API_PATH}`; @@ -26,37 +27,99 @@ export function initRoutes(server, licenseUid) { server.route({ method: 'GET', - path: `${ROOT}/data/ems`, + path: `${ROOT}/${EMS_DATA_FILE_PATH}`, handler: async (request) => { + if (!mapConfig.proxyElasticMapsServiceInMaps) { + server.log('warning', `Cannot load content from EMS when map.proxyElasticMapsServiceInMaps is turned off`); + throw Boom.notFound(); + } + if (!request.query.id) { server.log('warning', 'Must supply id parameters to retrieve EMS file'); return null; } - const ems = await getEMSResources(licenseUid); - + const ems = await getEMSResources(emsClient, mapConfig.includeElasticMapsService, licenseUid, false); const layer = ems.fileLayers.find(layer => layer.id === request.query.id); if (!layer) { return null; } - const file = await fetch(layer.url); - return await file.json(); + try { + const file = await fetch(layer.url); + return await file.json(); + } catch(e) { + server.log('warning', `Cannot connect to EMS for file, error: ${e.message}`); + throw Boom.badRequest(`Cannot connect to EMS`); + } + + } + }); + + + server.route({ + method: 'GET', + path: `${ROOT}/${EMS_DATA_TMS_PATH}`, + handler: async (request, h) => { + + if (!mapConfig.proxyElasticMapsServiceInMaps) { + server.log('warning', `Cannot load content from EMS when map.proxyElasticMapsServiceInMaps is turned off`); + throw Boom.notFound(); + } + + if (!request.query.id || + typeof parseInt(request.query.x, 10) !== 'number' || + typeof parseInt(request.query.y, 10) !== 'number' || + typeof parseInt(request.query.z, 10) !== 'number' + ) { + server.log('warning', 'Must supply id/x/y/z parameters to retrieve EMS tile'); + return null; + } + + const ems = await getEMSResources(emsClient, mapConfig.includeElasticMapsService, licenseUid, false); + const tmsService = ems.tmsServices.find(layer => layer.id === request.query.id); + if (!tmsService) { + return null; + } + const url = tmsService.url + .replace('{x}', request.query.x) + .replace('{y}', request.query.y) + .replace('{z}', request.query.z); + + try { + const tile = await fetch(url); + const arrayBuffer = await tile.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + let response = h.response(buffer); + response = response.bytes(buffer.length); + response = response.header('Content-Disposition', 'inline'); + response = response.header('Content-type', 'image/png'); + response = response.encoding('binary'); + return response; + } catch(e) { + server.log('warning', `Cannot connect to EMS for tile, error: ${e.message}`); + throw Boom.badRequest(`Cannot connect to EMS`); + } } }); server.route({ method: 'GET', - path: `${ROOT}/meta`, + path: `${ROOT}/${EMS_META_PATH}`, handler: async () => { + if (!mapConfig.proxyElasticMapsServiceInMaps) { + server.log('warning', `Cannot load content from EMS when map.proxyElasticMapsServiceInMaps is turned off`); + throw Boom.notFound(); + } + let ems; try { - ems = await getEMSResources(licenseUid); + ems = await getEMSResources(emsClient, mapConfig.includeElasticMapsService, licenseUid, true); } catch (e) { - server.log('warning', `Cannot connect to EMS, error: ${e}`); + server.log('warning', `Cannot connect to EMS, error: ${e.message}`); ems = { fileLayers: [], tmsServices: [] @@ -64,15 +127,9 @@ export function initRoutes(server, licenseUid) { } return ({ - data_sources: { - ems: { - file: ems.fileLayers, - tms: ems.tmsServices - }, - kibana: { - regionmap: _.get(mapConfig, 'regionmap.layers', []), - tilemap: _.get(mapConfig, 'tilemap', []) - } + ems: { + file: ems.fileLayers, + tms: ems.tmsServices } }); } @@ -93,57 +150,9 @@ export function initRoutes(server, licenseUid) { try { const { count } = await callWithRequest(request, 'count', { index: query.index }); return { count }; - } catch(error) { + } catch (error) { return h.response().code(400); } } }); - - async function getEMSResources(licenseUid) { - - if (!mapConfig.includeElasticMapsService) { - return { - fileLayers: [], - tmsServices: [] - }; - } - - emsClient.addQueryParams({ license: licenseUid }); - const fileLayerObjs = await emsClient.getFileLayers(); - const tmsServicesObjs = await emsClient.getTMSServices(); - - const fileLayers = fileLayerObjs.map(fileLayer => { - //backfill to static settings - const format = fileLayer.getDefaultFormatType(); - const meta = fileLayer.getDefaultFormatMeta(); - - return { - name: fileLayer.getDisplayName(), - origin: fileLayer.getOrigin(), - id: fileLayer.getId(), - created_at: fileLayer.getCreatedAt(), - attribution: fileLayer.getHTMLAttribution(), - attributions: fileLayer.getAttributions(), - fields: fileLayer.getFieldsInLanguage(), - url: fileLayer.getDefaultFormatUrl(), - format: format, //legacy: format and meta are split up - meta: meta, //legacy, format and meta are split up, - emsLink: fileLayer.getEMSHotLink() - }; - }); - - const tmsServices = tmsServicesObjs.map(tmsService => { - return { - origin: tmsService.getOrigin(), - id: tmsService.getId(), - minZoom: tmsService.getMinZoom(), - maxZoom: tmsService.getMaxZoom(), - attribution: tmsService.getHTMLAttribution(), - attributionMarkdown: tmsService.getMarkdownAttribution(), - url: tmsService.getUrlTemplate() - }; - }); - - return { fileLayers, tmsServices }; - } } diff --git a/x-pack/plugins/maps/server/sample_data/ecommerce_saved_objects.js b/x-pack/plugins/maps/server/sample_data/ecommerce_saved_objects.js index 8fcc191a77917..f047f5aaffca7 100644 --- a/x-pack/plugins/maps/server/sample_data/ecommerce_saved_objects.js +++ b/x-pack/plugins/maps/server/sample_data/ecommerce_saved_objects.js @@ -47,7 +47,7 @@ export const getEcommerceSavedObjects = () => { } ], 'migrationVersion': { - 'map': '7.1.0' + 'map': '7.2.0' }, 'attributes': { 'title': i18n.translate('xpack.maps.sampleData.ecommerceSpec.mapsTitle', { diff --git a/x-pack/plugins/maps/server/sample_data/flights_saved_objects.js b/x-pack/plugins/maps/server/sample_data/flights_saved_objects.js index b05690bb0d5fe..ca32c58d77e76 100644 --- a/x-pack/plugins/maps/server/sample_data/flights_saved_objects.js +++ b/x-pack/plugins/maps/server/sample_data/flights_saved_objects.js @@ -32,7 +32,7 @@ export const getFlightsSavedObjects = () => { } ], 'migrationVersion': { - 'map': '7.1.0' + 'map': '7.2.0' }, 'attributes': { 'title': i18n.translate('xpack.maps.sampleData.flightaSpec.mapsTitle', { diff --git a/x-pack/plugins/maps/server/sample_data/web_logs_saved_objects.js b/x-pack/plugins/maps/server/sample_data/web_logs_saved_objects.js index 5d7ea053b66da..1a26b1ff7781b 100644 --- a/x-pack/plugins/maps/server/sample_data/web_logs_saved_objects.js +++ b/x-pack/plugins/maps/server/sample_data/web_logs_saved_objects.js @@ -32,7 +32,7 @@ export const getWebLogsSavedObjects = () => { } ], 'migrationVersion': { - 'map': '7.1.0' + 'map': '7.2.0' }, 'attributes': { 'title': i18n.translate('xpack.maps.sampleData.flightaSpec.logsTitle', { diff --git a/x-pack/plugins/ml/common/constants/index_patterns.ts b/x-pack/plugins/ml/common/constants/index_patterns.ts index 429cf84543252..16554b1be1648 100644 --- a/x-pack/plugins/ml/common/constants/index_patterns.ts +++ b/x-pack/plugins/ml/common/constants/index_patterns.ts @@ -8,5 +8,6 @@ export const ML_ANNOTATIONS_INDEX_ALIAS_READ = '.ml-annotations-read'; export const ML_ANNOTATIONS_INDEX_ALIAS_WRITE = '.ml-annotations-write'; export const ML_ANNOTATIONS_INDEX_PATTERN = '.ml-annotations-6'; +export const ML_CONFIG_INDEX_PATTERN = '.ml-config'; export const ML_RESULTS_INDEX_PATTERN = '.ml-anomalies-*'; export const ML_NOTIFICATION_INDEX_PATTERN = '.ml-notifications'; diff --git a/x-pack/plugins/ml/common/constants/jobs_list.ts b/x-pack/plugins/ml/common/constants/jobs_list.ts index c07c88d01a4d0..c6392e0155595 100644 --- a/x-pack/plugins/ml/common/constants/jobs_list.ts +++ b/x-pack/plugins/ml/common/constants/jobs_list.ts @@ -7,3 +7,4 @@ export const DEFAULT_REFRESH_INTERVAL_MS = 30000; export const MINIMUM_REFRESH_INTERVAL_MS = 1000; export const DELETING_JOBS_REFRESH_INTERVAL_MS = 2000; +export const PROGRESS_JOBS_REFRESH_INTERVAL_MS = 2000; diff --git a/x-pack/plugins/ml/index.js b/x-pack/plugins/ml/index.js index c165fe3ab40de..d9f2e30996a91 100644 --- a/x-pack/plugins/ml/index.js +++ b/x-pack/plugins/ml/index.js @@ -9,7 +9,9 @@ import { resolve } from 'path'; import Boom from 'boom'; import { checkLicense } from './server/lib/check_license'; +import { addLinksToSampleDatasets } from './server/lib/sample_data_sets'; import { FEATURE_ANNOTATIONS_ENABLED } from './common/constants/feature_flags'; +import { LICENSE_TYPE } from './common/constants/license'; import { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status'; import { annotationRoutes } from './server/routes/annotations'; @@ -34,6 +36,7 @@ import { fileDataVisualizerRoutes } from './server/routes/file_data_visualizer'; import { i18n } from '@kbn/i18n'; import { initMlServerLog } from './server/client/log'; + export const ml = (kibana) => { return new kibana.Plugin({ require: ['kibana', 'elasticsearch', 'xpack_main'], @@ -77,7 +80,17 @@ export const ml = (kibana) => { xpackMainPlugin.status.once('green', () => { // Register a function that is called whenever the xpack info changes, // to re-compute the license check results for this plugin - xpackMainPlugin.info.feature(thisPlugin.id).registerLicenseCheckResultsGenerator(checkLicense); + const mlFeature = xpackMainPlugin.info.feature(thisPlugin.id); + mlFeature.registerLicenseCheckResultsGenerator(checkLicense); + + // Add links to the Kibana sample data sets if ml is enabled + // and there is a full license (trial or platinum). + if (mlFeature.isEnabled() === true) { + const licenseCheckResults = mlFeature.getLicenseCheckResults(); + if (licenseCheckResults.licenseType === LICENSE_TYPE.FULL) { + addLinksToSampleDatasets(server); + } + } }); xpackMainPlugin.registerFeature({ @@ -150,3 +163,4 @@ export const ml = (kibana) => { }); }; + diff --git a/x-pack/plugins/ml/public/app.js b/x-pack/plugins/ml/public/app.js index 16d0f877a6a46..f5f36bffbba3c 100644 --- a/x-pack/plugins/ml/public/app.js +++ b/x-pack/plugins/ml/public/app.js @@ -16,7 +16,6 @@ import 'ui/autoload/all'; import 'ui/kbn_top_nav'; import 'plugins/ml/access_denied'; -import 'plugins/ml/lib/angular_bootstrap_patch'; import 'plugins/ml/jobs'; import 'plugins/ml/services/calendar_service'; import 'plugins/ml/components/messagebar'; diff --git a/x-pack/plugins/ml/public/components/confirm_modal/confirm_modal_controller.js b/x-pack/plugins/ml/public/components/confirm_modal/confirm_modal_controller.js index a83b94bfe83f2..bc8301229d24a 100644 --- a/x-pack/plugins/ml/public/components/confirm_modal/confirm_modal_controller.js +++ b/x-pack/plugins/ml/public/components/confirm_modal/confirm_modal_controller.js @@ -7,9 +7,10 @@ import { uiModules } from 'ui/modules'; +import { i18n } from '@kbn/i18n'; const module = uiModules.get('apps/ml'); -module.controller('MlConfirmModal', function ($scope, $modalInstance, params, i18n) { +module.controller('MlConfirmModal', function ($scope, $modalInstance, params) { $scope.okFunc = params.ok; $scope.cancelFunc = params.cancel; @@ -17,11 +18,11 @@ module.controller('MlConfirmModal', function ($scope, $modalInstance, params, i1 $scope.message = params.message || ''; $scope.title = params.title || ''; - $scope.okLabel = params.okLabel || i18n('xpack.ml.confirmModal.okButtonLabel', { + $scope.okLabel = params.okLabel || i18n.translate('xpack.ml.confirmModal.okButtonLabel', { defaultMessage: 'OK', }); - $scope.cancelLabel = params.cancelLabel || i18n('xpack.ml.confirmModal.cancelButtonLabel', { + $scope.cancelLabel = params.cancelLabel || i18n.translate('xpack.ml.confirmModal.cancelButtonLabel', { defaultMessage: 'Cancel', }); diff --git a/x-pack/plugins/ml/public/components/display_value/display_value.js b/x-pack/plugins/ml/public/components/display_value/display_value.js new file mode 100644 index 0000000000000..b9db4510ca533 --- /dev/null +++ b/x-pack/plugins/ml/public/components/display_value/display_value.js @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + + +import React from 'react'; +import { + EuiToolTip +} from '@elastic/eui'; + + +const MAX_CHARS = 12; + +export function DisplayValue({ value }) { + const length = String(value).length; + let formattedValue; + + if (length <= MAX_CHARS) { + formattedValue = value; + } else { + formattedValue = ( + + + {value} + + + ); + } + + return formattedValue; +} diff --git a/x-pack/plugins/ml/public/components/display_value/index.js b/x-pack/plugins/ml/public/components/display_value/index.js new file mode 100644 index 0000000000000..d023894470ed4 --- /dev/null +++ b/x-pack/plugins/ml/public/components/display_value/index.js @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { DisplayValue } from './display_value'; diff --git a/x-pack/plugins/ml/public/components/field_data_card/_field_data_card.scss b/x-pack/plugins/ml/public/components/field_data_card/_field_data_card.scss index ee125cd392d18..5f1e480c7ce24 100644 --- a/x-pack/plugins/ml/public/components/field_data_card/_field_data_card.scss +++ b/x-pack/plugins/ml/public/components/field_data_card/_field_data_card.scss @@ -45,7 +45,7 @@ @include euiPanel($selector: 'card-contents'); .card-contents { - height: 393px; + height: 400px; border-radius: 0px 0px $euiBorderRadius $euiBorderRadius; overflow: hidden; } @@ -68,6 +68,15 @@ display: inline-block; } + .stat.min.value, .stat.max.value, .stat.median.value { + font-size: $euiFontSizeS; + @include euiTextTruncate; + } + + .valueWrapper { + display: inline; + } + .not-exist-message { padding: 50px 30px 0px 30px; text-align: center; diff --git a/x-pack/plugins/ml/public/components/field_data_card/content_types/card_number.html b/x-pack/plugins/ml/public/components/field_data_card/content_types/card_number.html index f743c1caf015e..024ab88b0bef0 100644 --- a/x-pack/plugins/ml/public/components/field_data_card/content_types/card_number.html +++ b/x-pack/plugins/ml/public/components/field_data_card/content_types/card_number.html @@ -1,5 +1,5 @@
-
+
-
{{ card.stats.min | formatField:card.fieldFormat }}
-
{{ card.stats.median | formatField:card.fieldFormat }}
-
{{ card.stats.max | formatField:card.fieldFormat }}
+
+ + +
+
+ + +
+
+ + +
diff --git a/x-pack/plugins/ml/public/components/field_data_card/display_value_directive.js b/x-pack/plugins/ml/public/components/field_data_card/display_value_directive.js new file mode 100644 index 0000000000000..f94e721319c2e --- /dev/null +++ b/x-pack/plugins/ml/public/components/field_data_card/display_value_directive.js @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + + +import 'ngreact'; +import { uiModules } from 'ui/modules'; +const module = uiModules.get('apps/ml', ['react']); + +import { DisplayValue } from '../../components/display_value'; + +module.directive('mlDisplayValue', function (reactDirective) { + return reactDirective( + DisplayValue, + undefined, + { restrict: 'E' } + ); +}); diff --git a/x-pack/plugins/ml/public/components/field_data_card/document_count_chart_directive.js b/x-pack/plugins/ml/public/components/field_data_card/document_count_chart_directive.js index 406b078e3d107..1550d8077afda 100644 --- a/x-pack/plugins/ml/public/components/field_data_card/document_count_chart_directive.js +++ b/x-pack/plugins/ml/public/components/field_data_card/document_count_chart_directive.js @@ -12,13 +12,14 @@ */ import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; import d3 from 'd3'; import moment from 'moment'; import { parseInterval } from 'ui/utils/parse_interval'; import { numTicksForDateFormat } from '../../util/chart_utils'; import { calculateTextWidth } from '../../util/string_utils'; -import { IntervalHelperProvider } from '../../util/ml_time_buckets'; +import { MlTimeBuckets } from '../../util/ml_time_buckets'; import { mlChartTooltipService } from '../../components/chart_tooltip/chart_tooltip_service'; import { formatHumanReadableDateTime } from '../../util/date_utils'; @@ -26,7 +27,7 @@ import { uiModules } from 'ui/modules'; import { timefilter } from 'ui/timefilter'; const module = uiModules.get('apps/ml'); -module.directive('mlDocumentCountChart', function (Private, i18n) { +module.directive('mlDocumentCountChart', function () { function link(scope, element, attrs) { const svgWidth = attrs.width ? +attrs.width : 400; const svgHeight = scope.height = attrs.height ? +attrs.height : 400; @@ -43,8 +44,6 @@ module.directive('mlDocumentCountChart', function (Private, i18n) { let barChartGroup; let barWidth = 5; // Adjusted according to data aggregation interval. - const MlTimeBuckets = Private(IntervalHelperProvider); - scope.chartData = []; element.on('$destroy', function () { @@ -157,7 +156,7 @@ module.directive('mlDocumentCountChart', function (Private, i18n) { function showChartTooltip(data, rect) { const formattedDate = formatHumanReadableDateTime(data.time); - const contents = i18n('xpack.ml.fieldDataCard.documentCountChart.chartTooltip', { + const contents = i18n.translate('xpack.ml.fieldDataCard.documentCountChart.chartTooltip', { defaultMessage: '{formattedDate}{br}{hr}count: {dataValue}', values: { formattedDate, diff --git a/x-pack/plugins/ml/public/components/field_data_card/index.js b/x-pack/plugins/ml/public/components/field_data_card/index.js index ab3668f1f1fbc..5aeb0e4ca2298 100644 --- a/x-pack/plugins/ml/public/components/field_data_card/index.js +++ b/x-pack/plugins/ml/public/components/field_data_card/index.js @@ -7,6 +7,7 @@ import './document_count_chart_directive'; +import './display_value_directive'; import './field_data_card_directive'; import './metric_distribution_chart_directive'; import './top_values_directive'; diff --git a/x-pack/plugins/ml/public/components/field_data_card/metric_distribution_chart_directive.js b/x-pack/plugins/ml/public/components/field_data_card/metric_distribution_chart_directive.js index e32d26a34f828..d23fdfa28028f 100644 --- a/x-pack/plugins/ml/public/components/field_data_card/metric_distribution_chart_directive.js +++ b/x-pack/plugins/ml/public/components/field_data_card/metric_distribution_chart_directive.js @@ -12,6 +12,7 @@ */ import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; import d3 from 'd3'; import { numTicks } from '../../util/chart_utils'; @@ -21,7 +22,7 @@ import { mlChartTooltipService } from '../../components/chart_tooltip/chart_tool import { uiModules } from 'ui/modules'; const module = uiModules.get('apps/ml'); -module.directive('mlMetricDistributionChart', function (i18n) { +module.directive('mlMetricDistributionChart', function () { function link(scope, element, attrs) { const svgWidth = attrs.width ? +attrs.width : 400; @@ -179,7 +180,7 @@ module.directive('mlMetricDistributionChart', function (i18n) { .attr('y', 10) .attr('class', 'info-text') .attr('transform', `translate(${margin.left}, ${margin.top})`) - .text(i18n('xpack.ml.fieldDataCard.metricDistributionChart.displayingPercentilesLabel', { + .text(i18n.translate('xpack.ml.fieldDataCard.metricDistributionChart.displayingPercentilesLabel', { defaultMessage: 'Displaying {minPercent} - {maxPercent} percentiles', values: { minPercent, @@ -256,7 +257,7 @@ module.directive('mlMetricDistributionChart', function (i18n) { const minValFormatted = scope.card.fieldFormat.convert(bar.dataMin, 'text'); if (bar.dataMax > bar.dataMin) { const maxValFormatted = scope.card.fieldFormat.convert(bar.dataMax, 'text'); - contents = i18n('xpack.ml.fieldDataCard.metricDistributionChart.documentsBarPercentBetweenValuesDescription', { + contents = i18n.translate('xpack.ml.fieldDataCard.metricDistributionChart.documentsBarPercentBetweenValuesDescription', { defaultMessage: '{barPercent}% of documents have{br}values between {minValFormatted} and {maxValFormatted}', values: { barPercent: bar.percent, @@ -266,7 +267,7 @@ module.directive('mlMetricDistributionChart', function (i18n) { }, }); } else { - contents = i18n('xpack.ml.fieldDataCard.metricDistributionChart.documentsBarPercentValueDescription', { + contents = i18n.translate('xpack.ml.fieldDataCard.metricDistributionChart.documentsBarPercentValueDescription', { defaultMessage: '{barPercent}% of documents have{br}a value of {minValFormatted}', values: { barPercent: bar.percent, diff --git a/x-pack/plugins/ml/public/components/field_data_card/top_values.html b/x-pack/plugins/ml/public/components/field_data_card/top_values.html index 74c88ff9c2e25..0bb9396e993e3 100644 --- a/x-pack/plugins/ml/public/components/field_data_card/top_values.html +++ b/x-pack/plugins/ml/public/components/field_data_card/top_values.html @@ -28,7 +28,7 @@
-
-
-
- {{description.txt}} - -
-
-
diff --git a/x-pack/plugins/ml/public/components/job_select_list/job_select_button_directive.js b/x-pack/plugins/ml/public/components/job_select_list/job_select_button_directive.js deleted file mode 100644 index 51511ab544e9b..0000000000000 --- a/x-pack/plugins/ml/public/components/job_select_list/job_select_button_directive.js +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - - - -/* - * ml-job-select-list directive for rendering a multi-select control for selecting - * one or more jobs from the list of configured jobs. - */ - -import template from './job_select_button.html'; - -import 'ui/accessibility/kbn_accessible_click'; -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml'); - -import { JobSelectServiceProvider } from 'plugins/ml/components/job_select_list/job_select_service'; - -module.directive('jobSelectButton', function (Private) { - - const mlJobSelectService = Private(JobSelectServiceProvider); - - function link(scope) { - scope.selectJobBtnJobIdLabel = ''; - scope.unsafeHtml = ''; - scope.description = scope.singleSelection ? mlJobSelectService.singleJobDescription : mlJobSelectService.description; - - scope.createMenu = function () { - let txt = ' -
- - -
-
-
- - {{ ::'xpack.ml.jobSelectList.groupsTitle' | i18n: { defaultMessage: 'Groups' } }} -
-
-
-
-
- -
-
-
-
-
- -
- - {{ ::'xpack.ml.jobSelectList.jobsTitle' | i18n: { defaultMessage: 'Jobs' } }} -
-
-
-
- -
-
-
-
- - - - -
- -
- -
diff --git a/x-pack/plugins/ml/public/components/job_select_list/job_select_list_directive.js b/x-pack/plugins/ml/public/components/job_select_list/job_select_list_directive.js deleted file mode 100644 index 7fb3e07ab1aa5..0000000000000 --- a/x-pack/plugins/ml/public/components/job_select_list/job_select_list_directive.js +++ /dev/null @@ -1,407 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - - - -/* - * ml-job-select-list directive for rendering a multi-select control for selecting - * one or more jobs from the list of configured jobs. - */ - -import _ from 'lodash'; -import $ from 'jquery'; -import moment from 'moment'; -import d3 from 'd3'; - -import template from './job_select_list.html'; -import { isTimeSeriesViewJob } from 'plugins/ml/../common/util/job_utils'; -import { mlJobService } from 'plugins/ml/services/job_service'; -import { JobSelectServiceProvider } from 'plugins/ml/components/job_select_list/job_select_service'; - -import { timefilter } from 'ui/timefilter'; -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml'); - -module.directive('mlJobSelectList', function (Private) { - return { - restrict: 'AE', - replace: true, - transclude: true, - template, - controller: function ($scope, i18n) { - const mlJobSelectService = Private(JobSelectServiceProvider); - $scope.jobs = []; - $scope.groups = []; - $scope.homelessJobs = []; - $scope.singleSelection = false; - $scope.timeSeriesOnly = false; - $scope.noJobsCreated = undefined; - $scope.applyTimeRange = mlJobSelectService.jobSelectListState.applyTimeRange; - $scope.urlSelectedIds = {}; - $scope.selected = {}; - $scope.allGroupsSelected = false; - $scope.allJobsSelected = false; - $scope.selectedJobRadio = ''; - $scope.selectedCount = 0; - - mlJobService.loadJobs() - .then((resp) => { - if (resp.jobs.length > 0) { - $scope.noJobsCreated = false; - const jobs = []; - resp.jobs.forEach(job => { - if (job.groups && job.groups.length) { - job.groups.forEach(group => { - jobs.push(createJob(`${group}.${job.job_id}`, group, job)); - }); - } else { - jobs.push(createJob(job.job_id, null, job)); - } - }); - normalizeTimes(jobs); - $scope.jobs = jobs; - const { groups, homeless } = createGroups($scope.jobs); - $scope.groups = groups; - $scope.homelessJobs = homeless; - $scope.selected = { - groups: [], - jobs: [] - }; - - // count all jobs, including duplicates in groups. - // if it's the same as the number of ids passed in, tick all jobs - const jobCount = resp.jobs.reduce((sum, job) => (sum + ((job.groups === undefined) ? 1 : job.groups.length)), 0); - const selectAll = (jobCount === $scope.urlSelectedIds.jobs.length); - - // create the groups and jobs which are used in the menu - groups.forEach(group => { - $scope.selected.groups.push({ - id: group.id, - selected: group.selected, - // TODO: is the selectable property of a group still needed? - selectable: group.selectable, - timeRange: group.timeRange, - isGroup: true, - }); - }); - - jobs.forEach(job => { - if ($scope.selected.jobs.find(j => j.id === job.name) === undefined) { - $scope.selected.jobs.push({ - id: job.name, - selected: selectAll || job.selected, - disabled: job.disabled, - timeRange: job.timeRange, - running: job.running, - isGroup: false - }); - } - }); - - $scope.allJobsSelected = areAllJobsSelected(); - $scope.allGroupsSelected = areAllGroupsSelected(); - createSelectedCount(); - - // if in single selection mode, set the radio button controller ($scope.selectedJobRadio) - // to the selected job id - if ($scope.singleSelection === true) { - $scope.jobs.forEach(j => { - if (j.selected) { - $scope.selectedJobRadio = j.name; - } - }); - } - } else { - $scope.noJobsCreated = true; - } - $scope.$applyAsync(); - }).catch((resp) => { - console.log('mlJobSelectList controller - error getting job info from ES:', resp); - }); - - function createJob(jobId, groupId, job) { - return { - id: jobId, - name: job.job_id, - group: groupId, - isGroup: false, - selected: _.includes($scope.urlSelectedIds.jobs, job.job_id), - disabled: !($scope.timeSeriesOnly === false || isTimeSeriesViewJob(job) === true), - running: (job.datafeed_config && job.datafeed_config.state === 'started'), - timeRange: { - to: job.data_counts.latest_record_timestamp, - from: job.data_counts.earliest_record_timestamp, - fromPx: 0, - toPx: 0, - widthPx: 0, - label: '' - } - }; - } - - function createGroups(jobsIn) { - const jobGroups = {}; - const homeless = []; - // first pull all of the groups out of all of the jobs - // keeping homeless (groupless) jobs in a separate list - jobsIn.forEach(job => { - if (job.group !== null) { - if (jobGroups[job.group] === undefined) { - jobGroups[job.group] = [job]; - } else { - jobGroups[job.group].push(job); - } - } else { - homeless.push(job); - } - }); - - const groups = _.map(jobGroups, (jobs, id) => { - const group = { - id, - selected: false, - selectable: true, - expanded: false, - isGroup: true, - jobs - }; - // check to see whether all of the groups jobs have been selected, - // if they have, select the group - if ($scope.singleSelection === false) { - group.selected = _.includes($scope.urlSelectedIds.groups, id); - } - - // create an over all time range for the group - const timeRange = { - to: null, - toMoment: null, - from: null, - fromMoment: null, - fromPx: null, - toPx: null, - widthPx: null, - }; - - jobs.forEach(job => { - job.group = group; - - if (timeRange.to === null || job.timeRange.to > timeRange.to) { - timeRange.to = job.timeRange.to; - timeRange.toMoment = job.timeRange.toMoment; - } - if (timeRange.from === null || job.timeRange.from < timeRange.from) { - timeRange.from = job.timeRange.from; - timeRange.fromMoment = job.timeRange.fromMoment; - } - if (timeRange.toPx === null || job.timeRange.toPx > timeRange.toPx) { - timeRange.toPx = job.timeRange.toPx; - } - if (timeRange.fromPx === null || job.timeRange.fromPx < timeRange.fromPx) { - timeRange.fromPx = job.timeRange.fromPx; - } - }); - timeRange.widthPx = timeRange.toPx - timeRange.fromPx; - timeRange.toMoment = moment(timeRange.to); - timeRange.fromMoment = moment(timeRange.from); - - const fromString = timeRange.fromMoment.format('MMM Do YYYY, HH:mm'); - const toString = timeRange.toMoment.format('MMM Do YYYY, HH:mm'); - timeRange.label = i18n('xpack.ml.jobSelectList.groupTimeRangeLabel', { - defaultMessage: '{fromString} to {toString}', - values: { - fromString, - toString, - } - }); - - group.timeRange = timeRange; - return group; - }); - - return { - groups, - homeless - }; - } - - // apply the selected jobs - $scope.apply = function () { - // if in single selection mode, get the job id from $scope.selectedJobRadio - const selectedJobs = []; - if ($scope.singleSelection) { - selectedJobs.push(...$scope.selected.jobs.filter(j => j.id === $scope.selectedJobRadio)); - } else { - selectedJobs.push(...$scope.selected.jobs.filter(j => j.selected)); - selectedJobs.push(...$scope.selected.groups.filter(g => g.selected)); - } - - if (areAllJobsSelected()) { - // if all jobs have been selected, just store '*' in the url - mlJobSelectService.setJobIds(['*']); - } else { - const jobIds = selectedJobs.map(j => (j.isGroup ? `${j.id}.*` : j.id)); - mlJobSelectService.setJobIds(jobIds); - } - - // if the apply time range checkbox is ticked, - // find the min and max times for all selected jobs - // and apply them to the timefilter - if ($scope.applyTimeRange) { - const times = []; - selectedJobs.forEach(job => { - if (job.timeRange.from !== undefined) { - times.push(job.timeRange.from); - } - if (job.timeRange.to !== undefined) { - times.push(job.timeRange.to); - } - }); - if (times.length) { - const min = _.min(times); - const max = _.max(times); - timefilter.setTime({ - from: moment(min).toISOString(), - to: moment(max).toISOString() - }); - } - } - mlJobSelectService.jobSelectListState.applyTimeRange = $scope.applyTimeRange; - $scope.closePopover(); - }; - - // ticking a job - $scope.toggleSelection = function () { - // check to see if all jobs are now selected - $scope.allJobsSelected = areAllJobsSelected(); - $scope.allGroupsSelected = areAllGroupsSelected(); - createSelectedCount(); - }; - - // ticking the all jobs checkbox - $scope.toggleAllJobsSelection = function () { - const allJobsSelected = areAllJobsSelected(); - $scope.allJobsSelected = !allJobsSelected; - - $scope.selected.jobs.forEach(job => { - job.selected = $scope.allJobsSelected; - }); - - createSelectedCount(); - }; - - // ticking a group - $scope.toggleGroupSelection = function () { - $scope.allGroupsSelected = areAllGroupsSelected(); - createSelectedCount(); - }; - - // ticking the all jobs checkbox - $scope.toggleAllGroupsSelection = function () { - const allGroupsSelected = areAllGroupsSelected(); - $scope.allGroupsSelected = !allGroupsSelected; - - $scope.selected.groups.forEach(group => { - group.selected = $scope.allGroupsSelected; - }); - createSelectedCount(); - }; - - // check to see whether all jobs in the list have been selected - function areAllJobsSelected() { - let allSelected = true; - $scope.selected.jobs.forEach(job => { - if (job.selected === false) { - allSelected = false; - } - }); - return allSelected; - } - - // check to see whether all groups in the list have been selected - function areAllGroupsSelected() { - let allSelected = true; - $scope.selected.groups.forEach(group => { - if (group.selected === false) { - allSelected = false; - } - }); - return allSelected; - } - - function createSelectedCount() { - $scope.selectedCount = 0; - $scope.selected.jobs.forEach(job => { - if (job.selected) { - $scope.selectedCount++; - } - }); - $scope.selected.groups.forEach(group => { - if (group.selected) { - $scope.selectedCount++; - } - }); - } - - // create the data used for the gant charts - function normalizeTimes(jobs) { - const min = _.min(jobs, job => +job.timeRange.from); - const max = _.max(jobs, job => +job.timeRange.to); - - const gantScale = d3.scale.linear().domain([min.timeRange.from, max.timeRange.to]).range([1, 299]); - - jobs.forEach(job => { - if (job.timeRange.to !== undefined && job.timeRange.from !== undefined) { - job.timeRange.fromPx = gantScale(job.timeRange.from); - job.timeRange.toPx = gantScale(job.timeRange.to); - job.timeRange.widthPx = job.timeRange.toPx - job.timeRange.fromPx; - - job.timeRange.toMoment = moment(job.timeRange.to); - job.timeRange.fromMoment = moment(job.timeRange.from); - - const fromString = job.timeRange.fromMoment.format('MMM Do YYYY, HH:mm'); - const toString = job.timeRange.toMoment.format('MMM Do YYYY, HH:mm'); - job.timeRange.label = i18n('xpack.ml.jobSelectList.jobTimeRangeLabel', { - defaultMessage: '{fromString} to {toString}', - values: { - fromString, - toString, - } - }); - } - }); - - } - - $scope.useTimeRange = function (job) { - timefilter.setTime({ - from: job.timeRange.fromMoment.toISOString(), - to: job.timeRange.toMoment.toISOString() - }); - }; - }, - link: function (scope, element, attrs) { - const mlJobSelectService = Private(JobSelectServiceProvider); - scope.timeSeriesOnly = false; - if (attrs.timeseriesonly === 'true') { - scope.timeSeriesOnly = true; - } - - if (attrs.singleSelection === 'true') { - scope.singleSelection = true; - } - - // Make a copy of the list of jobs ids - // '*' is passed to indicate 'All jobs'. - scope.urlSelectedIds = { - groups: [...mlJobSelectService.groupIds], - jobs: [...mlJobSelectService.jobIdsWithGroup], - }; - - // Giving the parent div focus fixes checkbox tick UI selection on IE. - $('.ml-select-list', element).focus(); - } - }; -}); diff --git a/x-pack/plugins/ml/public/components/job_select_list/job_select_service.js b/x-pack/plugins/ml/public/components/job_select_list/job_select_service.js deleted file mode 100644 index 435ed41613456..0000000000000 --- a/x-pack/plugins/ml/public/components/job_select_list/job_select_service.js +++ /dev/null @@ -1,310 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - - - -// Service with functions used for broadcasting job picker changes - -import _ from 'lodash'; -import { toastNotifications } from 'ui/notify'; - -import { mlJobService } from 'plugins/ml/services/job_service'; - -let jobSelectService = undefined; - -export function JobSelectServiceProvider($rootScope, globalState, i18n) { - - function checkGlobalState() { - if (globalState.ml === undefined) { - globalState.ml = {}; - globalState.save(); - } - } - checkGlobalState(); - - function loadJobIdsFromGlobalState() { - const jobIds = []; - if (globalState.ml && globalState.ml.jobIds) { - let tempJobIds = []; - if (typeof globalState.ml.jobIds === 'string') { - tempJobIds.push(globalState.ml.jobIds); - } else { - tempJobIds = globalState.ml.jobIds; - } - tempJobIds = tempJobIds.map(id => String(id)); - const invalidIds = getInvalidJobIds(removeGroupIds(tempJobIds)); - warnAboutInvalidJobIds(invalidIds); - - let validIds = _.difference(tempJobIds, invalidIds); - - // if there are no valid ids, warn and then select the first job - if (validIds.length === 0) { - toastNotifications.addWarning(i18n('xpack.ml.jobSelect.noJobsSelectedWarningMessage', { - defaultMessage: 'No jobs selected, auto selecting first job', - })); - - if (mlJobService.jobs.length) { - validIds = [mlJobService.jobs[0].job_id]; - } - } - jobIds.push(...validIds); - - // replace the job ids in the url with the ones which are valid - storeJobIdsInGlobalState(jobIds); - } else { - checkGlobalState(); - - // no jobs selected, use the first in the list - if (mlJobService.jobs.length) { - jobIds.push(mlJobService.jobs[0].job_id); - } - storeJobIdsInGlobalState(jobIds); - } - - return jobIds; - } - - function storeJobIdsInGlobalState(jobIds) { - globalState.ml.jobIds = jobIds; - globalState.save(); - } - - // check that the ids read from the url exist by comparing them to the - // jobs loaded via mlJobsService. - function getInvalidJobIds(ids) { - return ids.filter(id => { - const job = _.find(mlJobService.jobs, { 'job_id': id }); - return (job === undefined && id !== '*'); - }); - } - - function removeGroupIds(jobIds) { - return jobIds.map(id => { - const splitId = id.split('.'); - return (splitId.length > 1) ? splitId[1] : splitId[0]; - }); - } - - function warnAboutInvalidJobIds(invalidIds) { - if (invalidIds.length > 0) { - toastNotifications.addWarning(i18n('xpack.ml.jobSelect.requestedJobsDoesNotExistWarningMessage', { - defaultMessage: `Requested -{invalidIdsLength, plural, one {job {invalidIds} does not exist} other {jobs {invalidIds} do not exist}}`, - values: { - invalidIdsLength: invalidIds.length, - invalidIds, - } - })); - } - } - - function createDescription(jobs) { - let txt = ''; - // add up the number of jobs including duplicates if they belong to multiple groups - const jobCount = mlJobService.jobs.length; - - // add up how many jobs belong to groups and how many don't - const selectedGroupJobs = []; - const groupCounts = {}; - let groupLessJobs = 0; - const splitJobs = jobs.map(job => { - const obj = splitJobId(job); - if (obj.group) { - // keep track of selected jobs from group selection - selectedGroupJobs.push(obj.job); - } - return obj; - }); - - splitJobs.forEach(jobObj => { - if (jobObj.group) { - groupCounts[jobObj.group] = (groupCounts[jobObj.group] || 0) + 1; - } else { - // if job has already been included via group selection don't add as groupless job - if (selectedGroupJobs.includes(jobObj.job) === false) { - groupLessJobs++; - } - } - }); - // All jobs have been selected - if ((_.uniq(selectedGroupJobs).length + groupLessJobs) === jobCount) { - txt = i18n('xpack.ml.jobSelect.allJobsDescription', { - defaultMessage: 'All jobs', - }); - } else { - const wholeGroups = []; - const groups = mlJobService.getJobGroups(); - // work out how many groups have all of their jobs selected - groups.forEach(group => { - const groupCount = groupCounts[group.id]; - if (groupCount !== undefined && groupCount === group.jobs.length) { - // this group has all of it's jobs selected - wholeGroups.push(group.id); - } else { - if (groupCount !== undefined) { - // this job doesn't so add it to the count of groupless jobs - groupLessJobs += groupCount; - } - } - }); - - // show the whole groups first - if (wholeGroups.length) { - txt = wholeGroups[0]; - if (wholeGroups.length > 1 || groupLessJobs > 0) { - const total = (wholeGroups.length - 1) + groupLessJobs; - txt = i18n('xpack.ml.jobSelect.wholeGroupDescription', { - defaultMessage: `{wholeGroup} (with {count, plural, zero {# job} one {# job} other {# jobs}}) and - {total, plural, zero {# other} one {# other} other {# others}}`, - values: { - count: groupCounts[wholeGroups[0]], - wholeGroup: wholeGroups[0], - total, - } - }); - } - } else { - // otherwise just list the job ids - txt = splitJobId(jobs[0]).job; - if (jobs.length > 1) { - txt = i18n('xpack.ml.jobSelect.jobDescription', { - defaultMessage: '{jobId} and {jobsAmount, plural, zero {# other} one {# other} other {# others}}', - values: { - jobId: splitJobId(jobs[0]).job, - jobsAmount: jobs.length - 1, - } - }); - } - } - } - return txt; - } - // function to split the group from the job and return both or just the job - function splitJobId(jobId) { - let obj = {}; - const splitId = jobId.split('.'); - if (splitId.length === 2) { - obj = { group: splitId[0], job: splitId[1] }; - } else { - obj = { job: jobId }; - } - return obj; - } - this.splitJobId = splitJobId; - - // expands `*` into groupId.jobId list - // expands `groupId.*` into `groupId.jobId` list - // returns list of expanded job ids - function expandGroups(jobIds) { - const newJobIds = []; - const groups = mlJobService.getJobGroups(); - jobIds.forEach(jobId => { - if (jobId === '*') { - mlJobService.jobs.forEach(job => { - if (job.groups === undefined) { - newJobIds.push(job.job_id); - } else { - newJobIds.push(...job.groups.map(g => `${g}.${job.job_id}`)); - } - }); - } else { - const splitId = splitJobId(jobId); - if (splitId.group !== undefined && splitId.job === '*') { - const groupId = splitId.group; - const group = groups.find(g => g.id === groupId); - group.jobs.forEach(j => { - newJobIds.push(`${groupId}.${j.job_id}`); - }); - } - else { - newJobIds.push(jobId); - } - } - }); - return newJobIds; - } - - function getGroupIds(jobIds) { - const groupIds = []; - jobIds.forEach(jobId => { - const splitId = splitJobId(jobId); - if (splitId.group !== undefined && splitId.job === '*') { - groupIds.push(splitId.group); - } - }); - return groupIds; - } - - // takes an array of ids. - // this could be a mixture of job ids, group ids or a *. - // stores an expanded list of job ids (i.e. groupId.jobId) and a list of jobs ids only. - // creates the description text used on the job picker button. - function processIds(service, ids) { - const expandedJobIds = expandGroups(ids); - service.jobIdsWithGroup.length = 0; - service.jobIdsWithGroup.push(...expandedJobIds); - service.groupIds = getGroupIds(ids); - service.jobIds.length = 0; - service.jobIds.push(...removeGroupIds(expandedJobIds)); - service.description.txt = createDescription(service.jobIdsWithGroup); - service.singleJobDescription.txt = ids[0]; - setBrowserTitle(service.description.txt); - } - - // display the job id in the tab title - function setBrowserTitle(title) { - document.title = `${title} - Kibana`; - } - - class JobSelectService { - constructor() { - this.jobIds = []; - this.groupIds = []; - this.description = { txt: '' }; - this.singleJobDescription = { txt: '' }; - this.jobSelectListState = { - applyTimeRange: true - }; - this.jobIdsWithGroup = []; - this.splitJobId = splitJobId; - } - - // Broadcasts that a change has been made to the selected jobs. - broadcastJobSelectionChange() { - $rootScope.$broadcast('jobSelectionChange', this.getSelectedJobIds()); - } - - // Add a listener for changes to the selected jobs. - listenJobSelectionChange(scope, callback) { - const handler = $rootScope.$on('jobSelectionChange', callback); - scope.$on('$destroy', handler); - } - - // called externally to retrieve the selected jobs ids. - // passing in `true` will load the jobs ids from the URL first - getSelectedJobIds(loadFromURL) { - if (loadFromURL) { - processIds(this, loadJobIdsFromGlobalState()); - } - return this.jobIds; - } - - // called externally to set the job ids. - // job ids are added to the URL and an event is broadcast for anything listening. - // e.g. the anomaly explorer or time series explorer. - // currently only called by the jobs selection menu. - setJobIds(jobIds) { - processIds(this, jobIds); - storeJobIdsInGlobalState(jobIds); - this.broadcastJobSelectionChange(); - } - } - - if (jobSelectService === undefined) { - jobSelectService = new JobSelectService(); - } - return jobSelectService; -} diff --git a/x-pack/plugins/ml/public/components/job_selector/id_badges/id_badges.test.js b/x-pack/plugins/ml/public/components/job_selector/id_badges/id_badges.test.js new file mode 100644 index 0000000000000..5f94e89ad2ba5 --- /dev/null +++ b/x-pack/plugins/ml/public/components/job_selector/id_badges/id_badges.test.js @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + +import React from 'react'; +import { cleanup, render } from 'react-testing-library'; +import { IdBadges } from './id_badges'; + + + +const props = { + limit: 2, + maps: { + groupsMap: { + 'group1': ['job1', 'job2'], + 'group2': ['job3'] + }, + jobsMap: { + 'job1': ['group1'], + 'job2': ['group1'], + 'job3': ['group2'], + 'job4': [] + } + }, + onLinkClick: jest.fn(), + selectedIds: ['group1', 'job1', 'job3'], + showAllBarBadges: false +}; + +const overLimitProps = { ...props, selectedIds: ['group1', 'job1', 'job3', 'job4'], }; + +describe('IdBadges', () => { + afterEach(cleanup); + + test('When group selected renders groupId and not corresponding jobIds', () => { + const { getByText, queryByText } = render(); + // group1 badge should be present + const groupId = getByText(/group1/); + expect(groupId).toBeDefined(); + // job1 is in group1 so it should not show up since group1 is selected + const jobId = queryByText(/job1/); + expect(jobId).toBeNull(); + }); + + describe('showAllBarBadges is false', () => { + + test('shows link to show more badges if selection is over limit', () => { + const { getByText } = render(); + const showMoreLink = getByText('And 1 more'); + expect(showMoreLink).toBeDefined(); + }); + + test('does not show link to show more badges if selection is under limit', () => { + const { queryByText } = render(); + const showMoreLink = queryByText(/ more/); + expect(showMoreLink).toBeNull(); + }); + + }); + + describe('showAllBarBadges is true', () => { + const overLimitShowAllProps = { + ...props, + showAllBarBadges: true, + selectedIds: ['group1', 'job1', 'job3', 'job4'] + }; + + test('shows all badges when selection is over limit', () => { + const { getByText } = render(); + const group1 = getByText(/group1/); + const job3 = getByText(/job3/); + const job4 = getByText(/job4/); + expect(group1).toBeDefined(); + expect(job3).toBeDefined(); + expect(job4).toBeDefined(); + }); + + test('shows hide link when selection is over limit', () => { + const { getByText, queryByText } = render(); + const showMoreLink = queryByText(/ more/); + expect(showMoreLink).toBeNull(); + + const hideLink = getByText('Hide'); + expect(hideLink).toBeDefined(); + }); + + }); + +}); diff --git a/x-pack/plugins/ml/public/components/job_selector/job_selector_table/job_selector_table.test.js b/x-pack/plugins/ml/public/components/job_selector/job_selector_table/job_selector_table.test.js index bc65543ad5b49..044fa3bd4c4fe 100644 --- a/x-pack/plugins/ml/public/components/job_selector/job_selector_table/job_selector_table.test.js +++ b/x-pack/plugins/ml/public/components/job_selector/job_selector_table/job_selector_table.test.js @@ -84,7 +84,19 @@ const props = { fromPx: 1, label: 'Apr 17th 2019, 20:04 to May 18th 2019, 19:45', widthPx: 93.1 - } + }, + }, + { + groups: ['test'], + id: 'non-timeseries-job', + isRunning: false, + isSingleMetricViewerJob: false, + job_id: 'non-timeseries-job', + timeRange: { + fromPx: 1, + label: 'Apr 17th 2019, 20:04 to May 18th 2019, 19:45', + widthPx: 93.1 + }, } ], onSelection: jest.fn(), @@ -110,6 +122,13 @@ describe('JobSelectorTable', () => { expect(radioButton.firstChild.checked).toEqual(true); }); + test('job cannot be selected if it is not a single metric viewer job', () => { + const timeseriesOnlyProps = { ...props, singleSelection: 'true', timeseriesOnly: 'true' }; + const { getByTestId } = render(); + const radioButton = getByTestId('non-timeseries-job-radio-button'); + expect(radioButton.firstChild.disabled).toEqual(true); + }); + }); describe('Not Single Selection', () => { diff --git a/x-pack/plugins/ml/public/components/job_selector/new_selection_id_badges/new_selection_id_badges.test.js b/x-pack/plugins/ml/public/components/job_selector/new_selection_id_badges/new_selection_id_badges.test.js new file mode 100644 index 0000000000000..a9e07c7e15f46 --- /dev/null +++ b/x-pack/plugins/ml/public/components/job_selector/new_selection_id_badges/new_selection_id_badges.test.js @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + +import React from 'react'; +import { cleanup, render } from 'react-testing-library'; +import { NewSelectionIdBadges } from './new_selection_id_badges'; + + + +const props = { + limit: 2, + maps: { + groupsMap: { + 'group1': ['job1', 'job2'], + 'group2': ['job3'] + } + }, + onLinkClick: jest.fn(), + onDeleteClick: jest.fn(), + newSelection: ['group1', 'job1', 'job3'], + showAllBadges: false +}; + +describe('NewSelectionIdBadges', () => { + afterEach(cleanup); + + describe('showAllBarBadges is false', () => { + + test('shows link to show more badges if selection is over limit', () => { + const { getByText } = render(); + const showMoreLink = getByText('And 1 more'); + expect(showMoreLink).toBeDefined(); + }); + + test('does not show link to show more badges if selection is within limit', () => { + const underLimitProps = { ...props, newSelection: ['group1', 'job1'], }; + const { queryByText } = render(); + const showMoreLink = queryByText(/ more/); + expect(showMoreLink).toBeNull(); + }); + + }); + + describe('showAllBarBadges is true', () => { + const showAllTrueProps = { + ...props, + showAllBadges: true + }; + + test('shows all badges when selection is over limit', () => { + const { getByText } = render(); + const group1 = getByText(/group1/); + const job1 = getByText(/job1/); + const job3 = getByText(/job3/); + expect(group1).toBeDefined(); + expect(job1).toBeDefined(); + expect(job3).toBeDefined(); + }); + + test('shows hide link when selection is over limit', () => { + const { getByText, queryByText } = render(); + const showMoreLink = queryByText(/ more/); + expect(showMoreLink).toBeNull(); + + const hideLink = getByText('Hide'); + expect(hideLink).toBeDefined(); + }); + + }); + +}); diff --git a/x-pack/plugins/ml/public/components/json_tooltip/tooltips.js b/x-pack/plugins/ml/public/components/json_tooltip/tooltips.js index 017a399ffa9b3..0728f68bb1ba0 100644 --- a/x-pack/plugins/ml/public/components/json_tooltip/tooltips.js +++ b/x-pack/plugins/ml/public/components/json_tooltip/tooltips.js @@ -269,6 +269,12 @@ export const getTooltips = () => { defaultMessage: 'A string that is a unique identifier to a list. Only applicable and required when conditionType is categorical.' }) }, + forecasting_modal_run_duration: { + text: i18n.translate('xpack.ml.tooltips.forecastingModalRunDurationTooltip', { + defaultMessage: 'Length of forecast, up to a maximum of 3650 days. ' + + 'Use s for seconds, m for minutes, h for hours, d for days, w for weeks.' + }) + }, forecasting_modal_view_list: { text: i18n.translate('xpack.ml.tooltips.forecastingModalViewListTooltip', { defaultMessage: 'Lists a maximum of five of the most recently run forecasts.' 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 9e09cb9d83531..97abaa3589cf4 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/_index.scss b/x-pack/plugins/ml/public/data_frame/_index.scss index f5f1f834b3e45..5f3de5b89293e 100644 --- a/x-pack/plugins/ml/public/data_frame/_index.scss +++ b/x-pack/plugins/ml/public/data_frame/_index.scss @@ -1 +1,2 @@ +@import 'components/aggregation_list/index'; @import 'components/group_by_list/index'; diff --git a/x-pack/plugins/ml/public/data_frame/common/kibana_context.ts b/x-pack/plugins/ml/public/data_frame/common/kibana_context.ts index 04771d9ab787f..94e8986753f50 100644 --- a/x-pack/plugins/ml/public/data_frame/common/kibana_context.ts +++ b/x-pack/plugins/ml/public/data_frame/common/kibana_context.ts @@ -6,14 +6,19 @@ import React from 'react'; -import { StaticIndexPattern } from 'ui/index_patterns'; +import { IndexPattern } from 'ui/index_patterns'; -interface KibanaContextValue { - currentIndexPattern: StaticIndexPattern; +export interface KibanaContextValue { + combinedQuery: any; + currentIndexPattern: IndexPattern; + currentSavedSearch: any; indexPatterns: any; + kbnBaseUrl: string; kibanaConfig: any; } +export type SavedSearchQuery = object; + // Because we're only getting the actual contextvalue within a wrapping angular component, // we need to initialize here with `null` because TypeScript doesn't allow createContext() // without a default value. The nullable union type takes care of allowing @@ -23,8 +28,11 @@ export const KibanaContext = React.createContext(nul export function isKibanaContext(arg: any): arg is KibanaContextValue { return ( + arg.combinedQuery !== undefined && arg.currentIndexPattern !== undefined && + arg.currentSavedSearch !== undefined && arg.indexPatterns !== undefined && + typeof arg.kbnBaseUrl === 'string' && arg.kibanaConfig !== undefined ); } diff --git a/x-pack/plugins/ml/public/data_frame/common/navigation.ts b/x-pack/plugins/ml/public/data_frame/common/navigation.ts index 4f3d886ce3a98..2c4f94b811f51 100644 --- a/x-pack/plugins/ml/public/data_frame/common/navigation.ts +++ b/x-pack/plugins/ml/public/data_frame/common/navigation.ts @@ -4,6 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ +import rison from 'rison-node'; + +import chrome from 'ui/chrome'; + export function moveToDataFrameWizard() { - window.location.href = `#/data_frames/new_job`; + window.location.href = '#/data_frames/new_job'; +} + +export function moveToDataFrameJobsList() { + window.location.href = '#/data_frames'; +} + +export function moveToDiscover(indexPatternId: string, kbnBaseUrl: string) { + const _g = rison.encode({}); + + // Add the index pattern ID to the appState part of the URL. + const _a = rison.encode({ + index: indexPatternId, + }); + + const baseUrl = chrome.addBasePath(kbnBaseUrl); + const hash = `#/discover?_g=${_g}&_a=${_a}`; + + window.location.href = `${baseUrl}${hash}`; } diff --git a/x-pack/plugins/ml/public/data_frame/common/pivot_aggs.ts b/x-pack/plugins/ml/public/data_frame/common/pivot_aggs.ts index 4ee09d85bee3d..1666d439cc106 100644 --- a/x-pack/plugins/ml/public/data_frame/common/pivot_aggs.ts +++ b/x-pack/plugins/ml/public/data_frame/common/pivot_aggs.ts @@ -32,11 +32,8 @@ export const pivotAggsFieldSupport = { [KBN_FIELD_TYPES.ATTACHMENT]: [PIVOT_SUPPORTED_AGGS.VALUE_COUNT], [KBN_FIELD_TYPES.BOOLEAN]: [PIVOT_SUPPORTED_AGGS.VALUE_COUNT], [KBN_FIELD_TYPES.DATE]: [ - PIVOT_SUPPORTED_AGGS.AVG, - PIVOT_SUPPORTED_AGGS.CARDINALITY, PIVOT_SUPPORTED_AGGS.MAX, PIVOT_SUPPORTED_AGGS.MIN, - PIVOT_SUPPORTED_AGGS.SUM, PIVOT_SUPPORTED_AGGS.VALUE_COUNT, ], [KBN_FIELD_TYPES.GEO_POINT]: [PIVOT_SUPPORTED_AGGS.VALUE_COUNT], @@ -70,6 +67,7 @@ export interface PivotAggsConfig { agg: PIVOT_SUPPORTED_AGGS; field: FieldName; aggName: AggName; + dropDownName: string; } export type PivotAggsConfigDict = Dictionary; diff --git a/x-pack/plugins/ml/public/data_frame/common/pivot_group_by.ts b/x-pack/plugins/ml/public/data_frame/common/pivot_group_by.ts index 133ccde8f44b2..3f571fb5b0367 100644 --- a/x-pack/plugins/ml/public/data_frame/common/pivot_group_by.ts +++ b/x-pack/plugins/ml/public/data_frame/common/pivot_group_by.ts @@ -49,6 +49,7 @@ export const pivotGroupByFieldSupport = { interface GroupByConfigBase { field: FieldName; aggName: AggName; + dropDownName: string; } // Don't allow an interval of '0', but allow a float interval of '0.1' with a leading zero. @@ -69,7 +70,7 @@ export enum DATE_HISTOGRAM_FORMAT { interface GroupByDateHistogram extends GroupByConfigBase { agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.DATE_HISTOGRAM; format?: DATE_HISTOGRAM_FORMAT; - interval: string; + calendar_interval: string; } interface GroupByHistogram extends GroupByConfigBase { @@ -85,7 +86,11 @@ export type GroupByConfigWithInterval = GroupByDateHistogram | GroupByHistogram; export type PivotGroupByConfig = GroupByDateHistogram | GroupByHistogram | GroupByTerms; export type PivotGroupByConfigDict = Dictionary; -export function groupByConfigHasInterval(arg: any): arg is GroupByConfigWithInterval { +export function isGroupByDateHistogram(arg: any): arg is GroupByDateHistogram { + return arg.hasOwnProperty('calendar_interval'); +} + +export function isGroupByHistogram(arg: any): arg is GroupByHistogram { return arg.hasOwnProperty('interval'); } @@ -106,7 +111,7 @@ export interface DateHistogramAgg { date_histogram: { field: FieldName; format?: DATE_HISTOGRAM_FORMAT; - interval: string; + calendar_interval: string; }; } diff --git a/x-pack/plugins/ml/public/data_frame/common/request.test.ts b/x-pack/plugins/ml/public/data_frame/common/request.test.ts index 708c44a74da4c..2f16f2494c90a 100644 --- a/x-pack/plugins/ml/public/data_frame/common/request.test.ts +++ b/x-pack/plugins/ml/public/data_frame/common/request.test.ts @@ -11,9 +11,32 @@ import { DefinePivotExposedState } from '../components/define_pivot/define_pivot import { PIVOT_SUPPORTED_GROUP_BY_AGGS } from './pivot_group_by'; import { PivotAggsConfig, PIVOT_SUPPORTED_AGGS } from './pivot_aggs'; -import { getDataFramePreviewRequest, getDataFrameRequest, getPivotQuery } from './request'; +import { + getDataFramePreviewRequest, + getDataFrameRequest, + getPivotQuery, + isDefaultQuery, + isSimpleQuery, + PivotQuery, +} from './request'; + +const defaultQuery: PivotQuery = { query_string: { query: '*' } }; +const matchAllQuery: PivotQuery = { match_all: {} }; +const simpleQuery: PivotQuery = { query_string: { query: 'airline:AAL' } }; describe('Data Frame: Common', () => { + test('isSimpleQuery()', () => { + expect(isSimpleQuery(defaultQuery)).toBe(true); + expect(isSimpleQuery(matchAllQuery)).toBe(false); + expect(isSimpleQuery(simpleQuery)).toBe(true); + }); + + test('isDefaultQuery()', () => { + expect(isDefaultQuery(defaultQuery)).toBe(true); + expect(isDefaultQuery(matchAllQuery)).toBe(false); + expect(isDefaultQuery(simpleQuery)).toBe(false); + }); + test('getPivotQuery()', () => { const query = getPivotQuery('the-query'); @@ -31,18 +54,24 @@ describe('Data Frame: Common', () => { { agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, field: 'the-group-by-field', - aggName: 'the-group-by-label', + aggName: 'the-group-by-agg-name', + dropDownName: 'the-group-by-drop-down-name', }, ]; const aggs: PivotAggsConfig[] = [ - { agg: PIVOT_SUPPORTED_AGGS.AVG, field: 'the-agg-field', aggName: 'the-agg-label' }, + { + agg: PIVOT_SUPPORTED_AGGS.AVG, + field: 'the-agg-field', + aggName: 'the-agg-agg-name', + dropDownName: 'the-agg-drop-down-name', + }, ]; const request = getDataFramePreviewRequest('the-index-pattern-title', query, groupBy, aggs); expect(request).toEqual({ pivot: { - aggregations: { 'the-agg-label': { avg: { field: 'the-agg-field' } } }, - group_by: { 'the-group-by-label': { terms: { field: 'the-group-by-field' } } }, + aggregations: { 'the-agg-agg-name': { avg: { field: 'the-agg-field' } } }, + group_by: { 'the-group-by-agg-name': { terms: { field: 'the-group-by-field' } } }, }, source: { index: 'the-index-pattern-title', @@ -55,12 +84,14 @@ describe('Data Frame: Common', () => { const groupBy: PivotGroupByConfig = { agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, field: 'the-group-by-field', - aggName: 'the-group-by-label', + aggName: 'the-group-by-agg-name', + dropDownName: 'the-group-by-drop-down-name', }; const agg: PivotAggsConfig = { agg: PIVOT_SUPPORTED_AGGS.AVG, field: 'the-agg-field', - aggName: 'the-agg-label', + aggName: 'the-agg-agg-name', + dropDownName: 'the-agg-drop-down-name', }; const pivotState: DefinePivotExposedState = { aggList: { 'the-agg-name': agg }, @@ -81,8 +112,8 @@ describe('Data Frame: Common', () => { expect(request).toEqual({ dest: { index: 'the-target-index' }, pivot: { - aggregations: { 'the-agg-label': { avg: { field: 'the-agg-field' } } }, - group_by: { 'the-group-by-label': { terms: { field: 'the-group-by-field' } } }, + aggregations: { 'the-agg-agg-name': { avg: { field: 'the-agg-field' } } }, + group_by: { 'the-group-by-agg-name': { terms: { field: 'the-group-by-field' } } }, }, source: { index: 'the-index-pattern-title', diff --git a/x-pack/plugins/ml/public/data_frame/common/request.ts b/x-pack/plugins/ml/public/data_frame/common/request.ts index d0edc627d06af..14aa1cded8ce9 100644 --- a/x-pack/plugins/ml/public/data_frame/common/request.ts +++ b/x-pack/plugins/ml/public/data_frame/common/request.ts @@ -6,7 +6,7 @@ import { DefaultOperator } from 'elasticsearch'; -import { StaticIndexPattern } from 'ui/index_patterns'; +import { IndexPattern } from 'ui/index_patterns'; import { dictionaryToArray } from '../../../common/types/common'; @@ -23,6 +23,7 @@ import { import { PivotAggDict, PivotAggsConfig } from './pivot_aggs'; import { DateHistogramAgg, HistogramAgg, PivotGroupByDict, TermsAgg } from './pivot_group_by'; +import { SavedSearchQuery } from './kibana_context'; export interface DataFramePreviewRequest { pivot: { @@ -52,25 +53,38 @@ export interface SimpleQuery { }; } -export function getPivotQuery(search: string): SimpleQuery { - return { - query_string: { - query: search, - default_operator: 'AND', - }, - }; +export type PivotQuery = SimpleQuery | SavedSearchQuery; + +export function getPivotQuery(search: string | SavedSearchQuery): PivotQuery { + if (typeof search === 'string') { + return { + query_string: { + query: search, + default_operator: 'AND', + }, + }; + } + + return search; +} + +export function isSimpleQuery(arg: any): arg is SimpleQuery { + return arg.query_string !== undefined; +} + +export function isDefaultQuery(query: PivotQuery): boolean { + return isSimpleQuery(query) && query.query_string.query === '*'; } export function getDataFramePreviewRequest( - indexPatternTitle: StaticIndexPattern['title'], - query: SimpleQuery, + indexPatternTitle: IndexPattern['title'], + query: PivotQuery, groupBy: PivotGroupByConfig[], aggs: PivotAggsConfig[] ): DataFramePreviewRequest { const request: DataFramePreviewRequest = { source: { index: indexPatternTitle, - query, }, pivot: { group_by: {}, @@ -78,6 +92,10 @@ export function getDataFramePreviewRequest( }, }; + if (!isDefaultQuery(query)) { + request.source.query = query; + } + groupBy.forEach(g => { if (g.agg === PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS) { const termsAgg: TermsAgg = { @@ -98,7 +116,7 @@ export function getDataFramePreviewRequest( const dateHistogramAgg: DateHistogramAgg = { date_histogram: { field: g.field, - interval: g.interval, + calendar_interval: g.calendar_interval, }, }; @@ -106,7 +124,7 @@ export function getDataFramePreviewRequest( // date_histrogram aggregation formats like 'yyyy-MM-dd'. The following code extracts // the interval unit from the configurations interval and adds a matching // aggregation format to the configuration. - const timeUnitMatch = g.interval.match(dateHistogramIntervalFormatRegex); + const timeUnitMatch = g.calendar_interval.match(dateHistogramIntervalFormatRegex); if (timeUnitMatch !== null && Array.isArray(timeUnitMatch) && timeUnitMatch.length === 2) { // the following is just a TS compatible way of using the // matched string like `d` as the property to access the enum. @@ -132,7 +150,7 @@ export function getDataFramePreviewRequest( } export function getDataFrameRequest( - indexPatternTitle: StaticIndexPattern['title'], + indexPatternTitle: IndexPattern['title'], pivotState: DefinePivotExposedState, jobDetailsState: JobDetailsExposedState ): DataFrameRequest { diff --git a/x-pack/plugins/ml/public/data_frame/components/aggregation_list/__snapshots__/agg_label_form.test.tsx.snap b/x-pack/plugins/ml/public/data_frame/components/aggregation_list/__snapshots__/agg_label_form.test.tsx.snap index 46356bd135b7c..699ea54a22534 100644 --- a/x-pack/plugins/ml/public/data_frame/components/aggregation_list/__snapshots__/agg_label_form.test.tsx.snap +++ b/x-pack/plugins/ml/public/data_frame/components/aggregation_list/__snapshots__/agg_label_form.test.tsx.snap @@ -4,11 +4,19 @@ exports[`Data Frame: Date histogram aggregation 1`] = ` - - the-group-by-label + + + the-group-by-agg-name + Date histogram aggregation 1`] = ` defaultData={ Object { "agg": "cardinality", - "aggName": "the-group-by-label", + "aggName": "the-group-by-agg-name", + "dropDownName": "the-group-by-drop-down-name", "field": "the-group-by-field", } } @@ -46,6 +55,7 @@ exports[`Data Frame: Date histogram aggregation 1`] = ` Minimal initialization 1`] = ` hasShadow={false} paddingSize="s" > - - - - - + Minimal initialization 1`] = ` hasShadow={false} paddingSize="s" > - the-agg +
+ the-agg +
Minimal initialization 1`] = ` > ', () => { const item: PivotAggsConfig = { agg: PIVOT_SUPPORTED_AGGS.CARDINALITY, field: 'the-group-by-field', - aggName: 'the-group-by-label', + aggName: 'the-group-by-agg-name', + dropDownName: 'the-group-by-drop-down-name', }; const props = { item, diff --git a/x-pack/plugins/ml/public/data_frame/components/aggregation_list/agg_label_form.tsx b/x-pack/plugins/ml/public/data_frame/components/aggregation_list/agg_label_form.tsx index 508585af1d6dd..c602407ae68c3 100644 --- a/x-pack/plugins/ml/public/data_frame/components/aggregation_list/agg_label_form.tsx +++ b/x-pack/plugins/ml/public/data_frame/components/aggregation_list/agg_label_form.tsx @@ -37,9 +37,11 @@ export const AggLabelForm: React.SFC = ({ } return ( - - {item.aggName} - + + + {item.aggName} + + = ({ /> - + ', () => { const item: PivotAggsConfig = { agg: PIVOT_SUPPORTED_AGGS.AVG, field: 'the-field', - aggName: 'the-form-row-label', + aggName: 'the-group-by-agg-name', + dropDownName: 'the-group-by-drop-down-name', }; const props: ListProps = { list: { 'the-agg': item }, diff --git a/x-pack/plugins/ml/public/data_frame/components/aggregation_list/list_form.tsx b/x-pack/plugins/ml/public/data_frame/components/aggregation_list/list_form.tsx index f9832cb64b295..ef3683d15cd0d 100644 --- a/x-pack/plugins/ml/public/data_frame/components/aggregation_list/list_form.tsx +++ b/x-pack/plugins/ml/public/data_frame/components/aggregation_list/list_form.tsx @@ -6,7 +6,7 @@ import React, { Fragment } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui'; +import { EuiPanel, EuiSpacer } from '@elastic/eui'; import { AggName, PivotAggsConfig, PivotAggsConfigDict } from '../../common'; @@ -28,17 +28,13 @@ export const AggListForm: React.SFC = ({ deleteHandler, list, onChang return ( - - - onChange(aggName, item)} - otherAggNames={otherAggNames} - options={options} - /> - - + onChange(aggName, item)} + otherAggNames={otherAggNames} + options={options} + /> {listKeys.length > 0 && } diff --git a/x-pack/plugins/ml/public/data_frame/components/aggregation_list/list_summary.test.tsx b/x-pack/plugins/ml/public/data_frame/components/aggregation_list/list_summary.test.tsx index ffa45cdf43c51..e23969cef47b1 100644 --- a/x-pack/plugins/ml/public/data_frame/components/aggregation_list/list_summary.test.tsx +++ b/x-pack/plugins/ml/public/data_frame/components/aggregation_list/list_summary.test.tsx @@ -16,7 +16,8 @@ describe('Data Frame: ', () => { const item: PivotAggsConfig = { agg: PIVOT_SUPPORTED_AGGS.AVG, field: 'the-field', - aggName: 'the-form-row-label', + aggName: 'the-group-by-agg-name', + dropDownName: 'the-group-by-drop-down-name', }; const props: AggListSummaryProps = { list: { 'the-agg': item }, diff --git a/x-pack/plugins/ml/public/data_frame/components/aggregation_list/list_summary.tsx b/x-pack/plugins/ml/public/data_frame/components/aggregation_list/list_summary.tsx index 7d3071e129920..ec9a947303780 100644 --- a/x-pack/plugins/ml/public/data_frame/components/aggregation_list/list_summary.tsx +++ b/x-pack/plugins/ml/public/data_frame/components/aggregation_list/list_summary.tsx @@ -20,7 +20,9 @@ export const AggListSummary: React.SFC = ({ list }) => { {aggNames.map((aggName: AggName) => ( - {aggName} + +
{aggName}
+
{aggNames.length > 0 && }
))} diff --git a/x-pack/plugins/ml/public/data_frame/components/aggregation_list/popover_form.test.tsx b/x-pack/plugins/ml/public/data_frame/components/aggregation_list/popover_form.test.tsx index e20a813cec118..d70dd657ed1d3 100644 --- a/x-pack/plugins/ml/public/data_frame/components/aggregation_list/popover_form.test.tsx +++ b/x-pack/plugins/ml/public/data_frame/components/aggregation_list/popover_form.test.tsx @@ -15,7 +15,8 @@ describe('Data Frame: Aggregation ', () => { test('Minimal initialization', () => { const defaultData: PivotAggsConfig = { agg: PIVOT_SUPPORTED_AGGS.CARDINALITY, - aggName: 'the-agg-name', + aggName: 'the-group-by-agg-name', + dropDownName: 'the-group-by-drop-down-name', field: 'the-field', }; const otherAggNames: AggName[] = []; diff --git a/x-pack/plugins/ml/public/data_frame/components/define_pivot/__snapshots__/define_pivot_form.test.tsx.snap b/x-pack/plugins/ml/public/data_frame/components/define_pivot/__snapshots__/define_pivot_form.test.tsx.snap index a9599693d4556..5c8717ba9e0e2 100644 --- a/x-pack/plugins/ml/public/data_frame/components/define_pivot/__snapshots__/define_pivot_form.test.tsx.snap +++ b/x-pack/plugins/ml/public/data_frame/components/define_pivot/__snapshots__/define_pivot_form.test.tsx.snap @@ -5,11 +5,15 @@ exports[`Data Frame: Minimal initialization 1`] = ` Minimal initialization 1`] = ` Minimal initialization 1`] = ` Object { "the-agg-name": Object { "agg": "avg", - "aggName": "the-agg-label", + "aggName": "the-group-by-agg-name", + "dropDownName": "the-group-by-drop-down-name", "field": "the-agg-field", }, } @@ -28,7 +33,8 @@ exports[`Data Frame: Minimal initialization 1`] = ` Object { "the-group-by-name": Object { "agg": "terms", - "aggName": "the-group-by-label", + "aggName": "the-group-by-agg-name", + "dropDownName": "the-group-by-drop-down-name", "field": "the-group-by-field", }, } diff --git a/x-pack/plugins/ml/public/data_frame/components/define_pivot/__snapshots__/pivot_preview.test.tsx.snap b/x-pack/plugins/ml/public/data_frame/components/define_pivot/__snapshots__/pivot_preview.test.tsx.snap index 2b34bfbb97d11..e97a9695101d4 100644 --- a/x-pack/plugins/ml/public/data_frame/components/define_pivot/__snapshots__/pivot_preview.test.tsx.snap +++ b/x-pack/plugins/ml/public/data_frame/components/define_pivot/__snapshots__/pivot_preview.test.tsx.snap @@ -5,11 +5,15 @@ exports[`Data Frame: Minimal initialization 1`] = ` Minimal initialization 1`] = ` Object { "the-agg-name": Object { "agg": "avg", - "aggName": "the-agg-label", + "aggName": "the-group-by-agg-name", + "dropDownName": "the-group-by-drop-down-name", "field": "the-agg-field", }, } @@ -28,7 +33,8 @@ exports[`Data Frame: Minimal initialization 1`] = ` Object { "the-group-by-name": Object { "agg": "terms", - "aggName": "the-group-by-label", + "aggName": "the-group-by-agg-name", + "dropDownName": "the-group-by-drop-down-name", "field": "the-group-by-field", }, } diff --git a/x-pack/plugins/ml/public/data_frame/components/define_pivot/common.test.ts b/x-pack/plugins/ml/public/data_frame/components/define_pivot/common.test.ts index 735a71d71759a..507a3f9a8d9a0 100644 --- a/x-pack/plugins/ml/public/data_frame/components/define_pivot/common.test.ts +++ b/x-pack/plugins/ml/public/data_frame/components/define_pivot/common.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { StaticIndexPattern } from 'ui/index_patterns'; +import { IndexPattern } from 'ui/index_patterns'; import { getDataFramePreviewRequest, @@ -19,9 +19,21 @@ import { getPivotPreviewDevConsoleStatement, getPivotDropdownOptions } from './c describe('Data Frame: Define Pivot Common', () => { test('getPivotDropdownOptions()', () => { - const indexPattern: StaticIndexPattern = { + // The field name includes the characters []> as well as a leading and ending space charcter + // which cannot be used for aggregation names. The test results verifies that the characters + // should still be present in field and dropDownName values, but should be stripped for aggName values. + const indexPattern: IndexPattern = { + id: 'the-index-pattern-id', title: 'the-index-pattern-title', - fields: [{ name: 'the-field', type: 'number', aggregatable: true, searchable: true }], + fields: [ + { + name: ' the-f[i]e>ld ', + type: 'number', + aggregatable: true, + filterable: true, + searchable: true, + }, + ], }; const options = getPivotDropdownOptions(indexPattern); @@ -29,39 +41,62 @@ describe('Data Frame: Define Pivot Common', () => { expect(options).toEqual({ aggOptions: [ { - label: 'the-field', + label: ' the-f[i]e>ld ', options: [ - { label: 'avg(the-field)' }, - { label: 'cardinality(the-field)' }, - { label: 'max(the-field)' }, - { label: 'min(the-field)' }, - { label: 'sum(the-field)' }, - { label: 'value_count(the-field)' }, + { label: 'avg( the-f[i]e>ld )' }, + { label: 'cardinality( the-f[i]e>ld )' }, + { label: 'max( the-f[i]e>ld )' }, + { label: 'min( the-f[i]e>ld )' }, + { label: 'sum( the-f[i]e>ld )' }, + { label: 'value_count( the-f[i]e>ld )' }, ], }, ], aggOptionsData: { - 'avg(the-field)': { agg: 'avg', field: 'the-field', aggName: 'avg(the-field)' }, - 'cardinality(the-field)': { + 'avg( the-f[i]e>ld )': { + agg: 'avg', + field: ' the-f[i]e>ld ', + aggName: 'the-field.avg', + dropDownName: 'avg( the-f[i]e>ld )', + }, + 'cardinality( the-f[i]e>ld )': { agg: 'cardinality', - field: 'the-field', - aggName: 'cardinality(the-field)', + field: ' the-f[i]e>ld ', + aggName: 'the-field.cardinality', + dropDownName: 'cardinality( the-f[i]e>ld )', + }, + 'max( the-f[i]e>ld )': { + agg: 'max', + field: ' the-f[i]e>ld ', + aggName: 'the-field.max', + dropDownName: 'max( the-f[i]e>ld )', }, - 'max(the-field)': { agg: 'max', field: 'the-field', aggName: 'max(the-field)' }, - 'min(the-field)': { agg: 'min', field: 'the-field', aggName: 'min(the-field)' }, - 'sum(the-field)': { agg: 'sum', field: 'the-field', aggName: 'sum(the-field)' }, - 'value_count(the-field)': { + 'min( the-f[i]e>ld )': { + agg: 'min', + field: ' the-f[i]e>ld ', + aggName: 'the-field.min', + dropDownName: 'min( the-f[i]e>ld )', + }, + 'sum( the-f[i]e>ld )': { + agg: 'sum', + field: ' the-f[i]e>ld ', + aggName: 'the-field.sum', + dropDownName: 'sum( the-f[i]e>ld )', + }, + 'value_count( the-f[i]e>ld )': { agg: 'value_count', - field: 'the-field', - aggName: 'value_count(the-field)', + field: ' the-f[i]e>ld ', + aggName: 'the-field.value_count', + dropDownName: 'value_count( the-f[i]e>ld )', }, }, - groupByOptions: [{ label: 'histogram(the-field)' }], + groupByOptions: [{ label: 'histogram( the-f[i]e>ld )' }], groupByOptionsData: { - 'histogram(the-field)': { + 'histogram( the-f[i]e>ld )': { agg: 'histogram', - field: 'the-field', - aggName: 'histogram(the-field)', + field: ' the-f[i]e>ld ', + aggName: 'the-field', + dropDownName: 'histogram( the-f[i]e>ld )', interval: '10', }, }, @@ -78,12 +113,14 @@ describe('Data Frame: Define Pivot Common', () => { const groupBy: PivotGroupByConfig = { agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, field: 'the-group-by-field', - aggName: 'the-group-by-label', + aggName: 'the-group-by-agg-name', + dropDownName: 'the-group-by-drop-down-name', }; const agg: PivotAggsConfig = { agg: PIVOT_SUPPORTED_AGGS.AVG, field: 'the-agg-field', - aggName: 'the-agg-label', + aggName: 'the-agg-agg-name', + dropDownName: 'the-agg-drop-down-name', }; const request = getDataFramePreviewRequest('the-index-pattern-title', query, [groupBy], [agg]); const pivotPreviewDevConsoleStatement = getPivotPreviewDevConsoleStatement(request); @@ -91,24 +128,18 @@ describe('Data Frame: Define Pivot Common', () => { expect(pivotPreviewDevConsoleStatement).toBe(`POST _data_frame/transforms/_preview { "source": { - "index": "the-index-pattern-title", - "query": { - "query_string": { - "query": "*", - "default_operator": "AND" - } - } + "index": "the-index-pattern-title" }, "pivot": { "group_by": { - "the-group-by-label": { + "the-group-by-agg-name": { "terms": { "field": "the-group-by-field" } } }, "aggregations": { - "the-agg-label": { + "the-agg-agg-name": { "avg": { "field": "the-agg-field" } 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 d33e38a26a78b..5d8b1c6625659 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 @@ -6,7 +6,7 @@ import { EuiComboBoxOptionProps } from '@elastic/eui'; -import { StaticIndexPattern } from 'ui/index_patterns'; +import { IndexPattern } from 'ui/index_patterns'; import { KBN_FIELD_TYPES } from '../../../../common/constants/field_types'; @@ -17,6 +17,7 @@ import { FieldName, PivotAggsConfigDict, pivotAggsFieldSupport, + PivotGroupByConfig, PivotGroupByConfigDict, pivotGroupByFieldSupport, PIVOT_SUPPORTED_GROUP_BY_AGGS, @@ -29,20 +30,23 @@ export interface Field { function getDefaultGroupByConfig( aggName: string, + dropDownName: string, fieldName: FieldName, groupByAgg: PIVOT_SUPPORTED_GROUP_BY_AGGS -) { +): PivotGroupByConfig { switch (groupByAgg) { case PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS: return { agg: groupByAgg, aggName, + dropDownName, field: fieldName, }; case PIVOT_SUPPORTED_GROUP_BY_AGGS.HISTOGRAM: return { agg: groupByAgg, aggName, + dropDownName, field: fieldName, interval: '10', }; @@ -50,13 +54,16 @@ function getDefaultGroupByConfig( return { agg: groupByAgg, aggName, + dropDownName, field: fieldName, - interval: '1m', + calendar_interval: '1m', }; } } -export function getPivotDropdownOptions(indexPattern: StaticIndexPattern) { +const illegalEsAggNameChars = /[[\]>]/g; + +export function getPivotDropdownOptions(indexPattern: IndexPattern) { // The available group by options const groupByOptions: EuiComboBoxOptionProps[] = []; const groupByOptionsData: PivotGroupByConfigDict = {}; @@ -73,21 +80,36 @@ export function getPivotDropdownOptions(indexPattern: StaticIndexPattern) { fields.forEach(field => { // Group by const availableGroupByAggs = pivotGroupByFieldSupport[field.type]; - availableGroupByAggs.forEach(groupByAgg => { - const aggName = `${groupByAgg}(${field.name})`; - const groupByOption: DropDownLabel = { label: aggName }; - groupByOptions.push(groupByOption); - groupByOptionsData[aggName] = getDefaultGroupByConfig(aggName, 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 => { - const aggName = `${agg}(${field.name})`; - aggOption.options.push({ label: aggName }); - aggOptionsData[aggName] = { agg, field: field.name, aggName }; - }); + 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/data_frame/components/define_pivot/define_pivot_form.test.tsx b/x-pack/plugins/ml/public/data_frame/components/define_pivot/define_pivot_form.test.tsx index 1684867b6e991..390e703cdfe60 100644 --- a/x-pack/plugins/ml/public/data_frame/components/define_pivot/define_pivot_form.test.tsx +++ b/x-pack/plugins/ml/public/data_frame/components/define_pivot/define_pivot_form.test.tsx @@ -7,8 +7,14 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { KibanaContext } from '../../common'; -import { DefinePivotForm } from './define_pivot_form'; +import { + KibanaContext, + PivotAggsConfigDict, + PivotGroupByConfigDict, + PIVOT_SUPPORTED_AGGS, + PIVOT_SUPPORTED_GROUP_BY_AGGS, +} from '../../common'; +import { DefinePivotForm, isAggNameConflict } from './define_pivot_form'; // workaround to make React.memo() work with enzyme jest.mock('react', () => { @@ -19,6 +25,7 @@ jest.mock('react', () => { describe('Data Frame: ', () => { test('Minimal initialization', () => { const currentIndexPattern = { + id: 'the-index-pattern-id', title: 'the-index-pattern-title', fields: [], }; @@ -28,7 +35,14 @@ describe('Data Frame: ', () => { const wrapper = shallow(
{}} /> @@ -38,3 +52,72 @@ describe('Data Frame: ', () => { expect(wrapper).toMatchSnapshot(); }); }); + +describe('Data Frame: isAggNameConflict()', () => { + test('detect aggregation name conflicts', () => { + const aggList: PivotAggsConfigDict = { + 'the-agg-name': { + agg: PIVOT_SUPPORTED_AGGS.AVG, + field: 'the-field-name', + aggName: 'the-agg-name', + dropDownName: 'the-dropdown-name', + }, + 'the-namespaced-agg-name.namespace': { + agg: PIVOT_SUPPORTED_AGGS.AVG, + field: 'the-field-name', + aggName: 'the-namespaced-agg-name.namespace', + dropDownName: 'the-dropdown-name', + }, + }; + + const groupByList: PivotGroupByConfigDict = { + 'the-group-by-agg-name': { + agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, + field: 'the-field-name', + aggName: 'the-group-by-agg-name', + dropDownName: 'the-dropdown-name', + }, + 'the-namespaced-group-by-agg-name.namespace': { + agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, + field: 'the-field-name', + aggName: 'the-namespaced-group-by-agg-name.namespace', + dropDownName: 'the-dropdown-name', + }, + }; + + // no conflict, completely different name, no namespacing involved + expect(isAggNameConflict('the-other-agg-name', aggList, groupByList)).toBe(false); + // no conflict, completely different name and no conflicting namespace + expect(isAggNameConflict('the-other-agg-name.namespace', aggList, groupByList)).toBe(false); + + // exact match conflict on aggregation name + expect(isAggNameConflict('the-agg-name', aggList, groupByList)).toBe(true); + // namespace conflict with `the-agg-name` aggregation + expect(isAggNameConflict('the-agg-name.namespace', aggList, groupByList)).toBe(true); + + // exact match conflict on group-by name + expect(isAggNameConflict('the-group-by-agg-name', aggList, groupByList)).toBe(true); + // namespace conflict with `the-group-by-agg-name` group-by + expect(isAggNameConflict('the-group-by-agg-name.namespace', aggList, groupByList)).toBe(true); + + // exact match conflict on namespaced agg name + expect(isAggNameConflict('the-namespaced-agg-name.namespace', aggList, groupByList)).toBe(true); + // no conflict, same base agg name but different namespace + expect(isAggNameConflict('the-namespaced-agg-name.namespace2', aggList, groupByList)).toBe( + false + ); + // namespace conflict because the new agg name is base name of existing nested field + expect(isAggNameConflict('the-namespaced-agg-name', aggList, groupByList)).toBe(true); + + // exact match conflict on namespaced group-by name + expect( + isAggNameConflict('the-namespaced-group-by-agg-name.namespace', aggList, groupByList) + ).toBe(true); + // no conflict, same base group-by name but different namespace + expect( + isAggNameConflict('the-namespaced-group-by-agg-name.namespace2', aggList, groupByList) + ).toBe(false); + // namespace conflict because the new group-by name is base name of existing nested field + expect(isAggNameConflict('the-namespaced-group-by-agg-name', aggList, groupByList)).toBe(true); + }); +}); diff --git a/x-pack/plugins/ml/public/data_frame/components/define_pivot/define_pivot_form.tsx b/x-pack/plugins/ml/public/data_frame/components/define_pivot/define_pivot_form.tsx index f718238050ca6..94a6f590d6448 100644 --- a/x-pack/plugins/ml/public/data_frame/components/define_pivot/define_pivot_form.tsx +++ b/x-pack/plugins/ml/public/data_frame/components/define_pivot/define_pivot_form.tsx @@ -31,13 +31,16 @@ import { AggName, DropDownLabel, getPivotQuery, - groupByConfigHasInterval, + isGroupByDateHistogram, + isGroupByHistogram, isKibanaContext, KibanaContext, + KibanaContextValue, PivotAggsConfig, PivotAggsConfigDict, PivotGroupByConfig, PivotGroupByConfigDict, + SavedSearchQuery, } from '../../common'; import { getPivotDropdownOptions } from './common'; @@ -45,21 +48,119 @@ import { getPivotDropdownOptions } from './common'; export interface DefinePivotExposedState { aggList: PivotAggsConfigDict; groupByList: PivotGroupByConfigDict; - search: string; + search: string | SavedSearchQuery; valid: boolean; } const defaultSearch = '*'; const emptySearch = ''; -export function getDefaultPivotState(): DefinePivotExposedState { +export function getDefaultPivotState(kibanaContext: KibanaContextValue): DefinePivotExposedState { return { aggList: {} as PivotAggsConfigDict, groupByList: {} as PivotGroupByConfigDict, - search: defaultSearch, + search: + kibanaContext.currentSavedSearch.id !== undefined + ? kibanaContext.combinedQuery + : defaultSearch, valid: false, }; } +export function isAggNameConflict( + aggName: AggName, + aggList: PivotAggsConfigDict, + groupByList: PivotGroupByConfigDict +) { + if (aggList[aggName] !== undefined) { + toastNotifications.addDanger( + i18n.translate('xpack.ml.dataframe.definePivot.aggExistsErrorMessage', { + defaultMessage: `An aggregation configuration with the name '{aggName}' already exists.`, + values: { aggName }, + }) + ); + return true; + } + + if (groupByList[aggName] !== undefined) { + toastNotifications.addDanger( + i18n.translate('xpack.ml.dataframe.definePivot.groupByExistsErrorMessage', { + defaultMessage: `A group by configuration with the name '{aggName}' already exists.`, + values: { aggName }, + }) + ); + return true; + } + + let conflict = false; + + // check the new aggName against existing aggs and groupbys + const aggNameSplit = aggName.split('.'); + let aggNameCheck: string; + aggNameSplit.forEach(aggNamePart => { + aggNameCheck = aggNameCheck === undefined ? aggNamePart : `${aggNameCheck}.${aggNamePart}`; + if (aggList[aggNameCheck] !== undefined || groupByList[aggNameCheck] !== undefined) { + toastNotifications.addDanger( + i18n.translate('xpack.ml.dataframe.definePivot.nestedConflictErrorMessage', { + defaultMessage: `Couldn't add configuration '{aggName}' because of a nesting conflict with '{aggNameCheck}'.`, + values: { aggName, aggNameCheck }, + }) + ); + conflict = true; + } + }); + + if (conflict) { + return true; + } + + // check all aggs against new aggName + conflict = Object.keys(aggList).some(aggListName => { + const aggListNameSplit = aggListName.split('.'); + let aggListNameCheck: string; + return aggListNameSplit.some(aggListNamePart => { + aggListNameCheck = + aggListNameCheck === undefined ? aggListNamePart : `${aggListNameCheck}.${aggListNamePart}`; + if (aggListNameCheck === aggName) { + toastNotifications.addDanger( + i18n.translate('xpack.ml.dataframe.definePivot.nestedAggListConflictErrorMessage', { + defaultMessage: `Couldn't add configuration '{aggName}' because of a nesting conflict with '{aggListName}'.`, + values: { aggName, aggListName }, + }) + ); + return true; + } + return false; + }); + }); + + if (conflict) { + return true; + } + + // check all group-bys against new aggName + conflict = Object.keys(groupByList).some(groupByListName => { + const groupByListNameSplit = groupByListName.split('.'); + let groupByListNameCheck: string; + return groupByListNameSplit.some(groupByListNamePart => { + groupByListNameCheck = + groupByListNameCheck === undefined + ? groupByListNamePart + : `${groupByListNameCheck}.${groupByListNamePart}`; + if (groupByListNameCheck === aggName) { + toastNotifications.addDanger( + i18n.translate('xpack.ml.dataframe.definePivot.nestedGroupByListConflictErrorMessage', { + defaultMessage: `Couldn't add configuration '{aggName}' because of a nesting conflict with '{groupByListName}'.`, + values: { aggName, groupByListName }, + }) + ); + return true; + } + return false; + }); + }); + + return conflict; +} interface Props { overrides?: DefinePivotExposedState; @@ -67,8 +168,6 @@ interface Props { } export const DefinePivotForm: SFC = React.memo(({ overrides = {}, onChange }) => { - const defaults = { ...getDefaultPivotState(), ...overrides }; - const kibanaContext = useContext(KibanaContext); if (!isKibanaContext(kibanaContext)) { @@ -77,6 +176,8 @@ export const DefinePivotForm: SFC = React.memo(({ overrides = {}, onChang const indexPattern = kibanaContext.currentIndexPattern; + const defaults = { ...getDefaultPivotState(kibanaContext), ...overrides }; + // The search filter const [search, setSearch] = useState(defaults.search); @@ -102,27 +203,31 @@ export const DefinePivotForm: SFC = React.memo(({ overrides = {}, onChang const addGroupBy = (d: DropDownLabel[]) => { const label: AggName = d[0].label; - if (groupByList[label] === undefined) { - groupByList[label] = groupByOptionsData[label]; - setGroupByList({ ...groupByList }); - } else { - toastNotifications.addDanger( - i18n.translate('xpack.ml.dataframe.definePivot.groupByExistsErrorMessage', { - defaultMessage: `A group by configuration with the name '{label}' already exists.`, - values: { label }, - }) - ); + const config: PivotGroupByConfig = groupByOptionsData[label]; + const aggName: AggName = config.aggName; + + if (isAggNameConflict(aggName, aggList, groupByList)) { + return; } + + groupByList[aggName] = config; + setGroupByList({ ...groupByList }); }; const updateGroupBy = (previousAggName: AggName, item: PivotGroupByConfig) => { - delete groupByList[previousAggName]; - groupByList[item.aggName] = item; - setGroupByList({ ...groupByList }); + const groupByListWithoutPrevious = { ...groupByList }; + delete groupByListWithoutPrevious[previousAggName]; + + if (isAggNameConflict(item.aggName, aggList, groupByListWithoutPrevious)) { + return; + } + + groupByListWithoutPrevious[item.aggName] = item; + setGroupByList({ ...groupByListWithoutPrevious }); }; - const deleteGroupBy = (label: AggName) => { - delete groupByList[label]; + const deleteGroupBy = (aggName: AggName) => { + delete groupByList[aggName]; setGroupByList({ ...groupByList }); }; @@ -131,27 +236,31 @@ export const DefinePivotForm: SFC = React.memo(({ overrides = {}, onChang const addAggregation = (d: DropDownLabel[]) => { const label: AggName = d[0].label; - if (aggList[label] === undefined) { - aggList[label] = aggOptionsData[label]; - setAggList({ ...aggList }); - } else { - toastNotifications.addDanger( - i18n.translate('xpack.ml.dataframe.definePivot.aggExistsErrorMessage', { - defaultMessage: `An aggregation configuration with the name '{label}' already exists.`, - values: { label }, - }) - ); + const config: PivotAggsConfig = aggOptionsData[label]; + const aggName: AggName = config.aggName; + + if (isAggNameConflict(aggName, aggList, groupByList)) { + return; } + + aggList[aggName] = config; + setAggList({ ...aggList }); }; const updateAggregation = (previousAggName: AggName, item: PivotAggsConfig) => { - delete aggList[previousAggName]; - aggList[item.aggName] = item; - setAggList({ ...aggList }); + const aggListWithoutPrevious = { ...aggList }; + delete aggListWithoutPrevious[previousAggName]; + + if (isAggNameConflict(item.aggName, aggListWithoutPrevious, groupByList)) { + return; + } + + aggListWithoutPrevious[item.aggName] = item; + setAggList({ ...aggListWithoutPrevious }); }; - const deleteAggregation = (label: AggName) => { - delete aggList[label]; + const deleteAggregation = (aggName: AggName) => { + delete aggList[aggName]; setAggList({ ...aggList }); }; @@ -168,7 +277,10 @@ export const DefinePivotForm: SFC = React.memo(({ overrides = {}, onChang pivotAggsArr.map(d => `${d.agg} ${d.field} ${d.aggName}`).join(' '), pivotGroupByArr .map( - d => `${d.agg} ${d.field} ${groupByConfigHasInterval(d) ? d.interval : ''} ${d.aggName}` + d => + `${d.agg} ${d.field} ${isGroupByHistogram(d) ? d.interval : ''} ${ + isGroupByDateHistogram(d) ? d.calendar_interval : '' + } ${d.aggName}` ) .join(' '), search, @@ -176,25 +288,69 @@ export const DefinePivotForm: SFC = React.memo(({ overrides = {}, onChang ] ); - const displaySearch = search === defaultSearch ? emptySearch : search; + // TODO This should use the actual value of `indices.query.bool.max_clause_count` + const maxIndexFields = 1024; + const numIndexFields = indexPattern.fields.length; + const disabledQuery = numIndexFields > maxIndexFields; return ( - - + + {kibanaContext.currentIndexPattern.title} + + {!disabledQuery && ( + + + + )} + + )} + + {kibanaContext.currentSavedSearch.id !== undefined && ( + - + > + {kibanaContext.currentSavedSearch.title} + + )} { describe('Data Frame: ', () => { test('Minimal initialization', () => { const currentIndexPattern = { + id: 'the-index-pattern-id', title: 'the-index-pattern-title', fields: [], }; @@ -34,12 +35,14 @@ describe('Data Frame: ', () => { const groupBy: PivotGroupByConfig = { agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, field: 'the-group-by-field', - aggName: 'the-group-by-label', + aggName: 'the-group-by-agg-name', + dropDownName: 'the-group-by-drop-down-name', }; const agg: PivotAggsConfig = { agg: PIVOT_SUPPORTED_AGGS.AVG, field: 'the-agg-field', - aggName: 'the-agg-label', + aggName: 'the-group-by-agg-name', + dropDownName: 'the-group-by-drop-down-name', }; const props: DefinePivotExposedState = { aggList: { 'the-agg-name': agg }, @@ -53,7 +56,14 @@ describe('Data Frame: ', () => { const wrapper = shallow(
diff --git a/x-pack/plugins/ml/public/data_frame/components/define_pivot/define_pivot_summary.tsx b/x-pack/plugins/ml/public/data_frame/components/define_pivot/define_pivot_summary.tsx index 69427d40f8953..184b9c4330ae7 100644 --- a/x-pack/plugins/ml/public/data_frame/components/define_pivot/define_pivot_summary.tsx +++ b/x-pack/plugins/ml/public/data_frame/components/define_pivot/define_pivot_summary.tsx @@ -4,34 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { SFC, useContext } from 'react'; +import React, { Fragment, SFC, useContext } from 'react'; import { i18n } from '@kbn/i18n'; -import { - EuiComboBoxOptionProps, - EuiFlexGroup, - EuiFlexItem, - EuiForm, - EuiFormRow, - EuiText, -} from '@elastic/eui'; - -import { KBN_FIELD_TYPES } from '../../../../common/constants/field_types'; +import { EuiFlexGroup, EuiFlexItem, EuiForm, EuiFormRow, EuiText } from '@elastic/eui'; import { AggListSummary } from '../../components/aggregation_list'; import { GroupByListSummary } from '../../components/group_by_list'; import { PivotPreview } from './pivot_preview'; -import { - DropDownOption, - getPivotQuery, - isKibanaContext, - KibanaContext, - PivotAggsConfigDict, - pivotAggsFieldSupport, -} from '../../common'; -import { Field } from './common'; +import { getPivotQuery, isKibanaContext, KibanaContext } from '../../common'; import { DefinePivotExposedState } from './define_pivot_form'; const defaultSearch = '*'; @@ -48,29 +31,6 @@ export const DefinePivotSummary: SFC = ({ return null; } - const indexPattern = kibanaContext.currentIndexPattern; - - const ignoreFieldNames = ['_id', '_index', '_type']; - const fields = indexPattern.fields - .filter(field => field.aggregatable === true && !ignoreFieldNames.includes(field.name)) - .map((field): Field => ({ name: field.name, type: field.type as KBN_FIELD_TYPES })); - - // The available aggregations - const aggOptions: EuiComboBoxOptionProps[] = []; - const aggOptionsData: PivotAggsConfigDict = {}; - - fields.forEach(field => { - // Aggregations - const aggOption: DropDownOption = { label: field.name, options: [] }; - const availableAggs = pivotAggsFieldSupport[field.type]; - availableAggs.forEach(agg => { - const aggName = `${agg}(${field.name})`; - aggOption.options.push({ label: aggName }); - aggOptionsData[aggName] = { agg, field: field.name, aggName }; - }); - aggOptions.push(aggOption); - }); - const pivotQuery = getPivotQuery(search); const displaySearch = search === defaultSearch ? emptySearch : search; @@ -79,13 +39,36 @@ export const DefinePivotSummary: SFC = ({ - - {displaySearch} - + {kibanaContext.currentSavedSearch.id === undefined && typeof search === 'string' && ( + + + {kibanaContext.currentIndexPattern.title} + + {displaySearch !== emptySearch && ( + + {displaySearch} + + )} + + )} + + {kibanaContext.currentSavedSearch.id !== undefined && ( + + {kibanaContext.currentSavedSearch.title} + + )} { describe('Data Frame: ', () => { test('Minimal initialization', () => { const currentIndexPattern = { + id: 'the-index-pattern-id', title: 'the-index-pattern-title', fields: [], }; @@ -34,12 +35,14 @@ describe('Data Frame: ', () => { const groupBy: PivotGroupByConfig = { agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, field: 'the-group-by-field', - aggName: 'the-group-by-label', + aggName: 'the-group-by-agg-name', + dropDownName: 'the-group-by-drop-down-name', }; const agg: PivotAggsConfig = { agg: PIVOT_SUPPORTED_AGGS.AVG, field: 'the-agg-field', - aggName: 'the-agg-label', + aggName: 'the-group-by-agg-name', + dropDownName: 'the-group-by-drop-down-name', }; const props = { aggs: { 'the-agg-name': agg }, @@ -52,7 +55,14 @@ describe('Data Frame: ', () => { const wrapper = shallow(
diff --git a/x-pack/plugins/ml/public/data_frame/components/define_pivot/pivot_preview.tsx b/x-pack/plugins/ml/public/data_frame/components/define_pivot/pivot_preview.tsx index 72307b91728f4..bc8d92b552e14 100644 --- a/x-pack/plugins/ml/public/data_frame/components/define_pivot/pivot_preview.tsx +++ b/x-pack/plugins/ml/public/data_frame/components/define_pivot/pivot_preview.tsx @@ -18,6 +18,7 @@ import { EuiInMemoryTableProps, EuiPanel, EuiProgress, + EuiText, EuiTitle, SortDirection, } from '@elastic/eui'; @@ -31,9 +32,11 @@ import { PivotAggsConfigDict, PivotGroupByConfig, PivotGroupByConfigDict, - SimpleQuery, + PivotQuery, } from '../../common'; +import { getFlattenedFields } from '../source_index_preview/common'; + import { getPivotPreviewDevConsoleStatement } from './common'; import { PIVOT_PREVIEW_STATUS, usePivotPreviewData } from './use_pivot_preview_data'; @@ -102,10 +105,33 @@ const PreviewTitle: SFC = ({ previewRequest }) => { ); }; +interface ErrorMessageProps { + message: string; +} + +const ErrorMessage: SFC = ({ message }) => { + const error = JSON.parse(message); + + const statusCodeLabel = i18n.translate('xpack.ml.dataframe.pivotPreview.statusCodeLabel', { + defaultMessage: 'Status code', + }); + + return ( + +
+        {(error.message &&
+          error.statusCode &&
+          `${statusCodeLabel}: ${error.statusCode}\n${error.message}`) ||
+          message}
+      
+
+ ); +}; + interface PivotPreviewProps { aggs: PivotAggsConfigDict; groupBy: PivotGroupByConfigDict; - query: SimpleQuery; + query: PivotQuery; } export const PivotPreview: SFC = React.memo(({ aggs, groupBy, query }) => { @@ -165,13 +191,30 @@ export const PivotPreview: SFC = React.memo(({ aggs, groupBy, color="danger" iconType="cross" > -

{errorMessage}

+ ); } if (dataFramePreviewData.length === 0) { + let noDataMessage = i18n.translate( + 'xpack.ml.dataframe.pivotPreview.dataFramePivotPreviewNoDataCalloutBody', + { + defaultMessage: + 'The preview request did not return any data. Please ensure the optional query returns data and that values exist for the field used by group-by and aggregation fields.', + } + ); + + const aggsArr = dictionaryToArray(aggs); + if (aggsArr.length === 0 || groupByArr.length === 0) { + noDataMessage = i18n.translate( + 'xpack.ml.dataframe.pivotPreview.dataFramePivotPreviewIncompleteConfigCalloutBody', + { + defaultMessage: 'Please choose at least one group-by field and aggregation.', + } + ); + } return ( @@ -184,20 +227,13 @@ export const PivotPreview: SFC = React.memo(({ aggs, groupBy, )} color="primary" > -

- {i18n.translate( - 'xpack.ml.dataframe.pivotPreview.dataFramePivotPreviewNoDataCalloutBody', - { - defaultMessage: 'Please choose at least one group-by field and aggregation.', - } - )} -

+

{noDataMessage}

); } - const columnKeys = Object.keys(dataFramePreviewData[0]); + const columnKeys = getFlattenedFields(dataFramePreviewData[0]); columnKeys.sort(sortColumns(groupByArr)); const columns = columnKeys.map(k => { diff --git a/x-pack/plugins/ml/public/data_frame/components/define_pivot/use_pivot_preview_data.test.tsx b/x-pack/plugins/ml/public/data_frame/components/define_pivot/use_pivot_preview_data.test.tsx index 18a8a6cdbb5ef..a4780cc445b09 100644 --- a/x-pack/plugins/ml/public/data_frame/components/define_pivot/use_pivot_preview_data.test.tsx +++ b/x-pack/plugins/ml/public/data_frame/components/define_pivot/use_pivot_preview_data.test.tsx @@ -45,7 +45,12 @@ let pivotPreviewObj: UsePivotPreviewDataReturnType; describe('usePivotPreviewData', () => { test('indexPattern not defined', () => { testHook(() => { - pivotPreviewObj = usePivotPreviewData({ title: 'lorem', fields: [] }, query, {}, {}); + pivotPreviewObj = usePivotPreviewData( + { id: 'the-id', title: 'the-title', fields: [] }, + query, + {}, + {} + ); }); expect(pivotPreviewObj.errorMessage).toBe(''); @@ -56,7 +61,12 @@ describe('usePivotPreviewData', () => { test('indexPattern set triggers loading', () => { testHook(() => { - pivotPreviewObj = usePivotPreviewData({ title: 'lorem', fields: [] }, query, {}, {}); + pivotPreviewObj = usePivotPreviewData( + { id: 'the-id', title: 'the-title', fields: [] }, + query, + {}, + {} + ); }); expect(pivotPreviewObj.errorMessage).toBe(''); diff --git a/x-pack/plugins/ml/public/data_frame/components/define_pivot/use_pivot_preview_data.ts b/x-pack/plugins/ml/public/data_frame/components/define_pivot/use_pivot_preview_data.ts index 59c802c7d2d51..5eaea5978d675 100644 --- a/x-pack/plugins/ml/public/data_frame/components/define_pivot/use_pivot_preview_data.ts +++ b/x-pack/plugins/ml/public/data_frame/components/define_pivot/use_pivot_preview_data.ts @@ -6,7 +6,7 @@ import { useEffect, useState } from 'react'; -import { StaticIndexPattern } from 'ui/index_patterns'; +import { IndexPattern } from 'ui/index_patterns'; import { dictionaryToArray } from '../../../../common/types/common'; import { ml } from '../../../services/ml_api_service'; @@ -15,10 +15,11 @@ import { Dictionary } from '../../../../common/types/common'; import { DataFramePreviewRequest, getDataFramePreviewRequest, - groupByConfigHasInterval, + isGroupByDateHistogram, + isGroupByHistogram, PivotAggsConfigDict, PivotGroupByConfigDict, - SimpleQuery, + PivotQuery, } from '../../common'; export enum PIVOT_PREVIEW_STATUS { @@ -36,8 +37,8 @@ export interface UsePivotPreviewDataReturnType { } export const usePivotPreviewData = ( - indexPattern: StaticIndexPattern, - query: SimpleQuery, + indexPattern: IndexPattern, + query: PivotQuery, aggs: PivotAggsConfigDict, groupBy: PivotGroupByConfigDict ): UsePivotPreviewDataReturnType => { @@ -79,10 +80,13 @@ export const usePivotPreviewData = ( aggsArr.map(a => `${a.agg} ${a.field} ${a.aggName}`).join(' '), groupByArr .map( - g => `${g.agg} ${g.field} ${g.aggName} ${groupByConfigHasInterval(g) ? g.interval : ''}` + g => + `${g.agg} ${g.field} ${g.aggName} ${ + isGroupByDateHistogram(g) ? g.calendar_interval : '' + } ${isGroupByHistogram(g) ? g.interval : ''}` ) .join(' '), - query.query_string.query, + JSON.stringify(query), ] ); return { errorMessage, status, dataFramePreviewData, previewRequest }; diff --git a/x-pack/plugins/ml/public/data_frame/components/group_by_list/__snapshots__/group_by_label_form.test.tsx.snap b/x-pack/plugins/ml/public/data_frame/components/group_by_list/__snapshots__/group_by_label_form.test.tsx.snap index 04bd99e82922f..04a1cf96f8517 100644 --- a/x-pack/plugins/ml/public/data_frame/components/group_by_list/__snapshots__/group_by_label_form.test.tsx.snap +++ b/x-pack/plugins/ml/public/data_frame/components/group_by_list/__snapshots__/group_by_label_form.test.tsx.snap @@ -10,9 +10,9 @@ exports[`Data Frame: Date histogram aggregation 1`] = ` className="mlGroupByLabel--text" > - the-group-by-label + the-group-by-agg-name Date histogram aggregation 1`] = ` grow={false} > - 10m + 1m Date histogram aggregation 1`] = ` defaultData={ Object { "agg": "date_histogram", - "aggName": "the-group-by-label", + "aggName": "the-group-by-agg-name", + "calendar_interval": "1m", + "dropDownName": "the-group-by-drop-down-name", "field": "the-group-by-field", - "interval": "10m", } } onChange={[Function]} @@ -92,9 +93,9 @@ exports[`Data Frame: Histogram aggregation 1`] = ` className="mlGroupByLabel--text" > - the-group-by-label + the-group-by-agg-name Histogram aggregation 1`] = ` grow={false} > 100 @@ -136,7 +137,8 @@ exports[`Data Frame: Histogram aggregation 1`] = ` defaultData={ Object { "agg": "histogram", - "aggName": "the-group-by-label", + "aggName": "the-group-by-agg-name", + "dropDownName": "the-group-by-drop-down-name", "field": "the-group-by-field", "interval": "100", } @@ -174,9 +176,9 @@ exports[`Data Frame: Terms aggregation 1`] = ` className="mlGroupByLabel--text" > - the-group-by-label + the-group-by-agg-name Terms aggregation 1`] = ` defaultData={ Object { "agg": "terms", - "aggName": "the-group-by-label", + "aggName": "the-group-by-agg-name", + "dropDownName": "the-group-by-drop-down-name", "field": "the-group-by-field", } } diff --git a/x-pack/plugins/ml/public/data_frame/components/group_by_list/__snapshots__/group_by_label_summary.test.tsx.snap b/x-pack/plugins/ml/public/data_frame/components/group_by_list/__snapshots__/group_by_label_summary.test.tsx.snap index a387bfb0e7de1..5a2039de031d1 100644 --- a/x-pack/plugins/ml/public/data_frame/components/group_by_list/__snapshots__/group_by_label_summary.test.tsx.snap +++ b/x-pack/plugins/ml/public/data_frame/components/group_by_list/__snapshots__/group_by_label_summary.test.tsx.snap @@ -10,7 +10,7 @@ exports[`Data Frame: Date histogram aggregation 1`] = ` className="mlGroupByLabel--text" > the-options-data-id @@ -20,10 +20,10 @@ exports[`Data Frame: Date histogram aggregation 1`] = ` grow={false} > - 10m + 1m @@ -39,7 +39,7 @@ exports[`Data Frame: Histogram aggregation 1`] = ` className="mlGroupByLabel--text" > the-options-data-id @@ -49,7 +49,7 @@ exports[`Data Frame: Histogram aggregation 1`] = ` grow={false} > 100 @@ -68,7 +68,7 @@ exports[`Data Frame: Terms aggregation 1`] = ` className="mlGroupByLabel--text" > the-options-data-id diff --git a/x-pack/plugins/ml/public/data_frame/components/group_by_list/__snapshots__/list_form.test.tsx.snap b/x-pack/plugins/ml/public/data_frame/components/group_by_list/__snapshots__/list_form.test.tsx.snap index 8d0a81b2faf5a..e242eda727a13 100644 --- a/x-pack/plugins/ml/public/data_frame/components/group_by_list/__snapshots__/list_form.test.tsx.snap +++ b/x-pack/plugins/ml/public/data_frame/components/group_by_list/__snapshots__/list_form.test.tsx.snap @@ -12,7 +12,8 @@ exports[`Data Frame: Minimal initialization 1`] = ` item={ Object { "agg": "terms", - "aggName": "the-group-by-label", + "aggName": "the-group-by-agg-name", + "dropDownName": "the-group-by-drop-down-name", "field": "the-group-by-field", } } diff --git a/x-pack/plugins/ml/public/data_frame/components/group_by_list/__snapshots__/list_summary.test.tsx.snap b/x-pack/plugins/ml/public/data_frame/components/group_by_list/__snapshots__/list_summary.test.tsx.snap index e6fb681e45e6d..508d7af56f5c1 100644 --- a/x-pack/plugins/ml/public/data_frame/components/group_by_list/__snapshots__/list_summary.test.tsx.snap +++ b/x-pack/plugins/ml/public/data_frame/components/group_by_list/__snapshots__/list_summary.test.tsx.snap @@ -11,7 +11,8 @@ exports[`Data Frame: Minimal initialization 1`] = ` item={ Object { "agg": "terms", - "aggName": "the-group-by-label", + "aggName": "the-group-by-agg-name", + "dropDownName": "the-group-by-drop-down-name", "field": "the-group-by-field", } } diff --git a/x-pack/plugins/ml/public/data_frame/components/group_by_list/__snapshots__/popover_form.test.tsx.snap b/x-pack/plugins/ml/public/data_frame/components/group_by_list/__snapshots__/popover_form.test.tsx.snap index b2dc3e71ea812..5e5aed5829382 100644 --- a/x-pack/plugins/ml/public/data_frame/components/group_by_list/__snapshots__/popover_form.test.tsx.snap +++ b/x-pack/plugins/ml/public/data_frame/components/group_by_list/__snapshots__/popover_form.test.tsx.snap @@ -35,13 +35,45 @@ exports[`Data Frame: Group By Minimal initialization 1`] = ` label="Interval" labelType="label" > - ', () => { const item: PivotGroupByConfig = { agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.DATE_HISTOGRAM, field: 'the-group-by-field', - aggName: 'the-group-by-label', - interval: '10m', + aggName: 'the-group-by-agg-name', + dropDownName: 'the-group-by-drop-down-name', + calendar_interval: '1m', }; const props = { item, @@ -36,7 +37,8 @@ describe('Data Frame: ', () => { const item: PivotGroupByConfig = { agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.HISTOGRAM, field: 'the-group-by-field', - aggName: 'the-group-by-label', + aggName: 'the-group-by-agg-name', + dropDownName: 'the-group-by-drop-down-name', interval: '100', }; const props = { @@ -56,7 +58,8 @@ describe('Data Frame: ', () => { const item: PivotGroupByConfig = { agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, field: 'the-group-by-field', - aggName: 'the-group-by-label', + aggName: 'the-group-by-agg-name', + dropDownName: 'the-group-by-drop-down-name', }; const props = { item, diff --git a/x-pack/plugins/ml/public/data_frame/components/group_by_list/group_by_label_form.tsx b/x-pack/plugins/ml/public/data_frame/components/group_by_list/group_by_label_form.tsx index 1fdc2f20c5581..9ba7ed16137b1 100644 --- a/x-pack/plugins/ml/public/data_frame/components/group_by_list/group_by_label_form.tsx +++ b/x-pack/plugins/ml/public/data_frame/components/group_by_list/group_by_label_form.tsx @@ -12,7 +12,8 @@ import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiPopover, EuiTextColor } fr import { AggName, - groupByConfigHasInterval, + isGroupByDateHistogram, + isGroupByHistogram, PivotGroupByConfig, PivotGroupByConfigDict, } from '../../common'; @@ -41,15 +42,23 @@ export const GroupByLabelForm: React.SFC = ({ setPopoverVisibility(false); } + let interval: string | undefined; + + if (isGroupByDateHistogram(item)) { + interval = item.calendar_interval; + } else if (isGroupByHistogram(item)) { + interval = item.interval; + } + return ( - {item.aggName} + {item.aggName} - {groupByConfigHasInterval(item) && ( + {interval !== undefined && ( - - {item.interval} + + {interval} )} diff --git a/x-pack/plugins/ml/public/data_frame/components/group_by_list/group_by_label_summary.test.tsx b/x-pack/plugins/ml/public/data_frame/components/group_by_list/group_by_label_summary.test.tsx index b33424bd5d23f..7fe0a106dabc1 100644 --- a/x-pack/plugins/ml/public/data_frame/components/group_by_list/group_by_label_summary.test.tsx +++ b/x-pack/plugins/ml/public/data_frame/components/group_by_list/group_by_label_summary.test.tsx @@ -16,8 +16,9 @@ describe('Data Frame: ', () => { const item: PivotGroupByConfig = { agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.DATE_HISTOGRAM, field: 'the-group-by-field', - aggName: 'the-group-by-label', - interval: '10m', + aggName: 'the-group-by-agg-name', + dropDownName: 'the-group-by-drop-down-name', + calendar_interval: '1m', }; const props = { item, @@ -33,7 +34,8 @@ describe('Data Frame: ', () => { const item: PivotGroupByConfig = { agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.HISTOGRAM, field: 'the-group-by-field', - aggName: 'the-group-by-label', + aggName: 'the-group-by-agg-name', + dropDownName: 'the-group-by-drop-down-name', interval: '100', }; const props = { @@ -50,7 +52,8 @@ describe('Data Frame: ', () => { const item: PivotGroupByConfig = { agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, field: 'the-group-by-field', - aggName: 'the-group-by-label', + aggName: 'the-group-by-agg-name', + dropDownName: 'the-group-by-drop-down-name', }; const props = { item, diff --git a/x-pack/plugins/ml/public/data_frame/components/group_by_list/group_by_label_summary.tsx b/x-pack/plugins/ml/public/data_frame/components/group_by_list/group_by_label_summary.tsx index 8b7a8d94ed58b..30315f05c9d50 100644 --- a/x-pack/plugins/ml/public/data_frame/components/group_by_list/group_by_label_summary.tsx +++ b/x-pack/plugins/ml/public/data_frame/components/group_by_list/group_by_label_summary.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiTextColor } from '@elastic/eui'; -import { groupByConfigHasInterval, PivotGroupByConfig } from '../../common'; +import { isGroupByDateHistogram, isGroupByHistogram, PivotGroupByConfig } from '../../common'; interface Props { item: PivotGroupByConfig; @@ -16,15 +16,23 @@ interface Props { } export const GroupByLabelSummary: React.SFC = ({ item, optionsDataId }) => { + let interval: string | undefined; + + if (isGroupByDateHistogram(item)) { + interval = item.calendar_interval; + } else if (isGroupByHistogram(item)) { + interval = item.interval; + } + return ( - {optionsDataId} + {optionsDataId} - {groupByConfigHasInterval(item) && ( + {interval !== undefined && ( - - {item.interval} + + {interval} )} diff --git a/x-pack/plugins/ml/public/data_frame/components/group_by_list/list_form.test.tsx b/x-pack/plugins/ml/public/data_frame/components/group_by_list/list_form.test.tsx index 1649dec983ea1..d926cd379abe9 100644 --- a/x-pack/plugins/ml/public/data_frame/components/group_by_list/list_form.test.tsx +++ b/x-pack/plugins/ml/public/data_frame/components/group_by_list/list_form.test.tsx @@ -16,7 +16,8 @@ describe('Data Frame: ', () => { const item: PivotGroupByConfig = { agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, field: 'the-group-by-field', - aggName: 'the-group-by-label', + aggName: 'the-group-by-agg-name', + dropDownName: 'the-group-by-drop-down-name', }; const props = { list: { 'the-options-data-id': item }, diff --git a/x-pack/plugins/ml/public/data_frame/components/group_by_list/list_summary.test.tsx b/x-pack/plugins/ml/public/data_frame/components/group_by_list/list_summary.test.tsx index b9072d9f34e5e..32c6e4fd9430a 100644 --- a/x-pack/plugins/ml/public/data_frame/components/group_by_list/list_summary.test.tsx +++ b/x-pack/plugins/ml/public/data_frame/components/group_by_list/list_summary.test.tsx @@ -16,7 +16,8 @@ describe('Data Frame: ', () => { const item: PivotGroupByConfig = { agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, field: 'the-group-by-field', - aggName: 'the-group-by-label', + aggName: 'the-group-by-agg-name', + dropDownName: 'the-group-by-drop-down-name', }; const props = { list: { 'the-options-data-id': item }, diff --git a/x-pack/plugins/ml/public/data_frame/components/group_by_list/popover_form.test.tsx b/x-pack/plugins/ml/public/data_frame/components/group_by_list/popover_form.test.tsx index 187640dd2eb84..a7faf310205e4 100644 --- a/x-pack/plugins/ml/public/data_frame/components/group_by_list/popover_form.test.tsx +++ b/x-pack/plugins/ml/public/data_frame/components/group_by_list/popover_form.test.tsx @@ -78,8 +78,9 @@ describe('Data Frame: Group By ', () => { const defaultData: PivotGroupByConfig = { agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.DATE_HISTOGRAM, aggName: 'the-agg-name', + dropDownName: 'the-drop-down-name', field: 'the-field', - interval: '10m', + calendar_interval: '1m', }; const otherAggNames: AggName[] = []; const onChange = (item: PivotGroupByConfig) => {}; diff --git a/x-pack/plugins/ml/public/data_frame/components/group_by_list/popover_form.tsx b/x-pack/plugins/ml/public/data_frame/components/group_by_list/popover_form.tsx index d6fec63901b6c..e9f45eeb166ad 100644 --- a/x-pack/plugins/ml/public/data_frame/components/group_by_list/popover_form.tsx +++ b/x-pack/plugins/ml/public/data_frame/components/group_by_list/popover_form.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState } from 'react'; +import React, { Fragment, useState } from 'react'; import { i18n } from '@kbn/i18n'; @@ -15,7 +15,8 @@ import { dictionaryToArray } from '../../../../common/types/common'; import { AggName, dateHistogramIntervalFormatRegex, - groupByConfigHasInterval, + isGroupByDateHistogram, + isGroupByHistogram, histogramIntervalFormatRegex, isAggName, PivotGroupByConfig, @@ -64,6 +65,16 @@ interface SelectOption { type optionalInterval = string | undefined; +function getDefaultInterval(defaultData: PivotGroupByConfig): string | undefined { + if (isGroupByDateHistogram(defaultData)) { + return defaultData.calendar_interval; + } else if (isGroupByHistogram(defaultData)) { + return defaultData.interval; + } + + return undefined; +} + interface Props { defaultData: PivotGroupByConfig; otherAggNames: AggName[]; @@ -80,15 +91,17 @@ export const PopoverForm: React.SFC = ({ const [agg, setAgg] = useState(defaultData.agg); const [aggName, setAggName] = useState(defaultData.aggName); const [field, setField] = useState(defaultData.field); - const [interval, setInterval] = useState( - groupByConfigHasInterval(defaultData) ? defaultData.interval : undefined - ); + const [interval, setInterval] = useState(getDefaultInterval(defaultData)); function getUpdatedItem(): PivotGroupByConfig { const updatedItem = { ...defaultData, agg, aggName, field }; - if (groupByConfigHasInterval(updatedItem) && interval !== undefined) { + + if (isGroupByHistogram(updatedItem) && interval !== undefined) { updatedItem.interval = interval; + } else if (isGroupByDateHistogram(updatedItem) && interval !== undefined) { + updatedItem.calendar_interval = interval; } + // Casting to PivotGroupByConfig because TS would otherwise complain about the // PIVOT_SUPPORTED_GROUP_BY_AGGS type for `agg`. return updatedItem as PivotGroupByConfig; @@ -130,10 +143,11 @@ export const PopoverForm: React.SFC = ({ } const validInterval = - groupByConfigHasInterval(defaultData) && isIntervalValid(interval, defaultData.agg); + (isGroupByDateHistogram(defaultData) || isGroupByHistogram(defaultData)) && + isIntervalValid(interval, defaultData.agg); let formValid = validAggName; - if (formValid && groupByConfigHasInterval(defaultData)) { + if (formValid && (isGroupByDateHistogram(defaultData) || isGroupByHistogram(defaultData))) { formValid = isIntervalValid(interval, defaultData.agg); } @@ -178,7 +192,7 @@ export const PopoverForm: React.SFC = ({ /> )} - {groupByConfigHasInterval(defaultData) && ( + {(isGroupByDateHistogram(defaultData) || isGroupByHistogram(defaultData)) && ( = ({ defaultMessage: 'Interval', })} > - setInterval(e.target.value)} - /> + + {isGroupByHistogram(defaultData) && ( + setInterval(e.target.value)} + /> + )} + {isGroupByDateHistogram(defaultData) && ( + setInterval(e.target.value)} + /> + )} + )} diff --git a/x-pack/plugins/ml/public/data_frame/components/job_create/__snapshots__/job_create_form.test.tsx.snap b/x-pack/plugins/ml/public/data_frame/components/job_create/__snapshots__/job_create_form.test.tsx.snap new file mode 100644 index 0000000000000..3c01d4df4a0a5 --- /dev/null +++ b/x-pack/plugins/ml/public/data_frame/components/job_create/__snapshots__/job_create_form.test.tsx.snap @@ -0,0 +1,36 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Data Frame: Minimal initialization 1`] = ` +
+ + + +
+`; diff --git a/x-pack/plugins/ml/public/data_frame/components/job_create/job_create_form.test.tsx b/x-pack/plugins/ml/public/data_frame/components/job_create/job_create_form.test.tsx new file mode 100644 index 0000000000000..e181cfa7e04b7 --- /dev/null +++ b/x-pack/plugins/ml/public/data_frame/components/job_create/job_create_form.test.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { shallow } from 'enzyme'; +import React from 'react'; + +import { KibanaContext } from '../../common'; + +import { JobCreateForm } from './job_create_form'; + +// workaround to make React.memo() work with enzyme +jest.mock('react', () => { + const r = jest.requireActual('react'); + return { ...r, memo: (x: any) => x }; +}); + +describe('Data Frame: ', () => { + test('Minimal initialization', () => { + const props = { + createIndexPattern: false, + jobId: 'the-job-id', + jobConfig: {}, + overrides: { created: false, started: false, indexPatternId: undefined }, + onChange() {}, + }; + + const currentIndexPattern = { + id: 'the-index-pattern-id', + title: 'the-index-pattern-title', + fields: [], + }; + + // Using a wrapping
element because shallow() would fail + // with the Provider being the outer most component. + const wrapper = shallow( +
+ + + +
+ ); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/ml/public/data_frame/components/job_create/job_create_form.tsx b/x-pack/plugins/ml/public/data_frame/components/job_create/job_create_form.tsx index b6c5b52e22ac1..2460ae03170b0 100644 --- a/x-pack/plugins/ml/public/data_frame/components/job_create/job_create_form.tsx +++ b/x-pack/plugins/ml/public/data_frame/components/job_create/job_create_form.tsx @@ -13,31 +13,43 @@ import { // Module '"@elastic/eui"' has no exported member 'EuiCard'. // @ts-ignore EuiCard, + EuiCopy, + // Module '"@elastic/eui"' has no exported member 'EuiDescribedFormGroup'. + // @ts-ignore + EuiDescribedFormGroup, + EuiFlexGrid, EuiFlexGroup, EuiFlexItem, + EuiForm, + EuiHorizontalRule, EuiIcon, + EuiPanel, + EuiProgress, EuiSpacer, + EuiText, } from '@elastic/eui'; import { ml } from '../../../services/ml_api_service'; +import { PROGRESS_JOBS_REFRESH_INTERVAL_MS } from '../../../../common/constants/jobs_list'; + +import { moveToDataFrameJobsList, moveToDiscover } from '../../common'; import { KibanaContext, isKibanaContext } from '../../common'; export interface JobDetailsExposedState { created: boolean; started: boolean; + indexPatternId: string | undefined; } export function getDefaultJobCreateState(): JobDetailsExposedState { return { created: false, started: false, + indexPatternId: undefined, }; } -function gotToDataFrameJobManagement() { - window.location.href = '#/data_frames'; -} interface Props { createIndexPattern: boolean; jobId: string; @@ -52,6 +64,10 @@ export const JobCreateForm: SFC = React.memo( const [created, setCreated] = useState(defaults.created); const [started, setStarted] = useState(defaults.started); + const [indexPatternId, setIndexPatternId] = useState(defaults.indexPatternId); + const [progressPercentComplete, setProgressPercentComplete] = useState( + undefined + ); const kibanaContext = useContext(KibanaContext); @@ -61,9 +77,9 @@ export const JobCreateForm: SFC = React.memo( useEffect( () => { - onChange({ created, started }); + onChange({ created, started, indexPatternId }); }, - [created, started] + [created, started, indexPatternId] ); async function createDataFrame() { @@ -149,6 +165,8 @@ export const JobCreateForm: SFC = React.memo( values: { indexPatternName }, }) ); + + setIndexPatternId(id); return true; } catch (e) { toastNotifications.addDanger( @@ -162,38 +180,158 @@ export const JobCreateForm: SFC = React.memo( } }; + if (started === true && progressPercentComplete === undefined) { + // wrapping in function so we can keep the interval id in local scope + function startProgressBar() { + const interval = setInterval(async () => { + try { + const stats = await ml.dataFrame.getDataFrameTransformsStats(jobId); + const percent = Math.round(stats.transforms[0].state.progress.percent_complete); + setProgressPercentComplete(percent); + if (percent >= 100) { + clearInterval(interval); + } + } catch (e) { + toastNotifications.addDanger( + i18n.translate('xpack.ml.dataframe.jobCreateForm.progressErrorMessage', { + defaultMessage: 'An error occurred getting the progress percentage: {error}', + values: { error: JSON.stringify(e) }, + }) + ); + clearInterval(interval); + } + }, PROGRESS_JOBS_REFRESH_INTERVAL_MS); + setProgressPercentComplete(0); + } + + startProgressBar(); + } + + function getJobConfigDevConsoleStatement() { + return `PUT _data_frame/transforms/${jobId}\n${JSON.stringify(jobConfig, null, 2)}\n\n`; + } + + // TODO move this to SASS + const FLEX_GROUP_STYLE = { height: '90px', maxWidth: '800px' }; + const FLEX_ITEM_STYLE = { width: '200px' }; + const PANEL_ITEM_STYLE = { width: '300px' }; + return ( - - - {i18n.translate('xpack.ml.dataframe.jobCreateForm.createDataFrameButton', { - defaultMessage: 'Create data frame', - })} - -   + {!created && ( - - {i18n.translate('xpack.ml.dataframe.jobCreateForm.createAndStartDataFrameButton', { - defaultMessage: 'Create and start data frame', - })} - + + + + {i18n.translate('xpack.ml.dataframe.jobCreateForm.createAndStartDataFrameButton', { + defaultMessage: 'Create and start', + })} + + + + + {i18n.translate( + 'xpack.ml.dataframe.jobCreateForm.createAndStartDataFrameDescription', + { + defaultMessage: + 'Creates and starts the data frame job. A data frame job will increase search and indexing load in your cluster. Please stop the job if excessive load is experienced. After the job is started, you will be offered options to continue exploring the data frame job.', + } + )} + + + )} {created && ( - - {i18n.translate('xpack.ml.dataframe.jobCreateForm.startDataFrameButton', { - defaultMessage: 'Start data frame', - })} - + + + + {i18n.translate('xpack.ml.dataframe.jobCreateForm.startDataFrameButton', { + defaultMessage: 'Start', + })} + + + + + {i18n.translate('xpack.ml.dataframe.jobCreateForm.startDataFrameDescription', { + defaultMessage: + 'Starts the data frame job. A data frame job will increase search and indexing load in your cluster. Please stop the job if excessive load is experienced. After the job is started, you will be offered options to continue exploring the data frame job.', + })} + + + )} - {created && started && ( + + + + {i18n.translate('xpack.ml.dataframe.jobCreateForm.createDataFrameButton', { + defaultMessage: 'Create', + })} + + + + + {i18n.translate('xpack.ml.dataframe.jobCreateForm.createDataFrameDescription', { + defaultMessage: + 'Create the data frame job without starting it. You will be able to start the job later by returning to the data frame jobs list.', + })} + + + + + + + {(copy: () => void) => ( + + {i18n.translate( + 'xpack.ml.dataframe.jobCreateForm.copyJobConfigToClipBoardButton', + { + defaultMessage: 'Copy to clipboard', + } + )} + + )} + + + + + {i18n.translate( + 'xpack.ml.dataframe.jobCreateForm.copyJobConfigToClipBoardDescription', + { + defaultMessage: + 'Copies to the clipboard the Kibana Dev Console command for creating the job.', + } + )} + + + + {progressPercentComplete !== undefined && ( - - + + + {i18n.translate('xpack.ml.dataframe.jobCreateForm.progressTitle', { + defaultMessage: 'Progress', + })} + + + + + + + {progressPercentComplete}% + + + + )} + {created && ( + + + + } - title={i18n.translate('xpack.ml.dataframe.jobCreateForm.jobManagementCardTitle', { - defaultMessage: 'Job management', + title={i18n.translate('xpack.ml.dataframe.jobCreateForm.jobsListCardTitle', { + defaultMessage: 'Data frame jobs', })} description={i18n.translate( 'xpack.ml.dataframe.jobCreateForm.jobManagementCardDescription', @@ -201,13 +339,47 @@ export const JobCreateForm: SFC = React.memo( defaultMessage: 'Return to the data frame job management page.', } )} - onClick={gotToDataFrameJobManagement} + onClick={moveToDataFrameJobsList} /> - + {started === true && createIndexPattern === true && indexPatternId === undefined && ( + + + + +

+ {i18n.translate( + 'xpack.ml.dataframe.jobCreateForm.creatingIndexPatternMessage', + { + defaultMessage: 'Creating Kibana index pattern ...', + } + )} +

+
+
+
+ )} + {started === true && indexPatternId !== undefined && ( + + } + title={i18n.translate('xpack.ml.dataframe.jobCreateForm.discoverCardTitle', { + defaultMessage: 'Discover', + })} + description={i18n.translate( + 'xpack.ml.dataframe.jobCreateForm.discoverCardDescription', + { + defaultMessage: 'Use Discover to explore the data frame pivot.', + } + )} + onClick={() => moveToDiscover(indexPatternId, kibanaContext.kbnBaseUrl)} + /> + + )} +
)} -
+ ); } ); diff --git a/x-pack/plugins/ml/public/data_frame/components/source_index_preview/common.ts b/x-pack/plugins/ml/public/data_frame/components/source_index_preview/common.ts index 2dd5e1c31e559..419868f46d377 100644 --- a/x-pack/plugins/ml/public/data_frame/components/source_index_preview/common.ts +++ b/x-pack/plugins/ml/public/data_frame/components/source_index_preview/common.ts @@ -6,7 +6,7 @@ import { Dictionary } from '../../../../common/types/common'; -import { SimpleQuery } from '../../common'; +import { PivotQuery } from '../../common'; export type EsFieldName = string; @@ -20,7 +20,7 @@ export interface EsDoc extends Dictionary { export const MAX_COLUMNS = 5; -function getFlattenedFields(obj: EsDocSource): EsFieldName[] { +export function getFlattenedFields(obj: EsDocSource): EsFieldName[] { const flatDocFields: EsFieldName[] = []; const newDocFields = Object.keys(obj); newDocFields.forEach(f => { @@ -81,10 +81,7 @@ export const toggleSelectedField = ( return selectedFields; }; -export const getSourceIndexDevConsoleStatement = ( - query: SimpleQuery, - indexPatternTitle: string -) => { +export const getSourceIndexDevConsoleStatement = (query: PivotQuery, indexPatternTitle: string) => { return `GET ${indexPatternTitle}/_search\n${JSON.stringify( { query, diff --git a/x-pack/plugins/ml/public/data_frame/components/source_index_preview/source_index_preview.tsx b/x-pack/plugins/ml/public/data_frame/components/source_index_preview/source_index_preview.tsx index b71847071f144..ead7d1dbd69e4 100644 --- a/x-pack/plugins/ml/public/data_frame/components/source_index_preview/source_index_preview.tsx +++ b/x-pack/plugins/ml/public/data_frame/components/source_index_preview/source_index_preview.tsx @@ -5,6 +5,7 @@ */ import React, { FunctionComponent, useContext, useState } from 'react'; +import moment from 'moment-timezone'; import { i18n } from '@kbn/i18n'; @@ -36,15 +37,16 @@ interface ExpandableTableProps extends EuiInMemoryTableProps { const ExpandableTable = (EuiInMemoryTable as any) as FunctionComponent; +import { KBN_FIELD_TYPES } from '../../../../common/constants/field_types'; import { Dictionary } from '../../../../common/types/common'; +import { formatHumanReadableDateTimeSeconds } from '../../../util/date_utils'; -import { isKibanaContext, KibanaContext, SimpleQuery } from '../../common'; +import { isKibanaContext, KibanaContext, PivotQuery } from '../../common'; import { EsDoc, EsFieldName, getSourceIndexDevConsoleStatement, - getSelectableFields, MAX_COLUMNS, toggleSelectedField, } from './common'; @@ -87,7 +89,7 @@ const SourceIndexPreviewTitle: React.SFC = ({ indexPatt ); interface Props { - query: SimpleQuery; + query: PivotQuery; cellClick?(search: string): void; } @@ -199,24 +201,32 @@ export const SourceIndexPreview: React.SFC = React.memo(({ cellClick, que let docFields: EsFieldName[] = []; let docFieldsCount = 0; if (tableItems.length > 0) { - docFields = getSelectableFields(tableItems); + docFields = Object.keys(tableItems[0]._source); docFields.sort(); docFieldsCount = docFields.length; } const columns = selectedFields.map(k => { const column = { - field: `_source.${k}`, + field: `_source["${k}"]`, name: k, - render: undefined, sortable: true, truncateText: true, } as Dictionary; + const field = indexPattern.fields.find(f => f.name === k); + const render = (d: string) => { + return field !== undefined && field.type === KBN_FIELD_TYPES.DATE + ? formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000) + : d; + }; + + column.render = render; + if (CELL_CLICK_ENABLED && cellClick) { column.render = (d: string) => ( cellClick(`${k}:(${d})`)}> - {d} + {render(d)} ); } @@ -235,28 +245,26 @@ export const SourceIndexPreview: React.SFC = React.memo(({ cellClick, que }; } - if (docFieldsCount > MAX_COLUMNS || docFieldsCount > selectedFields.length) { - columns.unshift({ - align: RIGHT_ALIGNMENT, - width: '40px', - isExpander: true, - render: (item: EsDoc) => ( - toggleDetails(item)} - aria-label={ - itemIdToExpandedRowMap[item._id] - ? i18n.translate('xpack.ml.dataframe.sourceIndexPreview.rowCollapse', { - defaultMessage: 'Collapse', - }) - : i18n.translate('xpack.ml.dataframe.sourceIndexPreview.rowExpand', { - defaultMessage: 'Expand', - }) - } - iconType={itemIdToExpandedRowMap[item._id] ? 'arrowUp' : 'arrowDown'} - /> - ), - }); - } + columns.unshift({ + align: RIGHT_ALIGNMENT, + width: '40px', + isExpander: true, + render: (item: EsDoc) => ( + toggleDetails(item)} + aria-label={ + itemIdToExpandedRowMap[item._id] + ? i18n.translate('xpack.ml.dataframe.sourceIndexPreview.rowCollapse', { + defaultMessage: 'Collapse', + }) + : i18n.translate('xpack.ml.dataframe.sourceIndexPreview.rowExpand', { + defaultMessage: 'Expand', + }) + } + iconType={itemIdToExpandedRowMap[item._id] ? 'arrowUp' : 'arrowDown'} + /> + ), + }); const euiCopyText = i18n.translate('xpack.ml.dataframe.sourceIndexPreview.copyClipboardTooltip', { defaultMessage: 'Copy Dev Console statement of the source index preview to the clipboard.', diff --git a/x-pack/plugins/ml/public/data_frame/components/source_index_preview/use_source_index_data.test.tsx b/x-pack/plugins/ml/public/data_frame/components/source_index_preview/use_source_index_data.test.tsx index 69a73db844360..d157aada254b3 100644 --- a/x-pack/plugins/ml/public/data_frame/components/source_index_preview/use_source_index_data.test.tsx +++ b/x-pack/plugins/ml/public/data_frame/components/source_index_preview/use_source_index_data.test.tsx @@ -49,7 +49,12 @@ describe('useSourceIndexData', () => { test('indexPattern set triggers loading', () => { testHook(() => { act(() => { - sourceIndexObj = useSourceIndexData({ title: 'lorem', fields: [] }, query, [], () => {}); + sourceIndexObj = useSourceIndexData( + { id: 'the-id', title: 'the-title', fields: [] }, + query, + [], + () => {} + ); }); }); diff --git a/x-pack/plugins/ml/public/data_frame/components/source_index_preview/use_source_index_data.ts b/x-pack/plugins/ml/public/data_frame/components/source_index_preview/use_source_index_data.ts index f02a700151fa8..0772c0c388f2b 100644 --- a/x-pack/plugins/ml/public/data_frame/components/source_index_preview/use_source_index_data.ts +++ b/x-pack/plugins/ml/public/data_frame/components/source_index_preview/use_source_index_data.ts @@ -4,16 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ +import { get } from 'lodash'; + import React, { useEffect, useState } from 'react'; import { SearchResponse } from 'elasticsearch'; -import { StaticIndexPattern } from 'ui/index_patterns'; +import { IndexPattern } from 'ui/index_patterns'; import { ml } from '../../../services/ml_api_service'; -import { SimpleQuery } from '../../common'; -import { EsDoc, EsFieldName, getDefaultSelectableFields } from './common'; +import { isDefaultQuery, PivotQuery } from '../../common'; +import { EsDoc, EsFieldName, getDefaultSelectableFields, getFlattenedFields } from './common'; const SEARCH_SIZE = 1000; @@ -31,8 +33,8 @@ export interface UseSourceIndexDataReturnType { } export const useSourceIndexData = ( - indexPattern: StaticIndexPattern, - query: SimpleQuery, + indexPattern: IndexPattern, + query: PivotQuery, selectedFields: EsFieldName[], setSelectedFields: React.Dispatch> ): UseSourceIndexDataReturnType => { @@ -48,7 +50,8 @@ export const useSourceIndexData = ( const resp: SearchResponse = await ml.esSearch({ index: indexPattern.title, size: SEARCH_SIZE, - body: { query }, + // Instead of using the default query (`*`), fall back to a more efficient `match_all` query. + body: { query: isDefaultQuery(query) ? { match_all: {} } : query }, }); const docs = resp.hits.hits; @@ -58,7 +61,27 @@ export const useSourceIndexData = ( setSelectedFields(newSelectedFields); } - setTableItems(docs as EsDoc[]); + // Create a version of the doc's source with flattened field names. + // This avoids confusion later on if a field name has dots in its name + // or is a nested fields when displaying it via EuiInMemoryTable. + const flattenedFields = getFlattenedFields(docs[0]._source); + const transformedTableItems = docs.map(doc => { + const item = {} as { + [key: string]: any; + }; + flattenedFields.forEach(ff => { + item[ff] = get(doc._source, ff); + if (item[ff] === undefined) { + item[ff] = doc._source[`"${ff}"`]; + } + }); + return { + ...doc, + _source: item, + }; + }); + + setTableItems(transformedTableItems as EsDoc[]); setStatus(SOURCE_INDEX_STATUS.LOADED); } catch (e) { setErrorMessage(JSON.stringify(e)); @@ -71,7 +94,7 @@ export const useSourceIndexData = ( () => { getSourceIndexData(); }, - [indexPattern.title, query.query_string.query] + [indexPattern.title, JSON.stringify(query)] ); return { errorMessage, status, tableItems }; }; diff --git a/x-pack/plugins/ml/public/data_frame/index.ts b/x-pack/plugins/ml/public/data_frame/index.ts index fb8c8fadd3ecc..b493d1459655e 100644 --- a/x-pack/plugins/ml/public/data_frame/index.ts +++ b/x-pack/plugins/ml/public/data_frame/index.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import './pages/access_denied/directive'; +import './pages/access_denied/route'; import './pages/job_management/directive'; import './pages/job_management/route'; import './pages/data_frame_new_pivot/directive'; diff --git a/x-pack/plugins/ml/public/data_frame/pages/access_denied/directive.tsx b/x-pack/plugins/ml/public/data_frame/pages/access_denied/directive.tsx new file mode 100644 index 0000000000000..fd8b3bc480ec5 --- /dev/null +++ b/x-pack/plugins/ml/public/data_frame/pages/access_denied/directive.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; + +// @ts-ignore +import { uiModules } from 'ui/modules'; +import uiChrome from 'ui/chrome'; + +const module = uiModules.get('apps/ml', ['react']); + +import { I18nContext } from 'ui/i18n'; +import { InjectorService } from '../../../../common/types/angular'; + +import { Page } from './page'; + +module.directive('mlDataFrameAccessDenied', ($injector: InjectorService) => { + return { + scope: {}, + restrict: 'E', + link: (scope: ng.IScope, element: ng.IAugmentedJQuery) => { + const kbnBaseUrl = $injector.get('kbnBaseUrl'); + const kbnUrl = $injector.get('kbnUrl'); + + const goToKibana = () => { + window.location.href = uiChrome.getBasePath() + kbnBaseUrl; + }; + + const retry = () => { + kbnUrl.redirect('/data_frames'); + }; + + const props = { goToKibana, retry }; + + ReactDOM.render({React.createElement(Page, props)}, element[0]); + + element.on('$destroy', () => { + ReactDOM.unmountComponentAtNode(element[0]); + scope.$destroy(); + }); + }, + }; +}); diff --git a/x-pack/plugins/ml/public/data_frame/pages/access_denied/page.test.tsx b/x-pack/plugins/ml/public/data_frame/pages/access_denied/page.test.tsx new file mode 100644 index 0000000000000..d38cf18b4a78d --- /dev/null +++ b/x-pack/plugins/ml/public/data_frame/pages/access_denied/page.test.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { render, fireEvent, cleanup } from 'react-testing-library'; + +import { I18nProvider } from '@kbn/i18n/react'; + +import { Page } from './page'; + +afterEach(cleanup); + +describe('Data Frame: Access denied ', () => { + test('Minimal initialization', () => { + const props = { + goToKibana: jest.fn(), + retry: jest.fn(), + }; + + const tree = ( + + + + ); + + const { getByText } = render(tree); + + fireEvent.click(getByText(/Back to Kibana home/i)); + fireEvent.click(getByText(/Retry/i)); + + expect(props.goToKibana).toHaveBeenCalledTimes(1); + expect(props.retry).toHaveBeenCalledTimes(1); + }); +}); diff --git a/x-pack/plugins/ml/public/data_frame/pages/access_denied/page.tsx b/x-pack/plugins/ml/public/data_frame/pages/access_denied/page.tsx new file mode 100644 index 0000000000000..fa41b5490b7cd --- /dev/null +++ b/x-pack/plugins/ml/public/data_frame/pages/access_denied/page.tsx @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { SFC } from 'react'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { + EuiButton, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiPage, + EuiPageBody, + EuiPageContentBody, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; + +interface PageProps { + goToKibana: () => void; + retry: () => void; +} +export const Page: SFC = ({ goToKibana, retry }) => ( + + + + + +

+ +

+
+
+
+ + + + +

+ kibana_user, + dataFrameUserParam: ( + data_frame_transforms_user + ), + br:
, + }} + /> +

+
+
+ + + + + + + + + + + + + +
+
+
+); diff --git a/x-pack/plugins/ml/public/data_frame/pages/access_denied/route.ts b/x-pack/plugins/ml/public/data_frame/pages/access_denied/route.ts new file mode 100644 index 0000000000000..63689b4ec551e --- /dev/null +++ b/x-pack/plugins/ml/public/data_frame/pages/access_denied/route.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import uiRoutes from 'ui/routes'; + +// @ts-ignore +import { getDataFrameBreadcrumbs } from '../../breadcrumbs'; + +const template = ``; + +uiRoutes.when('/data_frames/access-denied', { + template, + k7Breadcrumbs: getDataFrameBreadcrumbs, +}); diff --git a/x-pack/plugins/ml/public/data_frame/pages/data_frame_new_pivot/directive.tsx b/x-pack/plugins/ml/public/data_frame/pages/data_frame_new_pivot/directive.tsx index 9fcbfa9b7bef4..3b86138027d1c 100644 --- a/x-pack/plugins/ml/public/data_frame/pages/data_frame_new_pivot/directive.tsx +++ b/x-pack/plugins/ml/public/data_frame/pages/data_frame_new_pivot/directive.tsx @@ -11,15 +11,20 @@ import ReactDOM from 'react-dom'; import { uiModules } from 'ui/modules'; const module = uiModules.get('apps/ml', ['react']); -import { StaticIndexPattern } from 'ui/index_patterns'; +import { IndexPattern } from 'ui/index_patterns'; import { I18nContext } from 'ui/i18n'; import { IPrivate } from 'ui/private'; +import { timefilter } from 'ui/timefilter'; import { InjectorService } from '../../../../common/types/angular'; // @ts-ignore import { SearchItemsProvider } from '../../../jobs/new_job/utils/new_job_utils'; // Simple drop-in type until new_job_utils offers types. -type CreateSearchItems = () => { indexPattern: StaticIndexPattern }; +type CreateSearchItems = () => { + indexPattern: IndexPattern; + savedSearch: any; + combinedQuery: any; +}; import { KibanaContext } from '../../common'; import { Page } from './page'; @@ -30,15 +35,22 @@ module.directive('mlNewDataFrame', ($injector: InjectorService) => { restrict: 'E', link: (scope: ng.IScope, element: ng.IAugmentedJQuery) => { const indexPatterns = $injector.get('indexPatterns'); + const kbnBaseUrl = $injector.get('kbnBaseUrl'); const kibanaConfig = $injector.get('config'); const Private: IPrivate = $injector.get('Private'); + timefilter.disableTimeRangeSelector(); + timefilter.disableAutoRefreshSelector(); + const createSearchItems: CreateSearchItems = Private(SearchItemsProvider); - const { indexPattern } = createSearchItems(); + const { indexPattern, savedSearch, combinedQuery } = createSearchItems(); const kibanaContext = { + combinedQuery, currentIndexPattern: indexPattern, + currentSavedSearch: savedSearch, indexPatterns, + kbnBaseUrl, kibanaConfig, }; diff --git a/x-pack/plugins/ml/public/data_frame/pages/data_frame_new_pivot/route.ts b/x-pack/plugins/ml/public/data_frame/pages/data_frame_new_pivot/route.ts index 858a79e778706..44699310229f5 100644 --- a/x-pack/plugins/ml/public/data_frame/pages/data_frame_new_pivot/route.ts +++ b/x-pack/plugins/ml/public/data_frame/pages/data_frame_new_pivot/route.ts @@ -11,7 +11,7 @@ import { checkBasicLicense } from '../../../license/check_license'; // @ts-ignore import { checkCreateDataFrameJobsPrivilege } from '../../../privilege/check_privilege'; // @ts-ignore -import { loadCurrentIndexPattern } from '../../../util/index_utils'; +import { loadCurrentIndexPattern, loadCurrentSavedSearch } from '../../../util/index_utils'; // @ts-ignore import { getDataFrameCreateBreadcrumbs } from '../../breadcrumbs'; @@ -24,5 +24,6 @@ uiRoutes.when('/data_frames/new_job/step/pivot?', { CheckLicense: checkBasicLicense, privileges: checkCreateDataFrameJobsPrivilege, indexPattern: loadCurrentIndexPattern, + savedSearch: loadCurrentSavedSearch, }, }); diff --git a/x-pack/plugins/ml/public/data_frame/pages/data_frame_new_pivot/wizard.tsx b/x-pack/plugins/ml/public/data_frame/pages/data_frame_new_pivot/wizard.tsx index 006cd199bbe8b..819a77889567a 100644 --- a/x-pack/plugins/ml/public/data_frame/pages/data_frame_new_pivot/wizard.tsx +++ b/x-pack/plugins/ml/public/data_frame/pages/data_frame_new_pivot/wizard.tsx @@ -85,7 +85,7 @@ export const Wizard: SFC = React.memo(() => { const [currentStep, setCurrentStep] = useState(WIZARD_STEPS.DEFINE_PIVOT); // The DEFINE_PIVOT state - const [pivotState, setPivot] = useState(getDefaultPivotState()); + const [pivotState, setPivot] = useState(getDefaultPivotState(kibanaContext)); // The JOB_DETAILS state const [jobDetailsState, setJobDetails] = useState(getDefaultJobDetailsState()); @@ -167,11 +167,8 @@ export const Wizard: SFC = React.memo(() => { children: ( {jobCreate} - {currentStep === WIZARD_STEPS.JOB_CREATE && ( - setCurrentStep(WIZARD_STEPS.JOB_DETAILS)} - previousActive={!jobCreateState.created} - /> + {currentStep === WIZARD_STEPS.JOB_CREATE && !jobCreateState.created && ( + setCurrentStep(WIZARD_STEPS.JOB_DETAILS)} /> )} ), diff --git a/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/__snapshots__/actions.test.tsx.snap b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/__snapshots__/action_delete.test.tsx.snap similarity index 100% rename from x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/__snapshots__/actions.test.tsx.snap rename to x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/__snapshots__/action_delete.test.tsx.snap diff --git a/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/__snapshots__/action_start.test.tsx.snap b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/__snapshots__/action_start.test.tsx.snap new file mode 100644 index 0000000000000..bdd95fd1d4a7f --- /dev/null +++ b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/__snapshots__/action_start.test.tsx.snap @@ -0,0 +1,24 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Data Frame: Job List Actions Minimal initialization 1`] = ` + + + + Start + + + +`; diff --git a/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/action_delete.test.tsx b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/action_delete.test.tsx new file mode 100644 index 0000000000000..5496f5eedb1de --- /dev/null +++ b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/action_delete.test.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { shallow } from 'enzyme'; +import React from 'react'; + +import { DataFrameJobListRow } from './common'; +import { DeleteAction } from './action_delete'; + +import dataFrameJobListRow from './__mocks__/data_frame_job_list_row.json'; + +describe('Data Frame: Job List Actions ', () => { + test('Minimal initialization', () => { + const item: DataFrameJobListRow = dataFrameJobListRow; + const props = { + disabled: false, + item, + deleteJob(d: DataFrameJobListRow) {}, + }; + + const wrapper = shallow(); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/action_delete.tsx b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/action_delete.tsx new file mode 100644 index 0000000000000..9fae0e4cf4459 --- /dev/null +++ b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/action_delete.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, SFC, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiButtonEmpty, + EuiConfirmModal, + EuiOverlayMask, + EuiToolTip, + EUI_MODAL_CONFIRM_BUTTON, +} from '@elastic/eui'; + +import { + checkPermission, + createPermissionFailureMessage, +} from '../../../../../privilege/check_privilege'; + +import { DataFrameJobListRow, DATA_FRAME_RUNNING_STATE } from './common'; + +interface DeleteActionProps { + item: DataFrameJobListRow; + deleteJob(d: DataFrameJobListRow): void; +} + +export const DeleteAction: SFC = ({ deleteJob, item }) => { + const disabled = item.state.task_state === DATA_FRAME_RUNNING_STATE.STARTED; + + const canDeleteDataFrameJob: boolean = checkPermission('canDeleteDataFrameJob'); + + const [isModalVisible, setModalVisible] = useState(false); + + const closeModal = () => setModalVisible(false); + const deleteAndCloseModal = () => { + setModalVisible(false); + deleteJob(item); + }; + const openModal = () => setModalVisible(true); + + const buttonDeleteText = i18n.translate('xpack.ml.dataframe.jobsList.deleteActionName', { + defaultMessage: 'Delete', + }); + + let deleteButton = ( + + {buttonDeleteText} + + ); + + if (disabled || !canDeleteDataFrameJob) { + deleteButton = ( + + {deleteButton} + + ); + } + + return ( + + {deleteButton} + {isModalVisible && ( + + +

+ {i18n.translate('xpack.ml.dataframe.jobsList.deleteModalBody', { + defaultMessage: `Are you sure you want to delete this job? The job's target index and optional Kibana index pattern will not be deleted.`, + })} +

+
+
+ )} +
+ ); +}; diff --git a/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/action_start.test.tsx b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/action_start.test.tsx new file mode 100644 index 0000000000000..ed527b234adaa --- /dev/null +++ b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/action_start.test.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { shallow } from 'enzyme'; +import React from 'react'; + +import { DataFrameJobListRow } from './common'; +import { StartAction } from './action_start'; + +import dataFrameJobListRow from './__mocks__/data_frame_job_list_row.json'; + +describe('Data Frame: Job List Actions ', () => { + test('Minimal initialization', () => { + const item: DataFrameJobListRow = dataFrameJobListRow; + const props = { + disabled: false, + item, + startJob(d: DataFrameJobListRow) {}, + }; + + const wrapper = shallow(); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/action_start.tsx b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/action_start.tsx new file mode 100644 index 0000000000000..9f61bbae9fdcd --- /dev/null +++ b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/action_start.tsx @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, SFC, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiButtonEmpty, + EuiConfirmModal, + EuiOverlayMask, + EuiToolTip, + EUI_MODAL_CONFIRM_BUTTON, +} from '@elastic/eui'; + +import { + checkPermission, + createPermissionFailureMessage, +} from '../../../../../privilege/check_privilege'; + +import { DataFrameJobListRow, isCompletedBatchJob } from './common'; + +interface StartActionProps { + item: DataFrameJobListRow; + startJob(d: DataFrameJobListRow): void; +} + +export const StartAction: SFC = ({ startJob, item }) => { + const canStartStopDataFrameJob: boolean = checkPermission('canStartStopDataFrameJob'); + + const [isModalVisible, setModalVisible] = useState(false); + + const closeModal = () => setModalVisible(false); + const startAndCloseModal = () => { + setModalVisible(false); + startJob(item); + }; + const openModal = () => setModalVisible(true); + + const buttonStartText = i18n.translate('xpack.ml.dataframe.jobsList.startActionName', { + defaultMessage: 'Start', + }); + + // Disable start for batch jobs which have completed. + const completedBatchJob = isCompletedBatchJob(item); + + let startButton = ( + + {buttonStartText} + + ); + + if (!canStartStopDataFrameJob || completedBatchJob) { + startButton = ( + + {startButton} + + ); + } + + return ( + + {startButton} + {isModalVisible && ( + + +

+ {i18n.translate('xpack.ml.dataframe.jobsList.startModalBody', { + defaultMessage: + 'A data frame job will increase search and indexing load in your cluster. Please stop the job if excessive load is experienced. Are you sure you want to start this job?', + })} +

+
+
+ )} +
+ ); +}; diff --git a/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/actions.test.tsx b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/actions.test.tsx index a0991aa8fdd06..53a70593b31eb 100644 --- a/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/actions.test.tsx +++ b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/actions.test.tsx @@ -4,28 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { shallow } from 'enzyme'; -import React from 'react'; - -import { DataFrameJobListRow } from './common'; -import { DeleteAction, getActions } from './actions'; - -import dataFrameJobListRow from './__mocks__/data_frame_job_list_row.json'; - -describe('Data Frame: Job List Actions ', () => { - test('Minimal initialization', () => { - const item: DataFrameJobListRow = dataFrameJobListRow; - const props = { - disabled: false, - item, - deleteJob(d: DataFrameJobListRow) {}, - }; - - const wrapper = shallow(); - - expect(wrapper).toMatchSnapshot(); - }); -}); +import { getActions } from './actions'; describe('Data Frame: Job List Actions', () => { test('getActions()', () => { diff --git a/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/actions.tsx b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/actions.tsx index ae999360b1c02..465bbc40b40a4 100644 --- a/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/actions.tsx +++ b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/actions.tsx @@ -4,15 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, SFC, useState } from 'react'; +import React from 'react'; import { i18n } from '@kbn/i18n'; -import { - EuiButtonEmpty, - EuiConfirmModal, - EuiOverlayMask, - EuiToolTip, - EUI_MODAL_CONFIRM_BUTTON, -} from '@elastic/eui'; +import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; import { checkPermission, @@ -22,96 +16,8 @@ import { import { DataFrameJobListRow, DATA_FRAME_RUNNING_STATE } from './common'; import { deleteJobFactory, startJobFactory, stopJobFactory } from './job_service'; -interface DeleteActionProps { - disabled: boolean; - item: DataFrameJobListRow; - deleteJob(d: DataFrameJobListRow): void; -} - -export const DeleteAction: SFC = ({ deleteJob, disabled, item }) => { - const canDeleteDataFrameJob: boolean = checkPermission('canDeleteDataFrameJob'); - - const [isModalVisible, setModalVisible] = useState(false); - - const closeModal = () => setModalVisible(false); - const deleteAndCloseModal = () => { - setModalVisible(false); - deleteJob(item); - }; - const openModal = () => setModalVisible(true); - - const buttonDeleteText = i18n.translate('xpack.ml.dataframe.jobsList.deleteActionName', { - defaultMessage: 'Delete', - }); - - let deleteButton = ( - - {buttonDeleteText} - - ); - - if (disabled || !canDeleteDataFrameJob) { - deleteButton = ( - - {deleteButton} - - ); - } - - return ( - - {deleteButton} - {isModalVisible && ( - - -

- {i18n.translate('xpack.ml.dataframe.jobsList.deleteModalBody', { - defaultMessage: 'Are you sure you want to delete this job?', - })} -

-
-
- )} -
- ); -}; +import { StartAction } from './action_start'; +import { DeleteAction } from './action_delete'; export const getActions = (getJobs: () => void) => { const canStartStopDataFrameJob: boolean = checkPermission('canStartStopDataFrameJob'); @@ -125,35 +31,7 @@ export const getActions = (getJobs: () => void) => { isPrimary: true, render: (item: DataFrameJobListRow) => { if (item.state.task_state !== DATA_FRAME_RUNNING_STATE.STARTED) { - const buttonStartText = i18n.translate('xpack.ml.dataframe.jobsList.startActionName', { - defaultMessage: 'Start', - }); - - const startButton = ( - startJob(item)} - aria-label={buttonStartText} - > - {buttonStartText} - - ); - - if (!canStartStopDataFrameJob) { - return ( - - {startButton} - - ); - } - - return startButton; + return ; } const buttonStopText = i18n.translate('xpack.ml.dataframe.jobsList.stopActionName', { @@ -188,13 +66,7 @@ export const getActions = (getJobs: () => void) => { }, { render: (item: DataFrameJobListRow) => { - return ( - - ); + return ; }, }, ]; diff --git a/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/common.test.ts b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/common.test.ts new file mode 100644 index 0000000000000..ae903746fdfca --- /dev/null +++ b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/common.test.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import mockDataFrameJobListRow from './__mocks__/data_frame_job_list_row.json'; + +import { DATA_FRAME_RUNNING_STATE, isCompletedBatchJob } from './common'; + +describe('Data Frame: isCompletedBatchJob()', () => { + test('isCompletedBatchJob()', () => { + // check the job config/state against the conditions + // that will be used by isCompletedBatchJob() + // followed by a call to isCompletedBatchJob() itself + expect(mockDataFrameJobListRow.state.checkpoint === 1).toBe(true); + expect(mockDataFrameJobListRow.sync === undefined).toBe(true); + expect(mockDataFrameJobListRow.state.task_state === DATA_FRAME_RUNNING_STATE.STOPPED).toBe( + true + ); + expect(isCompletedBatchJob(mockDataFrameJobListRow)).toBe(true); + + // adapt the mock config to resemble a non-completed job. + mockDataFrameJobListRow.state.checkpoint = 0; + expect(isCompletedBatchJob(mockDataFrameJobListRow)).toBe(false); + }); +}); diff --git a/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/common.ts b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/common.ts index 2ae0af0e9d819..d11c0f87af9c9 100644 --- a/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/common.ts +++ b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/common.ts @@ -12,6 +12,7 @@ export interface DataFrameJob { dest: string; id: JobId; source: string; + sync?: object; } export enum DATA_FRAME_RUNNING_STATE { @@ -50,6 +51,7 @@ export interface DataFrameJobStats { } export interface DataFrameJobListRow { + id: JobId; state: DataFrameJobState; stats: DataFrameJobStats; config: DataFrameJob; @@ -63,3 +65,13 @@ export enum DataFrameJobListColumn { } export type ItemIdToExpandedRowMap = Dictionary; + +export function isCompletedBatchJob(item: DataFrameJobListRow) { + // If `checkpoint=1`, `sync` is missing from the config and state is stopped, + // then this is a completed batch data frame job. + return ( + item.state.checkpoint === 1 && + item.config.sync === undefined && + item.state.task_state === DATA_FRAME_RUNNING_STATE.STOPPED + ); +} diff --git a/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/job_service/get_jobs.ts b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/job_service/get_jobs.ts index 2718c56fa08ce..52a99e7294d32 100644 --- a/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/job_service/get_jobs.ts +++ b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/job_service/get_jobs.ts @@ -42,16 +42,21 @@ export const getJobsFactory = ( const jobConfigs: GetDataFrameTransformsResponse = await ml.dataFrame.getDataFrameTransforms(); const jobStats: GetDataFrameTransformsStatsResponse = await ml.dataFrame.getDataFrameTransformsStats(); - const tableRows = jobConfigs.transforms.map(config => { - const stats = jobStats.transforms.find(d => config.id === d.id); + const tableRows = jobConfigs.transforms.reduce( + (reducedtableRows, config) => { + const stats = jobStats.transforms.find(d => config.id === d.id); - if (stats === undefined) { - throw new Error('job stats not available'); - } - - // table with expandable rows requires `id` on the outer most level - return { config, id: config.id, state: stats.state, stats: stats.stats }; - }); + // A newly created job might not have corresponding stats yet. + // If that's the case we just skip the job and don't add it to the jobs list yet. + if (stats === undefined) { + return reducedtableRows; + } + // Table with expandable rows requires `id` on the outer most level + reducedtableRows.push({ config, id: config.id, state: stats.state, stats: stats.stats }); + return reducedtableRows; + }, + [] as DataFrameJobListRow[] + ); setDataFrameJobs(tableRows); } catch (e) { diff --git a/x-pack/plugins/ml/public/datavisualizer/datavisualizer_controller.js b/x-pack/plugins/ml/public/datavisualizer/datavisualizer_controller.js index 8403d6bf47b04..7e32a35692bf0 100644 --- a/x-pack/plugins/ml/public/datavisualizer/datavisualizer_controller.js +++ b/x-pack/plugins/ml/public/datavisualizer/datavisualizer_controller.js @@ -12,6 +12,7 @@ */ import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; import rison from 'rison-node'; import 'plugins/ml/components/form_filter_input'; @@ -24,7 +25,7 @@ import { notify, toastNotifications } from 'ui/notify'; import { ML_JOB_FIELD_TYPES, KBN_FIELD_TYPES } from 'plugins/ml/../common/constants/field_types'; import { getDataVisualizerBreadcrumbs } from './breadcrumbs'; import { kbnTypeToMLJobType } from 'plugins/ml/util/field_types_utils'; -import { IntervalHelperProvider } from 'plugins/ml/util/ml_time_buckets'; +import { MlTimeBuckets } from 'plugins/ml/util/ml_time_buckets'; import { checkBasicLicense, isFullLicense } from 'plugins/ml/license/check_license'; import { checkGetJobsPrivilege } from 'plugins/ml/privilege/check_privilege'; import { SearchItemsProvider } from 'plugins/ml/jobs/new_job/utils/new_job_utils'; @@ -51,14 +52,7 @@ import { uiModules } from 'ui/modules'; const module = uiModules.get('apps/ml'); module - .controller('MlDataVisualizerViewFields', function ( - $scope, - $timeout, - $window, - Private, - AppState, - config, - i18n) { + .controller('MlDataVisualizerViewFields', function ($scope, $timeout, $window, Private, AppState, config) { timefilter.enableTimeRangeSelector(); timefilter.enableAutoRefreshSelector(); @@ -112,13 +106,13 @@ module $scope.searchQueryText = _.get(queryBarQry, 'query', ''); } else { toastNotifications.addWarning({ - title: i18n('xpack.ml.datavisualizer.languageSyntaxNotSupportedWarningTitle', { + title: i18n.translate('xpack.ml.datavisualizer.languageSyntaxNotSupportedWarningTitle', { defaultMessage: '{language} syntax not supported', values: { language: (queryBarQry.language !== undefined) ? queryBarQry.language : '', } }), - text: i18n('xpack.ml.datavisualizer.languageSyntaxNotSupportedWarningDescription', { + text: i18n.translate('xpack.ml.datavisualizer.languageSyntaxNotSupportedWarningDescription', { defaultMessage: 'The Data Visualizer currently only supports queries using the lucene query syntax.', }), }); @@ -129,8 +123,6 @@ module $scope.samplerShardSize = $scope.appState.samplerShardSize ? $scope.appState.samplerShardSize : 5000; // -1 indicates no sampling. - const MlTimeBuckets = Private(IntervalHelperProvider); - let metricFieldRegexp; let metricFieldFilterTimeout; let fieldRegexp; @@ -513,7 +505,7 @@ module console.log('DataVisualizer - error getting stats for metric cards from elasticsearch:', err); if (err.statusCode === 500) { notify.error( - i18n('xpack.ml.datavisualizer.metricInternalServerErrorTitle', { + i18n.translate('xpack.ml.datavisualizer.metricInternalServerErrorTitle', { defaultMessage: 'Error loading data for metrics in index {index}. {message}. ' + 'The request may have timed out. Try using a smaller sample size or narrowing the time range.', values: { @@ -525,7 +517,7 @@ module ); } else { notify.error( - i18n('xpack.ml.datavisualizer.loadingMetricDataErrorTitle', { + i18n.translate('xpack.ml.datavisualizer.loadingMetricDataErrorTitle', { defaultMessage: 'Error loading data for metrics in index {index}. {message}', values: { index: indexPattern.title, @@ -583,7 +575,7 @@ module console.log('DataVisualizer - error getting non metric field stats from elasticsearch:', err); if (err.statusCode === 500) { notify.error( - i18n('xpack.ml.datavisualizer.fieldsInternalServerErrorTitle', { + i18n.translate('xpack.ml.datavisualizer.fieldsInternalServerErrorTitle', { defaultMessage: 'Error loading data for fields in index {index}. {message}. ' + 'The request may have timed out. Try using a smaller sample size or narrowing the time range.', values: { @@ -595,7 +587,7 @@ module ); } else { notify.error( - i18n('xpack.ml.datavisualizer.loadingFieldsDataErrorTitle', { + i18n.translate('xpack.ml.datavisualizer.loadingFieldsDataErrorTitle', { defaultMessage: 'Error loading data for fields in index {index}. {message}', values: { index: indexPattern.title, @@ -652,7 +644,7 @@ module console.log('DataVisualizer - error getting overall stats from elasticsearch:', err); if (err.statusCode === 500) { notify.error( - i18n('xpack.ml.datavisualizer.overallFieldsInternalServerErrorTitle', { + i18n.translate('xpack.ml.datavisualizer.overallFieldsInternalServerErrorTitle', { defaultMessage: 'Error loading data for fields in index {index}. {message}. ' + 'The request may have timed out. Try using a smaller sample size or narrowing the time range.', values: { @@ -664,7 +656,7 @@ module ); } else { notify.error( - i18n('xpack.ml.datavisualizer.loadingOverallFieldsDataErrorTitle', { + i18n.translate('xpack.ml.datavisualizer.loadingOverallFieldsDataErrorTitle', { defaultMessage: 'Error loading data for fields in index {index}. {message}', values: { index: indexPattern.title, diff --git a/x-pack/plugins/ml/public/explorer/explorer.js b/x-pack/plugins/ml/public/explorer/explorer.js index aaf4b77b66d32..375040301a0a3 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 3398eb2e168ec..1f010d01f47b2 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/explorer/explorer_controller.js b/x-pack/plugins/ml/public/explorer/explorer_controller.js index 51f785e021973..1bdff98b0aec2 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_controller.js +++ b/x-pack/plugins/ml/public/explorer/explorer_controller.js @@ -28,7 +28,7 @@ import { getAnomalyExplorerBreadcrumbs } from './breadcrumbs'; import { checkFullLicense } from '../license/check_license'; import { checkGetJobsPrivilege } from '../privilege/check_privilege'; import { getIndexPatterns, loadIndexPatterns } from '../util/index_utils'; -import { IntervalHelperProvider } from 'plugins/ml/util/ml_time_buckets'; +import { MlTimeBuckets } from 'plugins/ml/util/ml_time_buckets'; import { explorer$ } from './explorer_dashboard_service'; import { mlFieldFormatService } from 'plugins/ml/services/field_format_service'; import { mlJobService } from '../services/job_service'; @@ -82,7 +82,7 @@ module.controller('MlExplorerController', function ( const tzConfig = config.get('dateFormat:tz'); $scope.dateFormatTz = (tzConfig !== 'Browser') ? tzConfig : moment.tz.guess(); - $scope.MlTimeBuckets = Private(IntervalHelperProvider); + $scope.MlTimeBuckets = MlTimeBuckets; let resizeTimeout = null; diff --git a/x-pack/plugins/ml/public/explorer/explorer_utils.js b/x-pack/plugins/ml/public/explorer/explorer_utils.js index 6ceddde3077aa..39f294a567528 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_utils.js +++ b/x-pack/plugins/ml/public/explorer/explorer_utils.js @@ -20,6 +20,7 @@ import { mlResultsService } from 'plugins/ml/services/results_service'; import { MAX_CATEGORY_EXAMPLES, MAX_INFLUENCER_FIELD_VALUES, + SWIMLANE_TYPE, VIEW_BY_JOB_LABEL, } from './explorer_constants'; import { @@ -191,6 +192,7 @@ export function getSelectionTimeRange(selectedCells, interval, bounds) { export function getSelectionInfluencers(selectedCells, fieldName) { if ( selectedCells !== null && + selectedCells.type !== SWIMLANE_TYPE.OVERALL && selectedCells.viewByFieldName !== undefined && selectedCells.viewByFieldName !== VIEW_BY_JOB_LABEL ) { diff --git a/x-pack/plugins/ml/public/file_datavisualizer/components/fields_stats/field_stats_card.js b/x-pack/plugins/ml/public/file_datavisualizer/components/fields_stats/field_stats_card.js index b6feb695de8a0..b1f32ca8f5ecf 100644 --- a/x-pack/plugins/ml/public/file_datavisualizer/components/fields_stats/field_stats_card.js +++ b/x-pack/plugins/ml/public/file_datavisualizer/components/fields_stats/field_stats_card.js @@ -11,6 +11,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { FieldTypeIcon } from '../../../components/field_type_icon'; +import { DisplayValue } from '../../../components/display_value'; import { getMLJobTypeAriaLabel } from '../../../util/field_types_utils'; export function FieldStatsCard({ field }) { @@ -93,9 +94,15 @@ export function FieldStatsCard({ field }) {
-
{field.min_value}
-
{field.median_value}
-
{field.max_value}
+
+ +
+
+ +
+
+ +
} diff --git a/x-pack/plugins/ml/public/formatters/abbreviate_whole_number.js b/x-pack/plugins/ml/public/formatters/abbreviate_whole_number.js index 802aeb3a5d19c..445681be4ff81 100644 --- a/x-pack/plugins/ml/public/formatters/abbreviate_whole_number.js +++ b/x-pack/plugins/ml/public/formatters/abbreviate_whole_number.js @@ -12,9 +12,8 @@ */ import numeral from '@elastic/numeral'; -export function abbreviateWholeNumber(value, maxDigits) { - const maxNumDigits = (maxDigits !== undefined ? maxDigits : 3); - if (Math.abs(value) < Math.pow(10, maxNumDigits)) { +export function abbreviateWholeNumber(value, maxDigits = 3) { + if (Math.abs(value) < Math.pow(10, maxDigits)) { return value; } else { return numeral(value).format('0a'); diff --git a/x-pack/plugins/ml/public/hacks/toggle_app_link_in_nav.js b/x-pack/plugins/ml/public/hacks/toggle_app_link_in_nav.js index 27c5e28b3300b..ea10b3728d837 100644 --- a/x-pack/plugins/ml/public/hacks/toggle_app_link_in_nav.js +++ b/x-pack/plugins/ml/public/hacks/toggle_app_link_in_nav.js @@ -8,7 +8,7 @@ import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info'; import { uiModules } from 'ui/modules'; -import { onStart } from 'ui/new_platform'; +import { npStart } from 'ui/new_platform'; uiModules.get('xpack/ml').run((Private) => { const xpackInfo = Private(XPackInfoProvider); @@ -21,5 +21,5 @@ uiModules.get('xpack/ml').run((Private) => { disabled: !showAppLink || (showAppLink && !xpackInfo.get('features.ml.isAvailable', false)) }; - onStart(({ core }) => core.chrome.navLinks.update('ml', navLinkUpdates)); + npStart.core.chrome.navLinks.update('ml', navLinkUpdates); }); diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js b/x-pack/plugins/ml/public/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js index 214cba4edd30e..9d61d79b1d3e5 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js @@ -150,15 +150,17 @@ export const DeleteJobModal = injectI18n(class extends Component { }} />

- {(this.state.jobs.length > 1) && -

- -

- } +

+ +

} diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/job_actions/results.js b/x-pack/plugins/ml/public/jobs/jobs_list/components/job_actions/results.js index 7bc1335cf001b..8243f64f6114e 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/job_actions/results.js +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/job_actions/results.js @@ -14,32 +14,13 @@ import { } from '@elastic/eui'; import chrome from 'ui/chrome'; -import moment from 'moment'; -const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; -import { mlJobService } from 'plugins/ml/services/job_service'; +import { mlJobService } from '../../../../services/job_service'; import { injectI18n } from '@kbn/i18n/react'; function getLink(location, jobs) { - let from = undefined; - let to = undefined; - if (jobs.length === 1) { - from = jobs[0].earliestTimestampMs; - to = jobs[0].latestTimestampMs; - } else { - const jobsWithData = jobs.filter(j => (j.earliestTimestampMs !== undefined)); - if (jobsWithData.length > 0) { - from = Math.min(...jobsWithData.map(j => j.earliestTimestampMs)); - to = Math.max(...jobsWithData.map(j => j.latestTimestampMs)); - } - } - - const fromString = moment(from).format(TIME_FORMAT); // Defaults to 'now' if 'from' is undefined - const toString = moment(to).format(TIME_FORMAT); // Defaults to 'now' if 'to' is undefined - - const jobIds = jobs.map(j => j.id); - const url = mlJobService.createResultsUrl(jobIds, fromString, toString, location); - return `${chrome.getBasePath()}/app/${url}`; + const resultsPageUrl = mlJobService.createResultsUrlForJobs(jobs, location); + return `${chrome.getBasePath()}/app/${resultsPageUrl}`; } function ResultLinksUI({ jobs, intl }) { diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js b/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js index a5ce75a46fec8..69696e07ee8ff 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js @@ -95,6 +95,7 @@ export class JobsListView extends Component { componentWillUnmount() { timefilter.off('refreshIntervalUpdate'); + deletingJobsRefreshTimeout = null; this.clearRefreshInterval(); } @@ -126,7 +127,7 @@ export class JobsListView extends Component { } else { this.setRefreshInterval(value); } - this.refreshJobSummaryList(true); + this.refreshJobSummaryList(); } setRefreshInterval(interval) { @@ -317,7 +318,7 @@ export class JobsListView extends Component { // if there are some jobs in a deleting state, start polling for // deleting jobs so we can update the jobs list once the // deleting tasks are over - this.checkDeletingJobTasks(); + this.checkDeletingJobTasks(forceRefresh); } } catch (error) { console.error(error); @@ -326,17 +327,22 @@ export class JobsListView extends Component { } } - async checkDeletingJobTasks() { - const { jobIds } = await ml.jobs.deletingJobTasks(); + async checkDeletingJobTasks(forceRefresh = false) { + const { jobIds: taskJobIds } = await ml.jobs.deletingJobTasks(); - if (jobIds.length === 0 || isEqual(jobIds.sort(), this.state.deletingJobIds.sort())) { - this.setState({ - deletingJobIds: jobIds, - }); - this.refreshJobSummaryList(true); + const taskListHasChanged = (isEqual(taskJobIds.sort(), this.state.deletingJobIds.sort()) === false); + + this.setState({ + deletingJobIds: taskJobIds, + }); + + // only reload the jobs list if the contents of the task list has changed + // or the check refresh has been forced i.e. from a user action + if (taskListHasChanged || forceRefresh) { + this.refreshJobSummaryList(); } - if (jobIds.length > 0 && deletingJobsRefreshTimeout === null) { + if (taskJobIds.length > 0 && deletingJobsRefreshTimeout === null) { deletingJobsRefreshTimeout = setTimeout(() => { deletingJobsRefreshTimeout = null; this.checkDeletingJobTasks(); diff --git a/x-pack/plugins/ml/public/jobs/new_job/advanced/detector_filter_modal/detector_filter_modal_controller.js b/x-pack/plugins/ml/public/jobs/new_job/advanced/detector_filter_modal/detector_filter_modal_controller.js index 428a2bf4de102..1baceb4aa6c1a 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/advanced/detector_filter_modal/detector_filter_modal_controller.js +++ b/x-pack/plugins/ml/public/jobs/new_job/advanced/detector_filter_modal/detector_filter_modal_controller.js @@ -7,6 +7,7 @@ import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; import angular from 'angular'; import { mlMessageBarService } from 'plugins/ml/components/messagebar/messagebar_service'; @@ -14,10 +15,10 @@ import { mlMessageBarService } from 'plugins/ml/components/messagebar/messagebar import { uiModules } from 'ui/modules'; const module = uiModules.get('apps/ml'); -module.controller('MlDetectorFilterModal', function ($scope, $modalInstance, params, i18n) { +module.controller('MlDetectorFilterModal', function ($scope, $modalInstance, params) { const msgs = mlMessageBarService; msgs.clear(); - $scope.title = i18n('xpack.ml.newJob.advanced.detectorFilterModal.addNewFilterTitle', { + $scope.title = i18n.translate('xpack.ml.newJob.advanced.detectorFilterModal.addNewFilterTitle', { defaultMessage: 'Add new filter' }); $scope.detector = params.detector; @@ -27,10 +28,10 @@ module.controller('MlDetectorFilterModal', function ($scope, $modalInstance, par const add = params.add; const validate = params.validate; - $scope.updateButtonLabel = i18n('xpack.ml.newJob.advanced.detectorFilterModal.updateButtonLabel', { + $scope.updateButtonLabel = i18n.translate('xpack.ml.newJob.advanced.detectorFilterModal.updateButtonLabel', { defaultMessage: 'Update' }); - $scope.addButtonLabel = i18n('xpack.ml.newJob.advanced.detectorFilterModal.addButtonLabel', { + $scope.addButtonLabel = i18n.translate('xpack.ml.newJob.advanced.detectorFilterModal.addButtonLabel', { defaultMessage: 'Add' }); @@ -99,7 +100,7 @@ module.controller('MlDetectorFilterModal', function ($scope, $modalInstance, par // editing an existing filter $scope.editMode = true; $scope.filter = params.filter; - $scope.title = i18n('xpack.ml.newJob.advanced.detectorFilterModal.editFilterTitle', { + $scope.title = i18n.translate('xpack.ml.newJob.advanced.detectorFilterModal.editFilterTitle', { defaultMessage: 'Edit filter' }); index = params.index; diff --git a/x-pack/plugins/ml/public/jobs/new_job/advanced/detector_modal/detector_modal_controller.js b/x-pack/plugins/ml/public/jobs/new_job/advanced/detector_modal/detector_modal_controller.js index 8da5f59266263..e4487ea54020c 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/advanced/detector_modal/detector_modal_controller.js +++ b/x-pack/plugins/ml/public/jobs/new_job/advanced/detector_modal/detector_modal_controller.js @@ -7,6 +7,7 @@ import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; import angular from 'angular'; import { detectorToString } from 'plugins/ml/util/string_utils'; import { mlMessageBarService } from 'plugins/ml/components/messagebar/messagebar_service'; @@ -14,10 +15,10 @@ import { mlMessageBarService } from 'plugins/ml/components/messagebar/messagebar import { uiModules } from 'ui/modules'; const module = uiModules.get('apps/ml'); -module.controller('MlDetectorModal', function ($scope, $modalInstance, params, i18n) { +module.controller('MlDetectorModal', function ($scope, $modalInstance, params) { const msgs = mlMessageBarService; msgs.clear(); - $scope.title = i18n('xpack.ml.newJob.advanced.detectorModal.addNewDetectorTitle', { + $scope.title = i18n.translate('xpack.ml.newJob.advanced.detectorModal.addNewDetectorTitle', { defaultMessage: 'Add new detector' }); $scope.detector = { 'function': '' }; @@ -25,10 +26,10 @@ module.controller('MlDetectorModal', function ($scope, $modalInstance, params, i $scope.editMode = false; let index = -1; - $scope.updateButtonLabel = i18n('xpack.ml.newJob.advanced.detectorModal.updateButtonLabel', { + $scope.updateButtonLabel = i18n.translate('xpack.ml.newJob.advanced.detectorModal.updateButtonLabel', { defaultMessage: 'Update' }); - $scope.addButtonLabel = i18n('xpack.ml.newJob.advanced.detectorModal.addButtonLabel', { + $scope.addButtonLabel = i18n.translate('xpack.ml.newJob.advanced.detectorModal.addButtonLabel', { defaultMessage: 'Add' }); @@ -90,7 +91,7 @@ module.controller('MlDetectorModal', function ($scope, $modalInstance, params, i if (params.detector) { $scope.detector = params.detector; index = params.index; - $scope.title = i18n('xpack.ml.newJob.advanced.detectorModal.editDetectorTitle', { + $scope.title = i18n.translate('xpack.ml.newJob.advanced.detectorModal.editDetectorTitle', { defaultMessage: 'Edit detector' }); $scope.editMode = true; @@ -102,14 +103,14 @@ module.controller('MlDetectorModal', function ($scope, $modalInstance, params, i $scope.functionChange = function () { const func = _.findWhere($scope.functions, { id: $scope.detector.function }); - $scope.helpLink.label = i18n('xpack.ml.newJob.advanced.detectorModal.helpForAnalyticalFunctionsLabel', { + $scope.helpLink.label = i18n.translate('xpack.ml.newJob.advanced.detectorModal.helpForAnalyticalFunctionsLabel', { defaultMessage: 'Help for analytical functions' }); $scope.helpLink.uri = 'ml-functions.html'; if (func) { $scope.helpLink.uri = func.uri; - $scope.helpLink.label = i18n('xpack.ml.newJob.advanced.detectorModal.helpForAnalyticalFunctionLabel', { + $scope.helpLink.label = i18n.translate('xpack.ml.newJob.advanced.detectorModal.helpForAnalyticalFunctionLabel', { defaultMessage: 'Help for {funcId}', values: { funcId: func.id } }); diff --git a/x-pack/plugins/ml/public/jobs/new_job/advanced/detectors_list_directive.js b/x-pack/plugins/ml/public/jobs/new_job/advanced/detectors_list_directive.js index b42a5f7377b99..a63e936849643 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/advanced/detectors_list_directive.js +++ b/x-pack/plugins/ml/public/jobs/new_job/advanced/detectors_list_directive.js @@ -9,6 +9,7 @@ // directive for displaying detectors form list. import angular from 'angular'; +import { i18n } from '@kbn/i18n'; import _ from 'lodash'; import 'plugins/ml/jobs/new_job/advanced/detector_modal'; import 'plugins/ml/jobs/new_job/advanced/detector_filter_modal'; @@ -21,7 +22,7 @@ import { mlJobService } from 'plugins/ml/services/job_service'; import { uiModules } from 'ui/modules'; const module = uiModules.get('apps/ml'); -module.directive('mlJobDetectorsList', function ($modal, i18n) { +module.directive('mlJobDetectorsList', function ($modal) { return { restrict: 'AE', replace: true, @@ -97,7 +98,7 @@ module.directive('mlJobDetectorsList', function ($modal, i18n) { then: function (callback) { callback({ success: false, - message: i18n('xpack.ml.newJob.advanced.detectorsList.invalidExcludeFrequentParameterErrorMessage', { + message: i18n.translate('xpack.ml.newJob.advanced.detectorsList.invalidExcludeFrequentParameterErrorMessage', { defaultMessage: '{excludeFrequentParam} value must be: {allValue}, {noneValue}, {byValue} or {overValue}', values: { excludeFrequentParam: 'exclude_frequent', @@ -124,7 +125,7 @@ module.directive('mlJobDetectorsList', function ($modal, i18n) { return { success: false, message: ( - resp.message || i18n('xpack.ml.newJob.advanced.detectorsList.validationFailedErrorMessage', { + resp.message || i18n.translate('xpack.ml.newJob.advanced.detectorsList.validationFailedErrorMessage', { defaultMessage: 'Validation failed' }) ) diff --git a/x-pack/plugins/ml/public/jobs/new_job/advanced/new_job_controller.js b/x-pack/plugins/ml/public/jobs/new_job/advanced/new_job_controller.js index 775ebea6d032b..5724c5cd8a670 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/advanced/new_job_controller.js +++ b/x-pack/plugins/ml/public/jobs/new_job/advanced/new_job_controller.js @@ -7,6 +7,7 @@ import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; import angular from 'angular'; import 'ace'; import 'ui/angular_ui_select'; @@ -73,14 +74,7 @@ import { uiModules } from 'ui/modules'; const module = uiModules.get('apps/ml'); module.controller('MlNewJob', - function ( - $scope, - $route, - $location, - $modal, - Private, - mlConfirmModalService, - i18n) { + function ($scope, $route, $location, $modal, Private, mlConfirmModalService) { timefilter.disableTimeRangeSelector(); // remove time picker from top of page timefilter.disableAutoRefreshSelector(); // remove time picker from top of page @@ -137,53 +131,58 @@ module.controller('MlNewJob', $scope.elasticServerInfo = {}; $scope.jobGroupsUpdateFunction = {}; - $scope.enterJobNameLabel = i18n('xpack.ml.newJob.advanced.jobDetails.enterJobNameLabel', { + $scope.enterJobNameLabel = i18n.translate('xpack.ml.newJob.advanced.jobDetails.enterJobNameLabel', { defaultMessage: 'Enter a name for the job' }); - $scope.bucketSpanNotValidFormatLabel = i18n('xpack.ml.newJob.advanced.analysisConfiguration.bucketSpanNotValidFormatLabel', { + $scope.bucketSpanNotValidFormatLabel = i18n.translate('xpack.ml.newJob.advanced.analysisConfiguration.bucketSpanNotValidFormatLabel', { defaultMessage: '{bucketSpan} is not a valid time interval format', values: { bucketSpan: 'bucket_span' } }); - $scope.categorizationFiltersNotValidLabel = i18n('xpack.ml.newJob.advanced.analysisConfiguration.categorizationFiltersNotValidLabel', { - defaultMessage: 'Categorization filters must all be valid regular expressions' - }); - $scope.detectorNotConfiguredLabel = i18n('xpack.ml.newJob.advanced.analysisConfiguration.detectorNotConfiguredLabel', { + $scope.categorizationFiltersNotValidLabel = i18n.translate( + 'xpack.ml.newJob.advanced.analysisConfiguration.categorizationFiltersNotValidLabel', { + defaultMessage: 'Categorization filters must all be valid regular expressions' + }); + $scope.detectorNotConfiguredLabel = i18n.translate('xpack.ml.newJob.advanced.analysisConfiguration.detectorNotConfiguredLabel', { defaultMessage: 'At least one detector should be configured' }); - $scope.influencerNotSelectedLabel = i18n('xpack.ml.newJob.advanced.analysisConfiguration.influencerNotSelectedLabel', { + $scope.influencerNotSelectedLabel = i18n.translate('xpack.ml.newJob.advanced.analysisConfiguration.influencerNotSelectedLabel', { defaultMessage: 'At least one influencer should be selected' }); - $scope.validatingCardinalityLabel = i18n('xpack.ml.newJob.advanced.analysisConfiguration.validatingCardinalityLabel', { + $scope.validatingCardinalityLabel = i18n.translate('xpack.ml.newJob.advanced.analysisConfiguration.validatingCardinalityLabel', { defaultMessage: 'Validating cardinality…' }); - $scope.enableModelPlotLabel = i18n('xpack.ml.newJob.advanced.analysisConfiguration.enableModelPlotLabel', { + $scope.enableModelPlotLabel = i18n.translate('xpack.ml.newJob.advanced.analysisConfiguration.enableModelPlotLabel', { defaultMessage: 'Enable model plot' }); - $scope.specifyTimeFieldLabel = i18n('xpack.ml.newJob.advanced.dataDescription.specifyTimeFieldLabel', { + $scope.specifyTimeFieldLabel = i18n.translate('xpack.ml.newJob.advanced.dataDescription.specifyTimeFieldLabel', { defaultMessage: 'Time field should be specified' }); - $scope.specifyTimeFormatLabel = i18n('xpack.ml.newJob.advanced.dataDescription.specifyTimeFormatLabel', { + $scope.specifyTimeFormatLabel = i18n.translate('xpack.ml.newJob.advanced.dataDescription.specifyTimeFormatLabel', { defaultMessage: 'Time format should be specified' }); $scope.ui = { - pageTitle: i18n('xpack.ml.newJob.advanced.createNewJobTitle', { + pageTitle: i18n.translate('xpack.ml.newJob.advanced.createNewJobTitle', { defaultMessage: 'Create a new job' }), dataLocation: 'ES', dataPreview: '', currentTab: 0, tabs: [ - { index: 0, title: i18n('xpack.ml.newJob.advanced.tabs.jobDetailsLabel', { defaultMessage: 'Job Details' }) }, - { index: 1, title: i18n('xpack.ml.newJob.advanced.tabs.analysisConfigurationLabel', { defaultMessage: 'Analysis Configuration' }) }, + { index: 0, title: i18n.translate( + 'xpack.ml.newJob.advanced.tabs.jobDetailsLabel', { defaultMessage: 'Job Details' }) }, + { index: 1, title: i18n.translate( + 'xpack.ml.newJob.advanced.tabs.analysisConfigurationLabel', { defaultMessage: 'Analysis Configuration' }) }, { index: 2, - title: i18n('xpack.ml.newJob.advanced.tabs.dataDescriptionLabel', { defaultMessage: 'Data Description' }), + title: i18n.translate('xpack.ml.newJob.advanced.tabs.dataDescriptionLabel', { defaultMessage: 'Data Description' }), hidden: true }, - { index: 3, title: i18n('xpack.ml.newJob.advanced.tabs.datafeedLabel', { defaultMessage: 'Datafeed' }) }, - { index: 4, title: i18n('xpack.ml.newJob.advanced.tabs.editJsonLabel', { defaultMessage: 'Edit JSON' }) }, - { index: 5, title: i18n('xpack.ml.newJob.advanced.tabs.dataPreviewLabel', { defaultMessage: 'Data Preview' }), hidden: true }, + { index: 3, title: i18n.translate( + 'xpack.ml.newJob.advanced.tabs.datafeedLabel', { defaultMessage: 'Datafeed' }) }, + { index: 4, title: i18n.translate('xpack.ml.newJob.advanced.tabs.editJsonLabel', { defaultMessage: 'Edit JSON' }) }, + { index: 5, title: i18n.translate( + 'xpack.ml.newJob.advanced.tabs.dataPreviewLabel', { defaultMessage: 'Data Preview' }), hidden: true }, ], validation: { tabs: [ @@ -216,15 +215,15 @@ module.controller('MlNewJob', customInfluencers: [], tempCustomInfluencer: '', inputDataFormat: [ - { value: 'delimited', title: i18n('xpack.ml.newJob.advanced.delimitedLabel', { defaultMessage: 'Delimited' }) }, + { value: 'delimited', title: i18n.translate('xpack.ml.newJob.advanced.delimitedLabel', { defaultMessage: 'Delimited' }) }, { value: 'json', title: 'JSON' }, ], fieldDelimiterOptions: [ - { value: '\t', title: i18n('xpack.ml.newJob.advanced.tabLabel', { defaultMessage: 'tab' }) }, - { value: ' ', title: i18n('xpack.ml.newJob.advanced.spaceLabel', { defaultMessage: 'space' }) }, + { value: '\t', title: i18n.translate('xpack.ml.newJob.advanced.tabLabel', { defaultMessage: 'tab' }) }, + { value: ' ', title: i18n.translate('xpack.ml.newJob.advanced.spaceLabel', { defaultMessage: 'space' }) }, { value: ',', title: ',' }, { value: ';', title: ';' }, - { value: 'custom', title: i18n('xpack.ml.newJob.advanced.customLabel', { defaultMessage: 'custom' }) } + { value: 'custom', title: i18n.translate('xpack.ml.newJob.advanced.customLabel', { defaultMessage: 'custom' }) } ], selectedFieldDelimiter: ',', customFieldDelimiter: '', @@ -270,7 +269,7 @@ module.controller('MlNewJob', if (jobId) { $scope.mode = MODE.EDIT; console.log('Editing job', mlJobService.currentJob); - $scope.ui.pageTitle = i18n('xpack.ml.newJob.advanced.editingJobPageTitle', { + $scope.ui.pageTitle = i18n.translate('xpack.ml.newJob.advanced.editingJobPageTitle', { defaultMessage: 'Editing Job {jobId}', values: { jobId: $scope.job.job_id } }); @@ -288,7 +287,7 @@ module.controller('MlNewJob', } else { $scope.mode = MODE.CLONE; console.log('Cloning job', mlJobService.currentJob); - $scope.ui.pageTitle = i18n('xpack.ml.newJob.advanced.cloneJobFromPageTitle', { + $scope.ui.pageTitle = i18n.translate('xpack.ml.newJob.advanced.cloneJobFromPageTitle', { defaultMessage: 'Clone Job from {jobId}', values: { jobId: $scope.job.job_id } }); @@ -515,7 +514,7 @@ module.controller('MlNewJob', const tab = $scope.ui.validation.tabs[0]; tab.valid = false; tab.checks.jobId.valid = false; - tab.checks.jobId.message = i18n('xpack.ml.newJob.advanced.jobAlreadyExistsLabel', { + tab.checks.jobId.message = i18n.translate('xpack.ml.newJob.advanced.jobAlreadyExistsLabel', { defaultMessage: `'{jobId}' already exists, please choose a different name`, values: { jobId: $scope.job.job_id } }); @@ -533,10 +532,10 @@ module.controller('MlNewJob', } else { // if there are no influencers set, open a confirmation mlConfirm.open({ - message: i18n('xpack.ml.newJob.advanced.noInfluencersChosenConfirmModalDescription', { + message: i18n.translate('xpack.ml.newJob.advanced.noInfluencersChosenConfirmModalDescription', { defaultMessage: 'You have not chosen any influencers, do you want to continue?' }), - title: i18n('xpack.ml.newJob.advanced.noInfluencersChosenConfirmModalTitle', { + title: i18n.translate('xpack.ml.newJob.advanced.noInfluencersChosenConfirmModalTitle', { defaultMessage: 'No Influencers' }) }) @@ -590,7 +589,7 @@ module.controller('MlNewJob', // console.log('refreshed fields for index pattern .ml-anomalies-*'); // wait for mappings refresh before continuing on with the post save stuff msgs.info( - i18n('xpack.ml.newJob.advanced.newJobAddedNotificationMessage', { + i18n.translate('xpack.ml.newJob.advanced.newJobAddedNotificationMessage', { defaultMessage: `New Job '{jobId}' added`, values: { jobId: result.resp.job_id } }) @@ -606,13 +605,13 @@ module.controller('MlNewJob', }) .catch((resp) => { msgs.error( - i18n('xpack.ml.newJob.advanced.couldNotOpenJobErrorMessage', { + i18n.translate('xpack.ml.newJob.advanced.couldNotOpenJobErrorMessage', { defaultMessage: 'Could not open job:' }), resp ); msgs.error( - i18n('xpack.ml.newJob.advanced.jobCreatedAndCreatingDatafeedAnywayErrorMessage', { + i18n.translate('xpack.ml.newJob.advanced.jobCreatedAndCreatingDatafeedAnywayErrorMessage', { defaultMessage: 'Job created, creating datafeed anyway' }) ); @@ -629,7 +628,7 @@ module.controller('MlNewJob', }) .catch((resp) => { msgs.error( - i18n('xpack.ml.newJob.advanced.couldNotCreateDatafeedErrorMessage', { + i18n.translate('xpack.ml.newJob.advanced.couldNotCreateDatafeedErrorMessage', { defaultMessage: 'Could not create datafeed:' }), resp @@ -653,7 +652,7 @@ module.controller('MlNewJob', $scope.ui.saveStatus.job = -1; $scope.saveLock = false; msgs.error( - i18n('xpack.ml.newJob.advanced.unsuccessfulSavingResultErrorMessage', { + i18n.translate('xpack.ml.newJob.advanced.unsuccessfulSavingResultErrorMessage', { defaultMessage: 'Save failed: {message}', values: { message: result.resp.message } }) @@ -664,7 +663,7 @@ module.controller('MlNewJob', $scope.ui.saveStatus.job = -1; $scope.saveLock = false; msgs.error( - i18n('xpack.ml.newJob.advanced.saveFailedWithMessageErrorMessage', { + i18n.translate('xpack.ml.newJob.advanced.saveFailedWithMessageErrorMessage', { defaultMessage: 'Save failed: {message}', values: { message: result.resp.message } }) @@ -675,7 +674,7 @@ module.controller('MlNewJob', }) .catch(() => { msgs.error( - i18n('xpack.ml.newJob.advanced.saveFailedErrorMessage', { + i18n.translate('xpack.ml.newJob.advanced.saveFailedErrorMessage', { defaultMessage: 'Save failed' }) ); @@ -692,10 +691,10 @@ module.controller('MlNewJob', $scope.cancel = function () { mlConfirm.open({ - message: i18n('xpack.ml.newJob.advanced.cancelJobCreationConfirmModalDescription', { + message: i18n.translate('xpack.ml.newJob.advanced.cancelJobCreationConfirmModalDescription', { defaultMessage: 'Are you sure you want to cancel job creation?' }), - title: i18n('xpack.ml.newJob.advanced.cancelJobCreationConfirmModalTitle', { + title: i18n.translate('xpack.ml.newJob.advanced.cancelJobCreationConfirmModalTitle', { defaultMessage: 'Are you sure?' }) }) @@ -812,7 +811,7 @@ module.controller('MlNewJob', $scope.ui.cardinalityValidator.status = STATUS.FINISHED; $scope.ui.cardinalityValidator.message = ''; } else { - $scope.ui.cardinalityValidator.message = i18n( + $scope.ui.cardinalityValidator.message = i18n.translate( 'xpack.ml.newJob.advanced.recommendationForUsingModelPlotWithCardinalityDescription', { defaultMessage: 'Creating model plots is resource intensive and not recommended ' + @@ -829,7 +828,7 @@ module.controller('MlNewJob', }) .catch((error) => { console.log('Cardinality check error:', error); - $scope.ui.cardinalityValidator.message = i18n( + $scope.ui.cardinalityValidator.message = i18n.translate( 'xpack.ml.newJob.advanced.cardinalityNotValidErrorMessage', { defaultMessage: 'An error occurred validating the configuration ' + @@ -1218,7 +1217,7 @@ module.controller('MlNewJob', const validationResults = basicJobValidation($scope.job, $scope.fields, limits); const valid = validationResults.valid; - const message = i18n('xpack.ml.newJob.advanced.fillInAllrequiredFieldsValidationMessage', { + const message = i18n.translate('xpack.ml.newJob.advanced.fillInAllrequiredFieldsValidationMessage', { defaultMessage: 'Fill in all required fields' }); @@ -1243,7 +1242,7 @@ module.controller('MlNewJob', tabs[0].checks.jobId.valid = false; } else if (validationResults.contains('job_id_invalid')) { tabs[0].checks.jobId.valid = false; - const msg = i18n('xpack.ml.newJob.advanced.validateJob.jobNameAllowedCharactersDescription', { + const msg = i18n.translate('xpack.ml.newJob.advanced.validateJob.jobNameAllowedCharactersDescription', { defaultMessage: 'Job name can contain lowercase alphanumeric (a-z and 0-9), hyphens or underscores; ' + 'must start and end with an alphanumeric character' }); @@ -1252,7 +1251,7 @@ module.controller('MlNewJob', if (validationResults.contains('job_group_id_invalid')) { tabs[0].checks.groupIds.valid = false; - const msg = i18n('xpack.ml.newJob.advanced.validateJob.jobGroupNamesAllowedCharactersDescription', { + const msg = i18n.translate('xpack.ml.newJob.advanced.validateJob.jobGroupNamesAllowedCharactersDescription', { defaultMessage: 'Job group names can contain lowercase alphanumeric (a-z and 0-9), hyphens or underscores; ' + 'must start and end with an alphanumeric character' }); @@ -1261,7 +1260,7 @@ module.controller('MlNewJob', if (validationResults.contains('model_memory_limit_units_invalid')) { tabs[0].checks.modelMemoryLimit.valid = false; - const msg = i18n('xpack.ml.newJob.advanced.validateJob.modelMemoryLimitUnrecognizedUnitsErrorMessage', { + const msg = i18n.translate('xpack.ml.newJob.advanced.validateJob.modelMemoryLimitUnrecognizedUnitsErrorMessage', { defaultMessage: 'Model memory limit data unit unrecognized. It must be {allowedDataUnits} or {allowedDataUnit}', values: { allowedDataUnits: (ALLOWED_DATA_UNITS.slice(0, ALLOWED_DATA_UNITS.length - 1).join(', ')), @@ -1273,7 +1272,7 @@ module.controller('MlNewJob', if (validationResults.contains('model_memory_limit_invalid')) { tabs[0].checks.modelMemoryLimit.valid = false; - const msg = i18n('xpack.ml.newJob.advanced.validateJob.modelMemoryLimitInvalidRangeErrorMessage', { + const msg = i18n.translate('xpack.ml.newJob.advanced.validateJob.modelMemoryLimitInvalidRangeErrorMessage', { defaultMessage: 'Model memory limit cannot be higher than the maximum value of {maxModelMemoryLimit}', values: { maxModelMemoryLimit: limits.max_model_memory_limit.toUpperCase() } }); @@ -1282,10 +1281,11 @@ module.controller('MlNewJob', // tab 1 - Analysis Configuration if (validationResults.contains('categorization_filter_invalid')) { - tabs[1].checks.categorizationFilters.message = i18n('xpack.ml.newJob.advanced.validateJob.howToAllowFiltersDescription', { - defaultMessage: '{categorizationFieldName} must be set to allow filters', - values: { categorizationFieldName: 'categorizationFieldName' } - }); + tabs[1].checks.categorizationFilters.message = i18n.translate( + 'xpack.ml.newJob.advanced.validateJob.howToAllowFiltersDescription', { + defaultMessage: '{categorizationFieldName} must be set to allow filters', + values: { categorizationFieldName: 'categorizationFieldName' } + }); tabs[1].checks.categorizationFilters.valid = false; } @@ -1293,7 +1293,7 @@ module.controller('MlNewJob', tabs[1].checks.detectors.valid = false; } if (validationResults.contains('detectors_duplicates')) { - const msg = i18n('xpack.ml.newJob.advanced.validateJob.duplicateDetectorsFoundErrorMessage', { + const msg = i18n.translate('xpack.ml.newJob.advanced.validateJob.duplicateDetectorsFoundErrorMessage', { defaultMessage: 'Duplicate detectors were found. Detectors having the same combined configuration for ' + `'{function}', '{fieldName}', '{byFieldName}', '{overFieldName}' and '{partitionFieldName}' ` + 'are not allowed within the same job.', @@ -1314,13 +1314,13 @@ module.controller('MlNewJob', } if (validationResults.contains('bucket_span_empty')) { - tabs[1].checks.bucketSpan.message = i18n('xpack.ml.newJob.advanced.validateJob.bucketSpanMustBeSetErrorMessage', { + tabs[1].checks.bucketSpan.message = i18n.translate('xpack.ml.newJob.advanced.validateJob.bucketSpanMustBeSetErrorMessage', { defaultMessage: '{bucketSpan} must be set', values: { bucketSpan: 'bucket_span' } }); tabs[1].checks.bucketSpan.valid = false; } else if (validationResults.contains('bucket_span_invalid')) { - const msg = i18n('xpack.ml.newJob.advanced.validateJob.bucketSpanInvalidTimeIntervalFormatErrorMessage', { + const msg = i18n.translate('xpack.ml.newJob.advanced.validateJob.bucketSpanInvalidTimeIntervalFormatErrorMessage', { defaultMessage: '{bucketSpan} is not a valid time interval format e.g. {tenMinutes}, {oneHour}. It also needs to be higher than zero.', values: { bucketSpan: job.analysis_config.bucket_span, tenMinutes: '10m', oneHour: '1h' } @@ -1354,7 +1354,7 @@ module.controller('MlNewJob', // it can be overridden with a custom function to do an alternative test function validateIndex(tabs, dataFeedTest = () => (Object.keys($scope.fields).length === 0)) { if (dataFeedTest()) { - const msg = i18n('xpack.ml.newJob.advanced.validateJob.couldNotLoadFieldsFromIndexErrorMessage', { + const msg = i18n.translate('xpack.ml.newJob.advanced.validateJob.couldNotLoadFieldsFromIndexErrorMessage', { defaultMessage: 'Could not load fields from index' }); tabs[3].checks.hasAccessToIndex.valid = false; @@ -1414,7 +1414,7 @@ module.controller('MlNewJob', $scope.$applyAsync(); }); } else { - $scope.ui.dataPreview = i18n('xpack.ml.newJob.advanced.dataPreview.datafeedDoesNotExistLabel', { + $scope.ui.dataPreview = i18n.translate('xpack.ml.newJob.advanced.dataPreview.datafeedDoesNotExistLabel', { defaultMessage: 'Datafeed does not exist' }); $scope.$applyAsync(); diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/bucket_span_estimator/bucket_span_estimator_directive.js b/x-pack/plugins/ml/public/jobs/new_job/simple/components/bucket_span_estimator/bucket_span_estimator_directive.js index 92266ecb31c1e..7db6b39cf7447 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/components/bucket_span_estimator/bucket_span_estimator_directive.js +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/bucket_span_estimator/bucket_span_estimator_directive.js @@ -5,6 +5,7 @@ */ import React from 'react'; +import { i18n } from '@kbn/i18n'; import ReactDOM from 'react-dom'; import { BucketSpanEstimator } from './bucket_span_estimator_view'; @@ -16,7 +17,7 @@ import { I18nContext } from 'ui/i18n'; import { uiModules } from 'ui/modules'; const module = uiModules.get('apps/ml'); -module.directive('mlBucketSpanEstimator', function (i18n) { +module.directive('mlBucketSpanEstimator', function () { return { restrict: 'AE', replace: false, @@ -39,9 +40,10 @@ module.directive('mlBucketSpanEstimator', function (i18n) { const errorHandler = (error) => { console.log('Bucket span could not be estimated', error); $scope.ui.bucketSpanEstimator.status = STATUS.FAILED; - $scope.ui.bucketSpanEstimator.message = i18n('xpack.ml.newJob.simple.bucketSpanEstimator.bucketSpanCouldNotBeEstimatedMessage', { - defaultMessage: 'Bucket span could not be estimated' - }); + $scope.ui.bucketSpanEstimator.message = i18n.translate( + 'xpack.ml.newJob.simple.bucketSpanEstimator.bucketSpanCouldNotBeEstimatedMessage', { + defaultMessage: 'Bucket span could not be estimated' + }); $scope.$applyAsync(); }; @@ -122,10 +124,10 @@ module.directive('mlBucketSpanEstimator', function (i18n) { ); const estimatorRunning = ($scope.ui.bucketSpanEstimator.status === STATUS.RUNNING); const buttonText = (estimatorRunning) - ? i18n('xpack.ml.newJob.simple.bucketSpanEstimator.estimatingBucketSpanButtonLabel', { + ? i18n.translate('xpack.ml.newJob.simple.bucketSpanEstimator.estimatingBucketSpanButtonLabel', { defaultMessage: 'Estimating bucket span' }) - : i18n('xpack.ml.newJob.simple.bucketSpanEstimator.estimateBucketSpanButtonLabel', { + : i18n.translate('xpack.ml.newJob.simple.bucketSpanEstimator.estimateBucketSpanButtonLabel', { defaultMessage: 'Estimate bucket span' }); diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/enable_model_plot_checkbox/enable_model_plot_checkbox_directive.js b/x-pack/plugins/ml/public/jobs/new_job/simple/components/enable_model_plot_checkbox/enable_model_plot_checkbox_directive.js index 66bc2715a0f7e..988a5cebb082b 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/components/enable_model_plot_checkbox/enable_model_plot_checkbox_directive.js +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/enable_model_plot_checkbox/enable_model_plot_checkbox_directive.js @@ -5,6 +5,7 @@ */ import React from 'react'; +import { i18n } from '@kbn/i18n'; import ReactDOM from 'react-dom'; import { EnableModelPlotCheckbox } from './enable_model_plot_checkbox_view.js'; @@ -16,7 +17,7 @@ import { I18nContext } from 'ui/i18n'; import { uiModules } from 'ui/modules'; const module = uiModules.get('apps/ml'); -module.directive('mlEnableModelPlotCheckbox', function (i18n) { +module.directive('mlEnableModelPlotCheckbox', function () { return { restrict: 'AE', replace: false, @@ -37,12 +38,13 @@ module.directive('mlEnableModelPlotCheckbox', function (i18n) { function errorHandler(error) { console.log('Cardinality could not be validated', error); $scope.ui.cardinalityValidator.status = STATUS.FAILED; - $scope.ui.cardinalityValidator.message = i18n('xpack.ml.newJob.simple.enableModelPlot.validatingConfigurationErrorMessage', { - defaultMessage: 'An error occurred validating the configuration ' + + $scope.ui.cardinalityValidator.message = i18n.translate( + 'xpack.ml.newJob.simple.enableModelPlot.validatingConfigurationErrorMessage', { + defaultMessage: 'An error occurred validating the configuration ' + 'for running the job with model plot enabled. ' + 'Creating model plots can be resource intensive and not recommended where the cardinality of the selected fields is high. ' + 'You may want to select a dedicated results index on the Job Details tab.' - }); + }); // Go ahead and check the dedicated index box for them $scope.formConfig.useDedicatedIndex = true; } @@ -62,7 +64,7 @@ module.directive('mlEnableModelPlotCheckbox', function (i18n) { $scope.formConfig.enableModelPlot = true; $scope.ui.cardinalityValidator.status = STATUS.FINISHED; } else { - $scope.ui.cardinalityValidator.message = i18n('xpack.ml.newJob.simple.enableModelPlot.enableModelPlotDescription', { + $scope.ui.cardinalityValidator.message = i18n.translate('xpack.ml.newJob.simple.enableModelPlot.enableModelPlotDescription', { defaultMessage: 'Creating model plots is resource intensive and not recommended ' + 'where the cardinality of the selected fields is greater than 100. Estimated cardinality ' + 'for this job is {highCardinality}. ' + @@ -126,10 +128,10 @@ module.directive('mlEnableModelPlotCheckbox', function (i18n) { $scope.ui.cardinalityValidator.status === STATUS.FAILED) && $scope.ui.formValid === true); const checkboxText = (validatorRunning) - ? i18n('xpack.ml.newJob.simple.enableModelPlot.validatingCardinalityLabel', { + ? i18n.translate('xpack.ml.newJob.simple.enableModelPlot.validatingCardinalityLabel', { defaultMessage: 'Validating cardinality…' }) - : i18n('xpack.ml.newJob.simple.enableModelPlot.enableModelPlotLabel', { + : i18n.translate('xpack.ml.newJob.simple.enableModelPlot.enableModelPlotLabel', { defaultMessage: 'Enable model plot' }); diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/general_job_details/general_job_details_directive.js b/x-pack/plugins/ml/public/jobs/new_job/simple/components/general_job_details/general_job_details_directive.js index 0862f7ca1f19a..8845c7d21dc3c 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/components/general_job_details/general_job_details_directive.js +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/general_job_details/general_job_details_directive.js @@ -7,6 +7,7 @@ import template from './general_job_details.html'; +import { i18n } from '@kbn/i18n'; import { changeJobIDCase } from './change_job_id_case'; import { uiModules } from 'ui/modules'; @@ -17,16 +18,16 @@ module.directive('mlGeneralJobDetails', function () { restrict: 'E', replace: true, template, - controller: function ($scope, i18n) { + controller: function ($scope) { // force job ids to be lowercase $scope.changeJobIDCase = changeJobIDCase; - $scope.hideAdvancedButtonAriaLabel = i18n('xpack.ml.newJob.simple.generalJobDetails.hideAdvancedButtonAriaLabel', { + $scope.hideAdvancedButtonAriaLabel = i18n.translate('xpack.ml.newJob.simple.generalJobDetails.hideAdvancedButtonAriaLabel', { defaultMessage: 'Hide Advanced' }); - $scope.showAdvancedButtonAriaLabel = i18n('xpack.ml.newJob.simple.generalJobDetails.showAdvancedButtonAriaLabel', { + $scope.showAdvancedButtonAriaLabel = i18n.translate('xpack.ml.newJob.simple.generalJobDetails.showAdvancedButtonAriaLabel', { defaultMessage: 'Show Advanced' }); - $scope.enterNameForJobLabel = i18n('xpack.ml.newJob.simple.generalJobDetails.enterNameForJobLabel', { + $scope.enterNameForJobLabel = i18n.translate('xpack.ml.newJob.simple.generalJobDetails.enterNameForJobLabel', { defaultMessage: 'Enter a name for the job' }); } diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/post_save_options/post_save_options_directive.js b/x-pack/plugins/ml/public/jobs/new_job/simple/components/post_save_options/post_save_options_directive.js index 636ee25086bf4..a8ee15790ca7a 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/components/post_save_options/post_save_options_directive.js +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/post_save_options/post_save_options_directive.js @@ -7,6 +7,7 @@ import { postSaveService } from './post_save_service'; +import { i18n } from '@kbn/i18n'; import { mlCreateWatchService } from 'plugins/ml/jobs/new_job/simple/components/watcher/create_watch_service'; import { xpackFeatureProvider } from 'plugins/ml/license/check_license'; import template from './post_save_options.html'; @@ -14,7 +15,7 @@ import template from './post_save_options.html'; import { uiModules } from 'ui/modules'; const module = uiModules.get('apps/ml'); -module.directive('mlPostSaveOptions', function (Private, i18n) { +module.directive('mlPostSaveOptions', function (Private) { return { restrict: 'AE', replace: false, diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/post_save_options/post_save_service.js b/x-pack/plugins/ml/public/jobs/new_job/simple/components/post_save_options/post_save_service.js index 9844548bfc615..a38a3cd689309 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/components/post_save_options/post_save_service.js +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/post_save_options/post_save_service.js @@ -8,6 +8,7 @@ import { mlJobService } from 'plugins/ml/services/job_service'; +import { i18n } from '@kbn/i18n'; import { mlCreateWatchService } from 'plugins/ml/jobs/new_job/simple/components/watcher/create_watch_service'; import { mlMessageBarService } from 'plugins/ml/components/messagebar/messagebar_service'; @@ -30,7 +31,7 @@ class PostSaveService { this.externalCreateWatch; } - startRealtimeJob(jobId, i18n) { + startRealtimeJob(jobId) { return new Promise((resolve, reject) => { this.status.realtimeJob = this.STATUS.SAVING; @@ -45,7 +46,7 @@ class PostSaveService { resolve(); }).catch((resp) => { msgs.error( - i18n('xpack.ml.newJob.simple.postSaveOptions.couldNotStartDatafeedErrorMessage', { + i18n.translate('xpack.ml.newJob.simple.postSaveOptions.couldNotStartDatafeedErrorMessage', { defaultMessage: 'Could not start datafeed:' }), resp); this.status.realtimeJob = this.STATUS.SAVE_FAILED; @@ -56,7 +57,7 @@ class PostSaveService { }); } - apply(jobId, runInRealtime, createWatch, i18n) { + apply(jobId, runInRealtime, createWatch) { return new Promise((resolve) => { if (runInRealtime) { this.startRealtimeJob(jobId, i18n) diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/utils/chart_data_utils.js b/x-pack/plugins/ml/public/jobs/new_job/simple/components/utils/chart_data_utils.js index ee73c87783ecd..24aedb95acaa6 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/components/utils/chart_data_utils.js +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/utils/chart_data_utils.js @@ -9,14 +9,13 @@ // various util functions for populating the chartData object used by the job wizards import _ from 'lodash'; -import { IntervalHelperProvider } from 'plugins/ml/util/ml_time_buckets'; +import { MlTimeBuckets } from 'plugins/ml/util/ml_time_buckets'; import { calculateTextWidth } from 'plugins/ml/util/string_utils'; import { mlResultsService } from 'plugins/ml/services/results_service'; import { mlSimpleJobSearchService } from 'plugins/ml/jobs/new_job/simple/components/utils/search_service'; import { timefilter } from 'ui/timefilter'; -export function ChartDataUtilsProvider(Private) { - const TimeBuckets = Private(IntervalHelperProvider); +export function ChartDataUtilsProvider() { function loadDocCountData(formConfig, chartData) { return new Promise((resolve, reject) => { @@ -25,7 +24,7 @@ export function ChartDataUtilsProvider(Private) { const MAX_BARS = BAR_TARGET + (BAR_TARGET / 100) * 100; // 100% larger that bar target const query = formConfig.combinedQuery; const bounds = timefilter.getActiveBounds(); - const buckets = new TimeBuckets(); + const buckets = new MlTimeBuckets(); buckets.setBarTarget(BAR_TARGET); buckets.setMaxBars(MAX_BARS); buckets.setInterval('auto'); 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 26b9710fbd4ad..536b0b3b75045 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/simple/multi_metric/create_job/create_job_controller.js b/x-pack/plugins/ml/public/jobs/new_job/simple/multi_metric/create_job/create_job_controller.js index e4e456d470834..e0df234a5139c 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/multi_metric/create_job/create_job_controller.js +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/multi_metric/create_job/create_job_controller.js @@ -7,6 +7,7 @@ import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; import 'ui/angular_ui_select'; import { aggTypes } from 'ui/agg_types'; @@ -19,7 +20,7 @@ import angular from 'angular'; import uiRoutes from 'ui/routes'; import { checkLicenseExpired } from 'plugins/ml/license/check_license'; import { checkCreateJobsPrivilege } from 'plugins/ml/privilege/check_privilege'; -import { IntervalHelperProvider } from 'plugins/ml/util/ml_time_buckets'; +import { MlTimeBuckets } from 'plugins/ml/util/ml_time_buckets'; import { getCreateMultiMetricJobBreadcrumbs } from 'plugins/ml/jobs/breadcrumbs'; import { filterAggTypes } from 'plugins/ml/jobs/new_job/simple/components/utils/filter_agg_types'; import { validateJob } from 'plugins/ml/jobs/new_job/simple/components/utils/validate_job'; @@ -62,17 +63,11 @@ import { uiModules } from 'ui/modules'; const module = uiModules.get('apps/ml'); module - .controller('MlCreateMultiMetricJob', function ( - $scope, - $timeout, - Private, - AppState, - i18n) { + .controller('MlCreateMultiMetricJob', function ($scope, $timeout, Private, AppState) { timefilter.enableTimeRangeSelector(); timefilter.disableAutoRefreshSelector(); const msgs = mlMessageBarService; - const MlTimeBuckets = Private(IntervalHelperProvider); const moveToAdvancedJobCreation = Private(moveToAdvancedJobCreationProvider); const chartDataUtils = Private(ChartDataUtilsProvider); const mlMultiMetricJobService = Private(MultiMetricJobServiceProvider); @@ -117,19 +112,19 @@ module timeBasedIndexCheck(indexPattern, true); const pageTitle = (savedSearch.id !== undefined) ? - i18n('xpack.ml.newJob.simple.multiMetric.savedSearchPageTitle', { + i18n.translate('xpack.ml.newJob.simple.multiMetric.savedSearchPageTitle', { defaultMessage: 'saved search {savedSearchTitle}', values: { savedSearchTitle: savedSearch.title } }) : - i18n('xpack.ml.newJob.simple.multiMetric.indexPatternPageTitle', { + i18n.translate('xpack.ml.newJob.simple.multiMetric.indexPatternPageTitle', { defaultMessage: 'index pattern {indexPatternTitle}', values: { indexPatternTitle: indexPattern.title } }); - $scope.analysisStoppingLabel = i18n('xpack.ml.newJob.simple.multiMetric.analysisStoppingLabel', { + $scope.analysisStoppingLabel = i18n.translate('xpack.ml.newJob.simple.multiMetric.analysisStoppingLabel', { defaultMessage: 'Analysis stopping' }); - $scope.stopAnalysisLabel = i18n('xpack.ml.newJob.simple.multiMetric.stopAnalysisLabel', { + $scope.stopAnalysisLabel = i18n.translate('xpack.ml.newJob.simple.multiMetric.stopAnalysisLabel', { defaultMessage: 'Stop analysis' }); @@ -149,52 +144,52 @@ module timeFields: [], splitText: '', intervals: [{ - title: i18n('xpack.ml.newJob.simple.multiMetric.intervals.autoTitle', { + title: i18n.translate('xpack.ml.newJob.simple.multiMetric.intervals.autoTitle', { defaultMessage: 'Auto' }), value: 'auto', }, { - title: i18n('xpack.ml.newJob.simple.multiMetric.intervals.millisecondTitle', { + title: i18n.translate('xpack.ml.newJob.simple.multiMetric.intervals.millisecondTitle', { defaultMessage: 'Millisecond' }), value: 'ms' }, { - title: i18n('xpack.ml.newJob.simple.multiMetric.intervals.secondTitle', { + title: i18n.translate('xpack.ml.newJob.simple.multiMetric.intervals.secondTitle', { defaultMessage: 'Second' }), value: 's' }, { - title: i18n('xpack.ml.newJob.simple.multiMetric.intervals.minuteTitle', { + title: i18n.translate('xpack.ml.newJob.simple.multiMetric.intervals.minuteTitle', { defaultMessage: 'Minute' }), value: 'm' }, { - title: i18n('xpack.ml.newJob.simple.multiMetric.intervals.hourlyTitle', { + title: i18n.translate('xpack.ml.newJob.simple.multiMetric.intervals.hourlyTitle', { defaultMessage: 'Hourly' }), value: 'h' }, { - title: i18n('xpack.ml.newJob.simple.multiMetric.intervals.dailyTitle', { + title: i18n.translate('xpack.ml.newJob.simple.multiMetric.intervals.dailyTitle', { defaultMessage: 'Daily' }), value: 'd' }, { - title: i18n('xpack.ml.newJob.simple.multiMetric.intervals.weeklyTitle', { + title: i18n.translate('xpack.ml.newJob.simple.multiMetric.intervals.weeklyTitle', { defaultMessage: 'Weekly' }), value: 'w' }, { - title: i18n('xpack.ml.newJob.simple.multiMetric.intervals.monthlyTitle', { + title: i18n.translate('xpack.ml.newJob.simple.multiMetric.intervals.monthlyTitle', { defaultMessage: 'Monthly' }), value: 'M' }, { - title: i18n('xpack.ml.newJob.simple.multiMetric.intervals.yearlyTitle', { + title: i18n.translate('xpack.ml.newJob.simple.multiMetric.intervals.yearlyTitle', { defaultMessage: 'Yearly' }), value: 'y' }, { - title: i18n('xpack.ml.newJob.simple.multiMetric.intervals.customTitle', { + title: i18n.translate('xpack.ml.newJob.simple.multiMetric.intervals.customTitle', { defaultMessage: 'Custom' }), value: 'custom' @@ -253,7 +248,7 @@ module if (splitField !== undefined) { $scope.addDefaultFieldsToInfluencerList(); - $scope.ui.splitText = i18n('xpack.ml.newJob.simple.multiMetric.dataSplitByLabel', { + $scope.ui.splitText = i18n.translate('xpack.ml.newJob.simple.multiMetric.dataSplitByLabel', { defaultMessage: 'Data split by {splitFieldName}', values: { splitFieldName: splitField.name } }); @@ -492,13 +487,13 @@ module }) .catch((resp) => { msgs.error( - i18n('xpack.ml.newJob.simple.multiMetric.couldNotOpenJobErrorMessage', { + i18n.translate('xpack.ml.newJob.simple.multiMetric.couldNotOpenJobErrorMessage', { defaultMessage: 'Could not open job:' }), resp ); msgs.error( - i18n('xpack.ml.newJob.simple.multiMetric.jobCreatedAndDatafeedCreatingAnywayErrorMessage', { + i18n.translate('xpack.ml.newJob.simple.multiMetric.jobCreatedAndDatafeedCreatingAnywayErrorMessage', { defaultMessage: 'Job created, creating datafeed anyway' }) ); @@ -510,7 +505,7 @@ module .catch((resp) => { // save failed msgs.error( - i18n('xpack.ml.newJob.simple.multiMetric.saveFailedErrorMessage', { + i18n.translate('xpack.ml.newJob.simple.multiMetric.saveFailedErrorMessage', { defaultMessage: 'Save failed:' }), resp.resp @@ -558,7 +553,7 @@ module .catch((resp) => { // datafeed failed msgs.error( - i18n('xpack.ml.newJob.simple.multiMetric.couldNotStartDatafeedErrorMessage', { + i18n.translate('xpack.ml.newJob.simple.multiMetric.couldNotStartDatafeedErrorMessage', { defaultMessage: 'Could not start datafeed:' }), resp @@ -573,7 +568,7 @@ module }) .catch((resp) => { msgs.error( - i18n('xpack.ml.newJob.simple.multiMetric.saveDatafeedFailedErrorMessage', { + i18n.translate('xpack.ml.newJob.simple.multiMetric.saveDatafeedFailedErrorMessage', { defaultMessage: 'Save datafeed failed:', }), resp diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/population/create_job/create_job_chart_directive.js b/x-pack/plugins/ml/public/jobs/new_job/simple/population/create_job/create_job_chart_directive.js index 13cd21daa6bba..67345d9b385d0 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/population/create_job/create_job_chart_directive.js +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/population/create_job/create_job_chart_directive.js @@ -11,6 +11,7 @@ */ import $ from 'jquery'; +import { i18n } from '@kbn/i18n'; import d3 from 'd3'; import angular from 'angular'; import moment from 'moment'; @@ -24,7 +25,7 @@ import { mlChartTooltipService } from '../../../../../components/chart_tooltip/c import { uiModules } from 'ui/modules'; const module = uiModules.get('apps/ml'); -module.directive('mlPopulationJobChart', function (i18n) { +module.directive('mlPopulationJobChart', function () { function link(scope, element) { @@ -233,7 +234,7 @@ module.directive('mlPopulationJobChart', function (i18n) { const formattedDate = formatHumanReadableDateTime(data.date); contents += `${formattedDate}

`; contents += `${mlEscape(scope.overFieldName)}: ${mlEscape(data.label)}
`; - contents += i18n('xpack.ml.newJob.simple.population.chartTooltipValueLabel', { + contents += i18n.translate('xpack.ml.newJob.simple.population.chartTooltipValueLabel', { defaultMessage: 'Value: {dataValue}', values: { dataValue: scope.chartData.fieldFormat !== undefined diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/population/create_job/create_job_controller.js b/x-pack/plugins/ml/public/jobs/new_job/simple/population/create_job/create_job_controller.js index da383559d0d40..b3235ee6b374e 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/population/create_job/create_job_controller.js +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/population/create_job/create_job_controller.js @@ -7,6 +7,7 @@ import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; import 'ui/angular_ui_select'; import { aggTypes } from 'ui/agg_types/index'; @@ -19,7 +20,7 @@ import angular from 'angular'; import uiRoutes from 'ui/routes'; import { checkLicenseExpired } from 'plugins/ml/license/check_license'; import { checkCreateJobsPrivilege } from 'plugins/ml/privilege/check_privilege'; -import { IntervalHelperProvider } from 'plugins/ml/util/ml_time_buckets'; +import { MlTimeBuckets } from 'plugins/ml/util/ml_time_buckets'; import { getCreatePopulationJobBreadcrumbs } from 'plugins/ml/jobs/breadcrumbs'; import { filterAggTypes } from 'plugins/ml/jobs/new_job/simple/components/utils/filter_agg_types'; import { validateJob } from 'plugins/ml/jobs/new_job/simple/components/utils/validate_job'; @@ -61,17 +62,11 @@ import { uiModules } from 'ui/modules'; const module = uiModules.get('apps/ml'); module - .controller('MlCreatePopulationJob', function ( - $scope, - $timeout, - Private, - AppState, - i18n) { + .controller('MlCreatePopulationJob', function ($scope, $timeout, Private, AppState) { timefilter.enableTimeRangeSelector(); timefilter.disableAutoRefreshSelector(); const msgs = mlMessageBarService; - const MlTimeBuckets = Private(IntervalHelperProvider); const moveToAdvancedJobCreation = Private(moveToAdvancedJobCreationProvider); const chartDataUtils = Private(ChartDataUtilsProvider); const mlPopulationJobService = Private(PopulationJobServiceProvider); @@ -117,19 +112,19 @@ module timeBasedIndexCheck(indexPattern, true); const pageTitle = (savedSearch.id !== undefined) ? - i18n('xpack.ml.newJob.simple.population.savedSearchPageTitle', { + i18n.translate('xpack.ml.newJob.simple.population.savedSearchPageTitle', { defaultMessage: 'saved search {savedSearchTitle}', values: { savedSearchTitle: savedSearch.title } }) : - i18n('xpack.ml.newJob.simple.population.indexPatternPageTitle', { + i18n.translate('xpack.ml.newJob.simple.population.indexPatternPageTitle', { defaultMessage: 'index pattern {indexPatternTitle}', values: { indexPatternTitle: indexPattern.title } }); - $scope.analysisStoppingLabel = i18n('xpack.ml.newJob.simple.population.analysisStoppingLabel', { + $scope.analysisStoppingLabel = i18n.translate('xpack.ml.newJob.simple.population.analysisStoppingLabel', { defaultMessage: 'Analysis stopping' }); - $scope.stopAnalysisLabel = i18n('xpack.ml.newJob.simple.population.stopAnalysisLabel', { + $scope.stopAnalysisLabel = i18n.translate('xpack.ml.newJob.simple.population.stopAnalysisLabel', { defaultMessage: 'Stop analysis' }); @@ -150,52 +145,52 @@ module timeFields: [], splitText: '', intervals: [{ - title: i18n('xpack.ml.newJob.simple.population.intervals.autoTitle', { + title: i18n.translate('xpack.ml.newJob.simple.population.intervals.autoTitle', { defaultMessage: 'Auto' }), value: 'auto', }, { - title: i18n('xpack.ml.newJob.simple.population.intervals.millisecondTitle', { + title: i18n.translate('xpack.ml.newJob.simple.population.intervals.millisecondTitle', { defaultMessage: 'Millisecond' }), value: 'ms' }, { - title: i18n('xpack.ml.newJob.simple.population.intervals.secondTitle', { + title: i18n.translate('xpack.ml.newJob.simple.population.intervals.secondTitle', { defaultMessage: 'Second' }), value: 's' }, { - title: i18n('xpack.ml.newJob.simple.population.intervals.minuteTitle', { + title: i18n.translate('xpack.ml.newJob.simple.population.intervals.minuteTitle', { defaultMessage: 'Minute' }), value: 'm' }, { - title: i18n('xpack.ml.newJob.simple.population.intervals.hourlyTitle', { + title: i18n.translate('xpack.ml.newJob.simple.population.intervals.hourlyTitle', { defaultMessage: 'Hourly' }), value: 'h' }, { - title: i18n('xpack.ml.newJob.simple.population.intervals.dailyTitle', { + title: i18n.translate('xpack.ml.newJob.simple.population.intervals.dailyTitle', { defaultMessage: 'Daily' }), value: 'd' }, { - title: i18n('xpack.ml.newJob.simple.population.intervals.weeklyTitle', { + title: i18n.translate('xpack.ml.newJob.simple.population.intervals.weeklyTitle', { defaultMessage: 'Weekly' }), value: 'w' }, { - title: i18n('xpack.ml.newJob.simple.population.intervals.monthlyTitle', { + title: i18n.translate('xpack.ml.newJob.simple.population.intervals.monthlyTitle', { defaultMessage: 'Monthly' }), value: 'M' }, { - title: i18n('xpack.ml.newJob.simple.population.intervals.yearlyTitle', { + title: i18n.translate('xpack.ml.newJob.simple.population.intervals.yearlyTitle', { defaultMessage: 'Yearly' }), value: 'y' }, { - title: i18n('xpack.ml.newJob.simple.population.intervals.customTitle', { + title: i18n.translate('xpack.ml.newJob.simple.population.intervals.customTitle', { defaultMessage: 'Custom' }), value: 'custom' @@ -515,13 +510,13 @@ module }) .catch((resp) => { msgs.error( - i18n('xpack.ml.newJob.simple.population.couldNotOpenJobErrorMessage', { + i18n.translate('xpack.ml.newJob.simple.population.couldNotOpenJobErrorMessage', { defaultMessage: 'Could not open job:', }), resp ); msgs.error( - i18n('xpack.ml.newJob.simple.population.jobCreatedAndDatafeedCreatingAnywayErrorMessage', { + i18n.translate('xpack.ml.newJob.simple.population.jobCreatedAndDatafeedCreatingAnywayErrorMessage', { defaultMessage: 'Job created, creating datafeed anyway' }) ); @@ -533,7 +528,7 @@ module .catch((resp) => { // save failed msgs.error( - i18n('xpack.ml.newJob.simple.population.saveFailedErrorMessage', { + i18n.translate('xpack.ml.newJob.simple.population.saveFailedErrorMessage', { defaultMessage: 'Save failed:', }), resp.resp @@ -581,7 +576,7 @@ module .catch((resp) => { // datafeed failed msgs.error( - i18n('xpack.ml.newJob.simple.population.couldNotStartDatafeedErrorMessage', { + i18n.translate('xpack.ml.newJob.simple.population.couldNotStartDatafeedErrorMessage', { defaultMessage: 'Could not start datafeed:' }), resp @@ -596,7 +591,7 @@ module }) .catch((resp) => { msgs.error( - i18n('xpack.ml.newJob.simple.population.saveDatafeedFailedErrorMessage', { + i18n.translate('xpack.ml.newJob.simple.population.saveDatafeedFailedErrorMessage', { defaultMessage: 'Save datafeed failed:', }), resp diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/population/create_job/create_job_service.js b/x-pack/plugins/ml/public/jobs/new_job/simple/population/create_job/create_job_service.js index 9ffc5ea8da65f..f4d1f8a2d462a 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/population/create_job/create_job_service.js +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/population/create_job/create_job_service.js @@ -10,16 +10,15 @@ import _ from 'lodash'; import { EVENT_RATE_COUNT_FIELD, WIZARD_TYPE } from 'plugins/ml/jobs/new_job/simple/components/constants/general'; import { ML_MEDIAN_PERCENTS } from 'plugins/ml/../common/util/job_utils'; -import { IntervalHelperProvider } from 'plugins/ml/util/ml_time_buckets'; +import { MlTimeBuckets } from 'plugins/ml/util/ml_time_buckets'; import { mlFieldFormatService } from 'plugins/ml/services/field_format_service'; import { mlJobService } from 'plugins/ml/services/job_service'; import { createJobForSaving } from 'plugins/ml/jobs/new_job/utils/new_job_utils'; import { ml } from 'plugins/ml/services/ml_api_service'; import { timefilter } from 'ui/timefilter'; -export function PopulationJobServiceProvider(Private) { +export function PopulationJobServiceProvider() { - const TimeBuckets = Private(IntervalHelperProvider); const OVER_FIELD_EXAMPLES_COUNT = 40; class PopulationJobService { @@ -77,7 +76,7 @@ export function PopulationJobServiceProvider(Private) { }; }); - const searchJson = getSearchJsonFromConfig(formConfig, timefilter, TimeBuckets); + const searchJson = getSearchJsonFromConfig(formConfig, timefilter, MlTimeBuckets); ml.esSearch(searchJson) .then((resp) => { @@ -314,7 +313,7 @@ export function PopulationJobServiceProvider(Private) { function getSearchJsonFromConfig(formConfig) { const bounds = timefilter.getActiveBounds(); - const buckets = new TimeBuckets(); + const buckets = new MlTimeBuckets(); buckets.setInterval('auto'); buckets.setBounds(bounds); diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/recognize/check_module.js b/x-pack/plugins/ml/public/jobs/new_job/simple/recognize/check_module.js new file mode 100644 index 0000000000000..6e3846ba02063 --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/recognize/check_module.js @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import chrome from 'ui/chrome'; +import { i18n } from '@kbn/i18n'; +import { mlJobService } from '../../../../services/job_service'; +import { ml } from '../../../../services/ml_api_service'; +import { toastNotifications } from 'ui/notify'; + + +// Checks whether the jobs in a data recognizer module have been created. +// Redirects to the Anomaly Explorer to view the jobs if they have been created, +// or the recognizer job wizard for the module if not. +export function checkViewOrCreateJobs(Private, $route, kbnBaseUrl, kbnUrl) { + + return new Promise((resolve, reject) => { + const moduleId = $route.current.params.id; + const indexPatternId = $route.current.params.index; + + // Load the module, and check if the job(s) in the module have been created. + // If so, load the jobs in the Anomaly Explorer. + // Otherwise open the data recognizer wizard for the module. + // Always want to call reject() so as not to load original page. + ml.dataRecognizerModuleJobsExist({ moduleId }) + .then((resp) => { + const basePath = `${chrome.getBasePath()}/app/`; + + if (resp.jobsExist === true) { + const resultsPageUrl = mlJobService.createResultsUrlForJobs(resp.jobs, 'explorer'); + window.location.href = `${basePath}${resultsPageUrl}`; + reject(); + } else { + window.location.href = `${basePath}ml#/jobs/new_job/simple/recognize?id=${moduleId}&index=${indexPatternId}`; + reject(); + } + + }) + .catch((err) => { + console.log(`Error checking whether jobs in module ${moduleId} exists`, err); + toastNotifications.addWarning({ + title: i18n.translate('xpack.ml.newJob.simple.recognize.moduleCheckJobsExistWarningTitle', { + defaultMessage: 'Error checking module {moduleId}', + values: { moduleId } + }), + text: i18n.translate('xpack.ml.newJob.simple.recognize.moduleCheckJobsExistWarningDescription', { + defaultMessage: 'An error occurred trying to check whether the jobs in the module have been created.', + }) + }); + + + kbnUrl.redirect(`/jobs`); + reject(); + }); + }); +} diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/recognize/create_job/create_job_controller.js b/x-pack/plugins/ml/public/jobs/new_job/simple/recognize/create_job/create_job_controller.js index 6ab0b02c6aced..317b9cff49897 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/recognize/create_job/create_job_controller.js +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/recognize/create_job/create_job_controller.js @@ -7,6 +7,7 @@ import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; import angular from 'angular'; import 'ui/angular_ui_select'; import dateMath from '@elastic/datemath'; @@ -16,6 +17,7 @@ import { SearchItemsProvider, addNewJobToRecentlyAccessed } from 'plugins/ml/job import uiRoutes from 'ui/routes'; +import { checkViewOrCreateJobs } from '../check_module'; import { checkLicenseExpired } from 'plugins/ml/license/check_license'; import { checkCreateJobsPrivilege } from 'plugins/ml/privilege/check_privilege'; import { loadCurrentIndexPattern, loadCurrentSavedSearch } from 'plugins/ml/util/index_utils'; @@ -41,16 +43,20 @@ uiRoutes } }); +uiRoutes + .when('/modules/check_view_or_create', { + template, + resolve: { + checkViewOrCreateJobs + } + }); + + import { uiModules } from 'ui/modules'; const module = uiModules.get('apps/ml'); module - .controller('MlCreateRecognizerJobs', function ( - $scope, - $window, - $route, - Private, - i18n) { + .controller('MlCreateRecognizerJobs', function ($scope, $route, Private) { const mlCreateRecognizerJobsService = Private(CreateRecognizerJobsServiceProvider); timefilter.disableTimeRangeSelector(); @@ -91,24 +97,24 @@ module combinedQuery } = createSearchItems(); const pageTitle = (savedSearch.id !== undefined) ? - i18n('xpack.ml.newJob.simple.recognize.savedSearchPageTitle', { + i18n.translate('xpack.ml.newJob.simple.recognize.savedSearchPageTitle', { defaultMessage: 'saved search {savedSearchTitle}', values: { savedSearchTitle: savedSearch.title } }) : - i18n('xpack.ml.newJob.simple.recognize.indexPatternPageTitle', { + i18n.translate('xpack.ml.newJob.simple.recognize.indexPatternPageTitle', { defaultMessage: 'index pattern {indexPatternTitle}', values: { indexPatternTitle: indexPattern.title } }); $scope.displayQueryWarning = (savedSearch.id !== undefined); - $scope.hideAdvancedButtonAriaLabel = i18n('xpack.ml.newJob.simple.recognize.hideAdvancedButtonAriaLabel', { + $scope.hideAdvancedButtonAriaLabel = i18n.translate('xpack.ml.newJob.simple.recognize.hideAdvancedButtonAriaLabel', { defaultMessage: 'Hide Advanced' }); - $scope.showAdvancedButtonAriaLabel = i18n('xpack.ml.newJob.simple.recognize.showAdvancedButtonAriaLabel', { + $scope.showAdvancedButtonAriaLabel = i18n.translate('xpack.ml.newJob.simple.recognize.showAdvancedButtonAriaLabel', { defaultMessage: 'Show Advanced' }); - $scope.showAdvancedAriaLabel = i18n('xpack.ml.newJob.simple.recognize.showAdvancedAriaLabel', { + $scope.showAdvancedAriaLabel = i18n.translate('xpack.ml.newJob.simple.recognize.showAdvancedAriaLabel', { defaultMessage: 'Show advanced' }); @@ -119,13 +125,13 @@ module showJobInput: true, numberOfJobs: 0, kibanaLabels: { - dashboard: i18n('xpack.ml.newJob.simple.recognize.dashboardsLabel', { + dashboard: i18n.translate('xpack.ml.newJob.simple.recognize.dashboardsLabel', { defaultMessage: 'Dashboards' }), - search: i18n('xpack.ml.newJob.simple.recognize.searchesLabel', { + search: i18n.translate('xpack.ml.newJob.simple.recognize.searchesLabel', { defaultMessage: 'Searches' }), - visualization: i18n('xpack.ml.newJob.simple.recognize.visualizationsLabel', { + visualization: i18n.translate('xpack.ml.newJob.simple.recognize.visualizationsLabel', { defaultMessage: 'Visualizations' }), }, @@ -299,7 +305,7 @@ module } else { job.jobState = SAVE_STATE.FAILED; job.errors.push( - i18n('xpack.ml.newJob.simple.recognize.job.couldNotSaveJobErrorMessage', { + i18n.translate('xpack.ml.newJob.simple.recognize.job.couldNotSaveJobErrorMessage', { defaultMessage: 'Could not save job {jobId}', values: { jobId } }) @@ -321,7 +327,7 @@ module } else { job.datafeedState = SAVE_STATE.FAILED; job.errors.push( - i18n('xpack.ml.newJob.simple.recognize.datafeed.couldNotSaveDatafeedErrorMessage', { + i18n.translate('xpack.ml.newJob.simple.recognize.datafeed.couldNotSaveDatafeedErrorMessage', { defaultMessage: 'Could not save datafeed {datafeedId}', values: { datafeedId } }) @@ -348,7 +354,7 @@ module } else { obj.saveState = SAVE_STATE.FAILED; obj.errors.push( - i18n('xpack.ml.newJob.simple.recognize.kibanaObject.couldNotSaveErrorMessage', { + i18n.translate('xpack.ml.newJob.simple.recognize.kibanaObject.couldNotSaveErrorMessage', { defaultMessage: 'Could not save {objName} {objId}', values: { objName, objId: obj.id } }) @@ -363,11 +369,11 @@ module .catch((err) => { console.log('Error setting up module', err); toastNotifications.addWarning({ - title: i18n('xpack.ml.newJob.simple.recognize.moduleSetupFailedWarningTitle', { + title: i18n.translate('xpack.ml.newJob.simple.recognize.moduleSetupFailedWarningTitle', { defaultMessage: 'Error setting up module {moduleId}', values: { moduleId } }), - text: i18n('xpack.ml.newJob.simple.recognize.moduleSetupFailedWarningDescription', { + text: i18n.translate('xpack.ml.newJob.simple.recognize.moduleSetupFailedWarningDescription', { defaultMessage: 'An error occurred trying to create the {count, plural, one {job} other {jobs}} in the module.', values: { count: $scope.formConfig.jobs.length @@ -564,7 +570,7 @@ module if (isJobIdValid(label) === false) { valid = false; checks.jobLabel.valid = false; - const msg = i18n('xpack.ml.newJob.simple.recognize.jobLabelAllowedCharactersDescription', { + const msg = i18n.translate('xpack.ml.newJob.simple.recognize.jobLabelAllowedCharactersDescription', { defaultMessage: 'Job label can contain lowercase alphanumeric (a-z and 0-9), hyphens or underscores; ' + 'must start and end with an alphanumeric character' }); @@ -574,7 +580,7 @@ module if (isJobIdValid(group) === false) { valid = false; checks.groupIds.valid = false; - const msg = i18n('xpack.ml.newJob.simple.recognize.jobGroupAllowedCharactersDescription', { + const msg = i18n.translate('xpack.ml.newJob.simple.recognize.jobGroupAllowedCharactersDescription', { defaultMessage: 'Job group names can contain lowercase alphanumeric (a-z and 0-9), hyphens or underscores; ' + 'must start and end with an alphanumeric character' }); diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/single_metric/create_job/create_job_controller.js b/x-pack/plugins/ml/public/jobs/new_job/simple/single_metric/create_job/create_job_controller.js index e24c58698110a..de4a0a2e6cdb7 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/single_metric/create_job/create_job_controller.js +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/single_metric/create_job/create_job_controller.js @@ -7,6 +7,7 @@ import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; import 'ui/angular_ui_select'; import { aggTypes } from 'ui/agg_types'; @@ -20,7 +21,7 @@ import uiRoutes from 'ui/routes'; import { getSafeAggregationName } from 'plugins/ml/../common/util/job_utils'; import { checkLicenseExpired } from 'plugins/ml/license/check_license'; import { checkCreateJobsPrivilege } from 'plugins/ml/privilege/check_privilege'; -import { IntervalHelperProvider } from 'plugins/ml/util/ml_time_buckets'; +import { MlTimeBuckets } from 'plugins/ml/util/ml_time_buckets'; import { getCreateSingleMetricJobBreadcrumbs } from 'plugins/ml/jobs/breadcrumbs'; import { filterAggTypes } from 'plugins/ml/jobs/new_job/simple/components/utils/filter_agg_types'; import { validateJob } from 'plugins/ml/jobs/new_job/simple/components/utils/validate_job'; @@ -63,19 +64,11 @@ import { uiModules } from 'ui/modules'; const module = uiModules.get('apps/ml'); module - .controller('MlCreateSingleMetricJob', function ( - $scope, - $route, - $filter, - $timeout, - Private, - AppState, - i18n) { + .controller('MlCreateSingleMetricJob', function ($scope, $route, $timeout, Private, AppState) { timefilter.enableTimeRangeSelector(); timefilter.disableAutoRefreshSelector(); const msgs = mlMessageBarService; - const MlTimeBuckets = Private(IntervalHelperProvider); const moveToAdvancedJobCreation = Private(moveToAdvancedJobCreationProvider); const mlSingleMetricJobService = Private(SingleMetricJobServiceProvider); @@ -122,25 +115,25 @@ module timeBasedIndexCheck(indexPattern, true); - $scope.indexPatternLinkText = i18n('xpack.ml.newJob.simple.singleMetric.noResultsFound.indexPatternLinkText', { + $scope.indexPatternLinkText = i18n.translate('xpack.ml.newJob.simple.singleMetric.noResultsFound.indexPatternLinkText', { defaultMessage: 'full {indexPatternTitle} data', values: { indexPatternTitle: indexPattern.title } }); - $scope.nameNotValidMessage = i18n('xpack.ml.newJob.simple.singleMetric.nameNotValidMessage', { + $scope.nameNotValidMessage = i18n.translate('xpack.ml.newJob.simple.singleMetric.nameNotValidMessage', { defaultMessage: 'Enter a name for the job' }); - $scope.showAdvancedButtonAriaLabel = i18n('xpack.ml.newJob.simple.singleMetric.showAdvancedButtonAriaLabel', { + $scope.showAdvancedButtonAriaLabel = i18n.translate('xpack.ml.newJob.simple.singleMetric.showAdvancedButtonAriaLabel', { defaultMessage: 'Show Advanced' }); - $scope.hideAdvancedButtonAriaLabel = i18n('xpack.ml.newJob.simple.singleMetric.hideAdvancedButtonAriaLabel', { + $scope.hideAdvancedButtonAriaLabel = i18n.translate('xpack.ml.newJob.simple.singleMetric.hideAdvancedButtonAriaLabel', { defaultMessage: 'Hide Advanced' }); const pageTitle = (savedSearch.id !== undefined) ? - i18n('xpack.ml.newJob.simple.singleMetric.savedSearchPageTitle', { + i18n.translate('xpack.ml.newJob.simple.singleMetric.savedSearchPageTitle', { defaultMessage: 'saved search {savedSearchTitle}', values: { savedSearchTitle: savedSearch.title } }) - : i18n('xpack.ml.newJob.simple.singleMetric.indexPatternPageTitle', { + : i18n.translate('xpack.ml.newJob.simple.singleMetric.indexPatternPageTitle', { defaultMessage: 'index pattern {indexPatternTitle}', values: { indexPatternTitle: indexPattern.title } }); @@ -158,7 +151,7 @@ module fields: [], timeFields: [], intervals: [{ - title: i18n('xpack.ml.newJob.simple.singleMetric.autoIntervalUnitTitle', { + title: i18n.translate('xpack.ml.newJob.simple.singleMetric.autoIntervalUnitTitle', { defaultMessage: 'Auto' }), value: 'auto', @@ -168,47 +161,47 @@ module return agg.fieldIsTimeField(); }*/ }, { - title: i18n('xpack.ml.newJob.simple.singleMetric.millisecondIntervalUnitTitle', { + title: i18n.translate('xpack.ml.newJob.simple.singleMetric.millisecondIntervalUnitTitle', { defaultMessage: 'Millisecond' }), value: 'ms' }, { - title: i18n('xpack.ml.newJob.simple.singleMetric.secondIntervalUnitTitle', { + title: i18n.translate('xpack.ml.newJob.simple.singleMetric.secondIntervalUnitTitle', { defaultMessage: 'Second' }), value: 's' }, { - title: i18n('xpack.ml.newJob.simple.singleMetric.minuteIntervalUnitTitle', { + title: i18n.translate('xpack.ml.newJob.simple.singleMetric.minuteIntervalUnitTitle', { defaultMessage: 'Minute' }), value: 'm' }, { - title: i18n('xpack.ml.newJob.simple.singleMetric.hourlyIntervalUnitTitle', { + title: i18n.translate('xpack.ml.newJob.simple.singleMetric.hourlyIntervalUnitTitle', { defaultMessage: 'Hourly' }), value: 'h' }, { - title: i18n('xpack.ml.newJob.simple.singleMetric.dailyIntervalUnitTitle', { + title: i18n.translate('xpack.ml.newJob.simple.singleMetric.dailyIntervalUnitTitle', { defaultMessage: 'Daily' }), value: 'd' }, { - title: i18n('xpack.ml.newJob.simple.singleMetric.weeklyIntervalUnitTitle', { + title: i18n.translate('xpack.ml.newJob.simple.singleMetric.weeklyIntervalUnitTitle', { defaultMessage: 'Weekly' }), value: 'w' }, { - title: i18n('xpack.ml.newJob.simple.singleMetric.monthlyIntervalUnitTitle', { + title: i18n.translate('xpack.ml.newJob.simple.singleMetric.monthlyIntervalUnitTitle', { defaultMessage: 'Monthly' }), value: 'M' }, { - title: i18n('xpack.ml.newJob.simple.singleMetric.yearlyIntervalUnitTitle', { + title: i18n.translate('xpack.ml.newJob.simple.singleMetric.yearlyIntervalUnitTitle', { defaultMessage: 'Yearly' }), value: 'y' }, { - title: i18n('xpack.ml.newJob.simple.singleMetric.customIntervalUnitTitle', { + title: i18n.translate('xpack.ml.newJob.simple.singleMetric.customIntervalUnitTitle', { defaultMessage: 'Custom' }), value: 'custom' @@ -395,10 +388,10 @@ module saveNewDatafeed(job, true); }) .catch((resp) => { - msgs.error(i18n('xpack.ml.newJob.simple.singleMetric.openJobErrorMessage', { + msgs.error(i18n.translate('xpack.ml.newJob.simple.singleMetric.openJobErrorMessage', { defaultMessage: 'Could not open job: ' }), resp); - msgs.error(i18n('xpack.ml.newJob.simple.singleMetric.creatingDatafeedErrorMessage', { + msgs.error(i18n.translate('xpack.ml.newJob.simple.singleMetric.creatingDatafeedErrorMessage', { defaultMessage: 'Job created, creating datafeed anyway' })); // if open failed, still attempt to create the datafeed @@ -409,7 +402,7 @@ module }) .catch((resp) => { // save failed - msgs.error(i18n('xpack.ml.newJob.simple.singleMetric.saveFailedErrorMessage', { + msgs.error(i18n.translate('xpack.ml.newJob.simple.singleMetric.saveFailedErrorMessage', { defaultMessage: 'Save failed: ' }), resp.resp); $scope.$applyAsync(); @@ -455,7 +448,7 @@ module }) .catch((resp) => { // datafeed failed - msgs.error(i18n('xpack.ml.newJob.simple.singleMetric.datafeedNotStartedErrorMessage', { + msgs.error(i18n.translate('xpack.ml.newJob.simple.singleMetric.datafeedNotStartedErrorMessage', { defaultMessage: 'Could not start datafeed: ' }), resp); }) @@ -467,7 +460,7 @@ module } }) .catch((resp) => { - msgs.error(i18n('xpack.ml.newJob.simple.singleMetric.saveDatafeedFailedErrorMessage', { + msgs.error(i18n.translate('xpack.ml.newJob.simple.singleMetric.saveDatafeedFailedErrorMessage', { defaultMessage: 'Save datafeed failed: ' }), resp); $scope.$applyAsync(); diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/single_metric/create_job/create_job_service.js b/x-pack/plugins/ml/public/jobs/new_job/simple/single_metric/create_job/create_job_service.js index 0dd5fa567c8b3..457fad65e2a32 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/single_metric/create_job/create_job_service.js +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/single_metric/create_job/create_job_service.js @@ -214,7 +214,7 @@ export function SingleMetricJobServiceProvider() { buckets: { date_histogram: { field: formConfig.timeField, - interval: interval + fixed_interval: `${interval}ms` }, aggregations: { [formConfig.timeField]: { @@ -237,7 +237,7 @@ export function SingleMetricJobServiceProvider() { buckets: { date_histogram: { field: formConfig.timeField, - interval: ((interval / 100) * 10) // use 10% of bucketSpan to allow for better sampling + fixed_interval: `${((interval / 100) * 10)}ms` // use 10% of bucketSpan to allow for better sampling }, aggregations: { [dtr.field_name]: { @@ -261,7 +261,7 @@ export function SingleMetricJobServiceProvider() { buckets: { date_histogram: { field: formConfig.timeField, - interval: interval + fixed_interval: `${interval}ms` }, aggregations: { [formConfig.timeField]: { diff --git a/x-pack/plugins/ml/public/jobs/new_job/utils/new_job_utils.js b/x-pack/plugins/ml/public/jobs/new_job/utils/new_job_utils.js index d1a71feff8999..4319713e463ec 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/utils/new_job_utils.js +++ b/x-pack/plugins/ml/public/jobs/new_job/utils/new_job_utils.js @@ -15,7 +15,7 @@ import { mlJobService } from 'plugins/ml/services/job_service'; // Provider for creating the items used for searching and job creation. // Uses the $route object to retrieve the indexPattern and savedSearch from the url -export function SearchItemsProvider(Private, $route, config) { +export function SearchItemsProvider($route, config) { function createSearchItems() { let indexPattern = $route.current.locals.indexPattern; 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 d4bd8c488b32f..bc688c07cab2d 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/ml/public/jobs/new_job/wizard/steps/job_type/job_type_controller.js b/x-pack/plugins/ml/public/jobs/new_job/wizard/steps/job_type/job_type_controller.js index 970107e474b8c..df7768ee9f0c8 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/wizard/steps/job_type/job_type_controller.js +++ b/x-pack/plugins/ml/public/jobs/new_job/wizard/steps/job_type/job_type_controller.js @@ -12,6 +12,7 @@ */ import uiRoutes from 'ui/routes'; +import { i18n } from '@kbn/i18n'; import { checkLicenseExpired } from 'plugins/ml/license/check_license'; import { checkCreateJobsPrivilege } from 'plugins/ml/privilege/check_privilege'; import { getCreateJobBreadcrumbs } from 'plugins/ml/jobs/breadcrumbs'; @@ -40,10 +41,7 @@ import { uiModules } from 'ui/modules'; const module = uiModules.get('apps/ml'); module.controller('MlNewJobStepJobType', - function ( - $scope, - Private, - i18n) { + function ($scope, Private) { timefilter.disableTimeRangeSelector(); // remove time picker from top of page timefilter.disableAutoRefreshSelector(); // remove time picker from top of page @@ -59,11 +57,11 @@ module.controller('MlNewJobStepJobType', $scope.isTimeBasedIndex = timeBasedIndexCheck(indexPattern); if ($scope.isTimeBasedIndex === false) { $scope.indexWarningTitle = (savedSearch.id === undefined) ? - i18n('xpack.ml.newJob.wizard.jobType.indexPatternNotTimeBasedMessage', { + i18n.translate('xpack.ml.newJob.wizard.jobType.indexPatternNotTimeBasedMessage', { defaultMessage: 'Index pattern {indexPatternTitle} is not time based', values: { indexPatternTitle: indexPattern.title } }) - : i18n('xpack.ml.newJob.wizard.jobType.indexPatternFromSavedSearchNotTimeBasedMessage', { + : i18n.translate('xpack.ml.newJob.wizard.jobType.indexPatternFromSavedSearchNotTimeBasedMessage', { defaultMessage: '{savedSearchTitle} uses index pattern {indexPatternTitle} which is not time based', values: { savedSearchTitle: savedSearch.title, @@ -82,11 +80,11 @@ module.controller('MlNewJobStepJobType', }; $scope.pageTitleLabel = (savedSearch.id !== undefined) ? - i18n('xpack.ml.newJob.wizard.jobType.savedSearchPageTitleLabel', { + i18n.translate('xpack.ml.newJob.wizard.jobType.savedSearchPageTitleLabel', { defaultMessage: 'saved search {savedSearchTitle}', values: { savedSearchTitle: savedSearch.title } }) - : i18n('xpack.ml.newJob.wizard.jobType.indexPatternPageTitleLabel', { + : i18n.translate('xpack.ml.newJob.wizard.jobType.indexPatternPageTitleLabel', { defaultMessage: 'index pattern {indexPatternTitle}', values: { indexPatternTitle: indexPattern.title } }); diff --git a/x-pack/plugins/ml/public/lib/angular_bootstrap_patch.js b/x-pack/plugins/ml/public/lib/angular_bootstrap_patch.js deleted file mode 100644 index a13b151c26557..0000000000000 --- a/x-pack/plugins/ml/public/lib/angular_bootstrap_patch.js +++ /dev/null @@ -1,82 +0,0 @@ -/* eslint-disable @kbn/eslint/require-license-header */ - -/** - * @notice - * - * This product includes code that was extracted from angular-ui-bootstrap@0.13.1 - * which is available under an "MIT" license - * - * The MIT License - * - * Copyright (c) 2012-2016 the AngularUI Team, http://angular-ui.github.io/bootstrap/ - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -// This file contains a section of code taken from angular-ui-bootstrap@0.13.1 -// and adds it to kibana's included version of 0.12.1 -// It adds the ability to allow html to be used as the content of the popover component - -import 'ui/angular-bootstrap'; - -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml'); - -module - .directive('popover', [ '$tooltip', function ($tooltip) { - return $tooltip('popover', 'popover', 'click'); - }]) - .directive('popoverHtmlUnsafePopup', function ($compile) { - let template = '
'; - template += '
'; - template += '
'; - template += '

'; - template += '
'; - template += '
'; - return { - restrict: 'EA', - replace: true, - scope: { - title: '@', - content: '@', - placement: '@', - animation: '&', - isOpen: '&' - }, - template: template, - link: function (scope, element) { - // The content of the popup is added as a string and does not run through angular's templating system. - // therefore {{stuff}} substitutions don't happen. - // we have to manually apply the template, compile it with this scope and then set it as the html - scope.$apply(); - const cont = $compile(scope.content)(scope); - element.find('.popover-content').html(cont); - - // function to force the popover to close - scope.closePopover = function () { - scope.$parent.$parent.isOpen = false; - scope.$parent.$parent.$applyAsync(); - element.remove(); - }; - } - }; - }) - .directive('popoverHtmlUnsafe', ['$tooltip', function ($tooltip) { - return $tooltip('popoverHtmlUnsafe', 'popover', 'click'); - }]); diff --git a/x-pack/plugins/ml/public/privilege/check_privilege.ts b/x-pack/plugins/ml/public/privilege/check_privilege.ts index be46aebbeb90e..00a0d8dbeff16 100644 --- a/x-pack/plugins/ml/public/privilege/check_privilege.ts +++ b/x-pack/plugins/ml/public/privilege/check_privilege.ts @@ -71,7 +71,7 @@ export function checkGetDataFrameJobsPrivilege(kbnUrl: any): Promise if (privileges.canGetDataFrameJobs) { return resolve(privileges); } else { - kbnUrl.redirect('/access-denied'); + kbnUrl.redirect('/data_frames/access-denied'); return reject(); } }); diff --git a/x-pack/plugins/ml/public/register_feature.js b/x-pack/plugins/ml/public/register_feature.js index dfb8e4c75198f..7a5956e8192c0 100644 --- a/x-pack/plugins/ml/public/register_feature.js +++ b/x-pack/plugins/ml/public/register_feature.js @@ -8,13 +8,15 @@ import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue'; -FeatureCatalogueRegistryProvider.register(i18n => { +import { i18n } from '@kbn/i18n'; + +FeatureCatalogueRegistryProvider.register(() => { return { id: 'ml', - title: i18n('xpack.ml.machineLearningTitle', { + title: i18n.translate('xpack.ml.machineLearningTitle', { defaultMessage: 'Machine Learning' }), - description: i18n('xpack.ml.machineLearningDescription', { + description: i18n.translate('xpack.ml.machineLearningDescription', { defaultMessage: 'Automatically model the normal behavior of your time series data to detect anomalies.' }), icon: 'machineLearningApp', diff --git a/x-pack/plugins/ml/public/services/job_service.js b/x-pack/plugins/ml/public/services/job_service.js index c2c6953cd56b0..09f3a8b262e1b 100644 --- a/x-pack/plugins/ml/public/services/job_service.js +++ b/x-pack/plugins/ml/public/services/job_service.js @@ -733,9 +733,15 @@ class JobService { return groups; } + createResultsUrlForJobs(jobsList, resultsPage) { + return createResultsUrlForJobs(jobsList, resultsPage); + } + createResultsUrl(jobIds, from, to, resultsPage) { return createResultsUrl(jobIds, from, to, resultsPage); } + + } // private function used to check the job saving response @@ -878,6 +884,29 @@ function createJobUrls(jobsList, jobUrls) { }); } +function createResultsUrlForJobs(jobsList, resultsPage) { + let from = undefined; + let to = undefined; + if (jobsList.length === 1) { + from = jobsList[0].earliestTimestampMs; + to = jobsList[0].latestTimestampMs; + } else { + const jobsWithData = jobsList.filter(j => (j.earliestTimestampMs !== undefined)); + if (jobsWithData.length > 0) { + from = Math.min(...jobsWithData.map(j => j.earliestTimestampMs)); + to = Math.max(...jobsWithData.map(j => j.latestTimestampMs)); + } + } + + const timeFormat = 'YYYY-MM-DD HH:mm:ss'; + + const fromString = moment(from).format(timeFormat); // Defaults to 'now' if 'from' is undefined + const toString = moment(to).format(timeFormat); // Defaults to 'now' if 'to' is undefined + + const jobIds = jobsList.map(j => j.id); + return createResultsUrl(jobIds, fromString, toString, resultsPage); +} + function createResultsUrl(jobIds, start, end, resultsPage) { const idString = jobIds.map(j => `'${j}'`).join(','); const from = moment(start).toISOString(); diff --git a/x-pack/plugins/ml/public/services/ml_api_service/data_frame.js b/x-pack/plugins/ml/public/services/ml_api_service/data_frame.js index 30008e5152dfc..c2bb802e1eb6d 100644 --- a/x-pack/plugins/ml/public/services/ml_api_service/data_frame.js +++ b/x-pack/plugins/ml/public/services/ml_api_service/data_frame.js @@ -19,7 +19,14 @@ export const dataFrame = { method: 'GET' }); }, - getDataFrameTransformsStats() { + getDataFrameTransformsStats(jobId) { + if (jobId !== undefined) { + return http({ + url: `${basePath}/_data_frame/transforms/${jobId}/_stats`, + method: 'GET' + }); + } + return http({ url: `${basePath}/_data_frame/transforms/_stats`, method: 'GET' @@ -53,7 +60,7 @@ export const dataFrame = { }, stopDataFrameTransformsJob(jobId) { return http({ - url: `${basePath}/_data_frame/transforms/${jobId}/_stop`, + url: `${basePath}/_data_frame/transforms/${jobId}/_stop?force=true`, method: 'POST', }); }, diff --git a/x-pack/plugins/ml/public/services/ml_api_service/index.d.ts b/x-pack/plugins/ml/public/services/ml_api_service/index.d.ts index 6f229099856be..45185481c878c 100644 --- a/x-pack/plugins/ml/public/services/ml_api_service/index.d.ts +++ b/x-pack/plugins/ml/public/services/ml_api_service/index.d.ts @@ -22,7 +22,7 @@ declare interface Ml { dataFrame: { getDataFrameTransforms(): Promise; - getDataFrameTransformsStats(): Promise; + getDataFrameTransformsStats(jobId?: string): Promise; createDataFrameTransformsJob(jobId: string, jobConfig: any): Promise; deleteDataFrameTransformsJob(jobId: string): Promise; getDataFrameTransformsPreview(payload: any): Promise; diff --git a/x-pack/plugins/ml/public/services/ml_api_service/index.js b/x-pack/plugins/ml/public/services/ml_api_service/index.js index 662e4bb998448..b93d2b585a6de 100644 --- a/x-pack/plugins/ml/public/services/ml_api_service/index.js +++ b/x-pack/plugins/ml/public/services/ml_api_service/index.js @@ -262,6 +262,13 @@ export const ml = { }); }, + dataRecognizerModuleJobsExist(obj) { + return http({ + url: `${basePath}/modules/jobs_exist/${obj.moduleId}`, + method: 'GET' + }); + }, + setupDataRecognizerConfig(obj) { const data = pick(obj, [ 'prefix', diff --git a/x-pack/plugins/ml/public/services/ml_api_service/jobs.js b/x-pack/plugins/ml/public/services/ml_api_service/jobs.js index 594e0b066a2fd..25962620b9af2 100644 --- a/x-pack/plugins/ml/public/services/ml_api_service/jobs.js +++ b/x-pack/plugins/ml/public/services/ml_api_service/jobs.js @@ -117,4 +117,14 @@ export const jobs = { }); }, + jobsExist(jobIds) { + return http({ + url: `${basePath}/jobs/jobs_exist`, + method: 'POST', + data: { + jobIds, + } + }); + }, + }; diff --git a/x-pack/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js b/x-pack/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js index 33de5c111d7db..747400ba33b62 100644 --- a/x-pack/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js +++ b/x-pack/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js @@ -35,8 +35,11 @@ import { mlJobService } from 'plugins/ml/services/job_service'; import { mlForecastService } from 'plugins/ml/services/forecast_service'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +export const FORECAST_DURATION_MAX_DAYS = 3650; // Max forecast duration allowed by analytics. + const FORECAST_JOB_MIN_VERSION = '6.1.0'; // Forecasting only allowed for jobs created >= 6.1.0. const FORECASTS_VIEW_MAX = 5; // Display links to a maximum of 5 forecasts. +const FORECAST_DURATION_MAX_MS = FORECAST_DURATION_MAX_DAYS * 86400000; const WARN_NUM_PARTITIONS = 100; // Warn about running a forecast with this number of field values. const FORECAST_STATS_POLL_FREQUENCY = 250; // Frequency in ms at which to poll for forecast request stats. const WARN_NO_PROGRESS_MS = 120000; // If no progress in forecast request, abort check and warn. @@ -102,6 +105,14 @@ export const ForecastingModal = injectI18n(class ForecastingModal extends Compon defaultMessage: 'Invalid duration format', }) ); + } else if (duration.asMilliseconds() > FORECAST_DURATION_MAX_MS) { + isNewForecastDurationValid = false; + newForecastDurationErrors.push( + intl.formatMessage({ + id: 'xpack.ml.timeSeriesExplorer.forecastingModal.forecastDurationMustNotBeGreaterThanMaximumErrorMessage', + defaultMessage: 'Forecast duration must not be greater than {maximumForecastDurationDays} days', + }, { maximumForecastDurationDays: FORECAST_DURATION_MAX_DAYS }) + ); } else if (duration.asMilliseconds() === 0) { isNewForecastDurationValid = false; newForecastDurationErrors.push( @@ -163,19 +174,40 @@ export const ForecastingModal = injectI18n(class ForecastingModal extends Compon }); }; - runForecastErrorHandler = (resp) => { + runForecastErrorHandler = (resp, closeJob) => { + const intl = this.props.intl; + this.setState({ forecastProgress: PROGRESS_STATES.ERROR }); console.log('Time series forecast modal - error running forecast:', resp); if (resp && resp.message) { this.addMessage(resp.message, MESSAGE_LEVEL.ERROR, true); } else { this.addMessage( - this.props.intl.formatMessage({ + intl.formatMessage({ id: 'xpack.ml.timeSeriesExplorer.forecastingModal.unexpectedResponseFromRunningForecastErrorMessage', defaultMessage: 'Unexpected response from running forecast. The request may have failed.', }), MESSAGE_LEVEL.ERROR, true); } + + if (closeJob === true) { + this.setState({ jobClosingState: PROGRESS_STATES.WAITING }); + mlJobService.closeJob(this.props.job.job_id) + .then(() => { + this.setState({ jobClosingState: PROGRESS_STATES.DONE }); + }) + .catch((response) => { + console.log('Time series forecast modal - could not close job:', response); + this.addMessage( + intl.formatMessage({ + id: 'xpack.ml.timeSeriesExplorer.forecastingModal.errorWithClosingJobErrorMessage', + defaultMessage: 'Error closing job', + }), + MESSAGE_LEVEL.ERROR + ); + this.setState({ jobClosingState: PROGRESS_STATES.ERROR }); + }); + } }; runForecast = (closeJobAfterRunning) => { @@ -194,10 +226,10 @@ export const ForecastingModal = injectI18n(class ForecastingModal extends Compon if (resp.forecast_id !== undefined) { this.waitForForecastResults(resp.forecast_id, closeJobAfterRunning); } else { - this.runForecastErrorHandler(resp); + this.runForecastErrorHandler(resp, closeJobAfterRunning); } }) - .catch(this.runForecastErrorHandler); + .catch(resp => this.runForecastErrorHandler(resp, closeJobAfterRunning)); }; waitForForecastResults = (forecastId, closeJobAfterRunning) => { diff --git a/x-pack/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/run_controls.js b/x-pack/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/run_controls.js index 385c5f040d83c..8b421407c5de2 100644 --- a/x-pack/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/run_controls.js +++ b/x-pack/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/run_controls.js @@ -29,6 +29,7 @@ import { // don't use something like plugins/ml/../common // because it won't work with the jest tests import { JOB_STATE } from '../../../../common/constants/states'; +import { FORECAST_DURATION_MAX_DAYS } from './forecasting_modal'; import { ForecastProgress } from './forecast_progress'; import { mlNodesAvailable } from 'plugins/ml/ml_nodes_check/check_ml_nodes'; import { checkPermission, createPermissionFailureMessage } from 'plugins/ml/privilege/check_privilege'; @@ -138,8 +139,9 @@ export function RunControls({ error={newForecastDurationErrors} helpText={} > {disabledState.isDisabledToolTipText === undefined ? durationInput diff --git a/x-pack/plugins/ml/public/timeseriesexplorer/timeseries_search_service.js b/x-pack/plugins/ml/public/timeseriesexplorer/timeseries_search_service.js index 50bb1f48086ab..d92a0143b7672 100644 --- a/x-pack/plugins/ml/public/timeseriesexplorer/timeseries_search_service.js +++ b/x-pack/plugins/ml/public/timeseriesexplorer/timeseries_search_service.js @@ -128,6 +128,8 @@ function getChartDetails(job, detectorIndex, entityFields, earliestMs, latestMs) }) .then((results) => { _.each(blankEntityFields, (field) => { + // results will not contain keys for non-aggregatable fields, + // so store as 0 to indicate over all field values. obj.results.entityData.entities.push({ fieldName: field.fieldName, cardinality: _.get(results, field.fieldName, 0) diff --git a/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.html b/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.html index 440c1231b83d6..c9822a06d5cf6 100644 --- a/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.html +++ b/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.html @@ -118,10 +118,11 @@ 0) { - let warningText = i18n('xpack.ml.timeSeriesExplorer.canNotViewRequestedJobsWarningMessage', { + let warningText = i18n.translate('xpack.ml.timeSeriesExplorer.canNotViewRequestedJobsWarningMessage', { defaultMessage: `You can't view requested {invalidIdsCount, plural, one {job} other {jobs}} {invalidIds} in this dashboard`, values: { invalidIdsCount: invalidIds.length, @@ -159,7 +165,7 @@ module.controller('MlTimeSeriesExplorerController', function ( } }); if (selectedJobIds.length === 0 && timeSeriesJobIds.length > 0) { - warningText += i18n('xpack.ml.timeSeriesExplorer.autoSelectingFirstJobText', { + warningText += i18n.translate('xpack.ml.timeSeriesExplorer.autoSelectingFirstJobText', { defaultMessage: ', auto selecting first job' }); } @@ -171,7 +177,7 @@ module.controller('MlTimeSeriesExplorerController', function ( if (selectedJobIds.length > 1) { // if more than one job, select the first job from the selection. toastNotifications.addWarning( - i18n('xpack.ml.timeSeriesExplorer.youCanViewOneJobAtTimeWarningMessage', { + i18n.translate('xpack.ml.timeSeriesExplorer.youCanViewOneJobAtTimeWarningMessage', { defaultMessage: 'You can only view one job at a time in this dashboard' }) ); @@ -183,7 +189,7 @@ module.controller('MlTimeSeriesExplorerController', function ( if (selectedJobIds.length > 0) { // if the group contains valid jobs, select the first toastNotifications.addWarning( - i18n('xpack.ml.timeSeriesExplorer.youCanViewOneJobAtTimeWarningMessage', { + i18n.translate('xpack.ml.timeSeriesExplorer.youCanViewOneJobAtTimeWarningMessage', { defaultMessage: 'You can only view one job at a time in this dashboard' }) ); @@ -775,7 +781,7 @@ module.controller('MlTimeSeriesExplorerController', function ( const appStateDtrIdx = $scope.appState.mlTimeSeriesExplorer.detectorIndex; let detectorIndex = appStateDtrIdx !== undefined ? appStateDtrIdx : +(viewableDetectors[0].index); if (_.find(viewableDetectors, { 'index': '' + detectorIndex }) === undefined) { - const warningText = i18n('xpack.ml.timeSeriesExplorer.requestedDetectorIndexNotValidWarningMessage', { + const warningText = i18n.translate('xpack.ml.timeSeriesExplorer.requestedDetectorIndexNotValidWarningMessage', { defaultMessage: 'Requested detector index {detectorIndex} is not valid for job {jobId}', values: { detectorIndex, @@ -986,7 +992,7 @@ module.controller('MlTimeSeriesExplorerController', function ( const barTarget = (bucketsTarget !== undefined ? bucketsTarget : 100); // Use a maxBars of 10% greater than the target. const maxBars = Math.floor(1.1 * barTarget); - const buckets = new TimeBuckets(); + const buckets = new MlTimeBuckets(); buckets.setInterval('auto'); buckets.setBounds(bounds); buckets.setBarTarget(Math.floor(barTarget)); @@ -1023,7 +1029,7 @@ module.controller('MlTimeSeriesExplorerController', function ( // Use a maxBars of 10% greater than the target. const maxBars = Math.floor(1.1 * CHARTS_POINT_TARGET); - const buckets = new TimeBuckets(); + const buckets = new MlTimeBuckets(); buckets.setInterval('auto'); buckets.setBarTarget(Math.floor(CHARTS_POINT_TARGET)); buckets.setMaxBars(maxBars); diff --git a/x-pack/plugins/ml/public/util/__tests__/ml_time_buckets.js b/x-pack/plugins/ml/public/util/__tests__/ml_time_buckets.js index c0ac9487a1ff0..a9b7e775b6704 100644 --- a/x-pack/plugins/ml/public/util/__tests__/ml_time_buckets.js +++ b/x-pack/plugins/ml/public/util/__tests__/ml_time_buckets.js @@ -10,26 +10,23 @@ import ngMock from 'ng_mock'; import expect from '@kbn/expect'; import moment from 'moment'; import { - IntervalHelperProvider, + MlTimeBuckets, getBoundsRoundedToInterval, calcEsInterval } from '../ml_time_buckets'; describe('ML - time buckets', () => { - let TimeBuckets; let autoBuckets; let customBuckets; beforeEach(() => { ngMock.module('kibana'); - ngMock.inject((Private) => { - // Create the TimeBuckets interval providers for use in the tests. - TimeBuckets = Private(IntervalHelperProvider); + ngMock.inject(() => { - autoBuckets = new TimeBuckets(); + autoBuckets = new MlTimeBuckets(); autoBuckets.setInterval('auto'); - customBuckets = new TimeBuckets(); + customBuckets = new MlTimeBuckets(); customBuckets.setInterval('auto'); customBuckets.setBarTarget(500); customBuckets.setMaxBars(550); diff --git a/x-pack/plugins/ml/public/util/ml_calc_auto_interval.js b/x-pack/plugins/ml/public/util/ml_calc_auto_interval.js index 871be1ea367ac..b78b6d3456566 100644 --- a/x-pack/plugins/ml/public/util/ml_calc_auto_interval.js +++ b/x-pack/plugins/ml/public/util/ml_calc_auto_interval.js @@ -19,7 +19,7 @@ import moment from 'moment'; const { duration: d } = moment; -export function TimeBucketsCalcAutoIntervalProvider() { +export function timeBucketsCalcAutoIntervalProvider() { // Note there is a current issue with Kibana (Kibana issue #9184) // which means we can't round to, for example, 2 week or 3 week buckets, diff --git a/x-pack/plugins/ml/public/util/ml_time_buckets.js b/x-pack/plugins/ml/public/util/ml_time_buckets.js index 74e9b7386ae72..c79a28d2b449e 100644 --- a/x-pack/plugins/ml/public/util/ml_time_buckets.js +++ b/x-pack/plugins/ml/public/util/ml_time_buckets.js @@ -14,125 +14,123 @@ import _ from 'lodash'; import moment from 'moment'; import dateMath from '@elastic/datemath'; +import chrome from 'ui/chrome'; -import { TimeBucketsCalcAutoIntervalProvider } from 'plugins/ml/util/ml_calc_auto_interval'; +import { timeBucketsCalcAutoIntervalProvider } from 'plugins/ml/util/ml_calc_auto_interval'; import { inherits } from 'plugins/ml/util/inherits'; const unitsDesc = dateMath.unitsDesc; const largeMax = unitsDesc.indexOf('w'); // Multiple units of week or longer converted to days for ES intervals. import { TimeBuckets } from 'ui/time_buckets'; -export function IntervalHelperProvider(Private, config) { - const calcAuto = Private(TimeBucketsCalcAutoIntervalProvider); - inherits(MlTimeBuckets, TimeBuckets); +const config = chrome.getUiSettingsClient(); - function MlTimeBuckets() { - this.barTarget = config.get('histogram:barTarget'); - this.maxBars = config.get('histogram:maxBars'); +const calcAuto = timeBucketsCalcAutoIntervalProvider(); +inherits(MlTimeBuckets, TimeBuckets); - // return MlTimeBuckets.Super.call(this); - } - - MlTimeBuckets.prototype.setBarTarget = function (bt) { - this.barTarget = bt; - }; - - MlTimeBuckets.prototype.setMaxBars = function (mb) { - this.maxBars = mb; - }; - - MlTimeBuckets.prototype.getInterval = function () { - const self = this; - const duration = self.getDuration(); - return decorateInterval(maybeScaleInterval(readInterval()), duration); +export function MlTimeBuckets() { + this.barTarget = config.get('histogram:barTarget'); + this.maxBars = config.get('histogram:maxBars'); - // either pull the interval from state or calculate the auto-interval - function readInterval() { - const interval = self._i; - if (moment.isDuration(interval)) return interval; - return calcAuto.near(self.barTarget, duration); - } + // return MlTimeBuckets.Super.call(this); +} - // check to see if the interval should be scaled, and scale it if so - function maybeScaleInterval(interval) { - if (!self.hasBounds()) return interval; +MlTimeBuckets.prototype.setBarTarget = function (bt) { + this.barTarget = bt; +}; - const maxLength = self.maxBars; - const approxLen = duration / interval; - let scaled; +MlTimeBuckets.prototype.setMaxBars = function (mb) { + this.maxBars = mb; +}; - // If the number of buckets we got back from using the barTarget is less than - // maxBars, than use the lessThan rule to try and get closer to maxBars. - if (approxLen > maxLength) { - scaled = calcAuto.lessThan(maxLength, duration); - } else { - return interval; - } +MlTimeBuckets.prototype.getInterval = function () { + const self = this; + const duration = self.getDuration(); + return decorateInterval(maybeScaleInterval(readInterval()), duration); - if (+scaled === +interval) return interval; + // either pull the interval from state or calculate the auto-interval + function readInterval() { + const interval = self._i; + if (moment.isDuration(interval)) return interval; + return calcAuto.near(self.barTarget, duration); + } - decorateInterval(interval, duration); - return _.assign(scaled, { - preScaled: interval, - scale: interval / scaled, - scaled: true - }); - } + // check to see if the interval should be scaled, and scale it if so + function maybeScaleInterval(interval) { + if (!self.hasBounds()) return interval; - }; + const maxLength = self.maxBars; + const approxLen = duration / interval; + let scaled; - // Returns an interval which in the last step of calculation is rounded to - // the closest multiple of the supplied divisor (in seconds). - MlTimeBuckets.prototype.getIntervalToNearestMultiple = function (divisorSecs) { - const interval = this.getInterval(); - const intervalSecs = interval.asSeconds(); - - const remainder = intervalSecs % divisorSecs; - if (remainder === 0) { + // If the number of buckets we got back from using the barTarget is less than + // maxBars, than use the lessThan rule to try and get closer to maxBars. + if (approxLen > maxLength) { + scaled = calcAuto.lessThan(maxLength, duration); + } else { return interval; } - // Create a new interval which is a multiple of the supplied divisor (not zero). - let nearestMultiple = remainder > (divisorSecs / 2) ? - intervalSecs + divisorSecs - remainder : intervalSecs - remainder; - nearestMultiple = nearestMultiple === 0 ? divisorSecs : nearestMultiple; - const nearestMultipleInt = moment.duration(nearestMultiple, 'seconds'); - decorateInterval(nearestMultipleInt, this.getDuration()); - - // Check to see if the new interval is scaled compared to the original. - const preScaled = _.get(interval, 'preScaled'); - if (preScaled !== undefined && preScaled < nearestMultipleInt) { - nearestMultipleInt.preScaled = preScaled; - nearestMultipleInt.scale = preScaled / nearestMultipleInt; - nearestMultipleInt.scaled = true; - } + if (+scaled === +interval) return interval; - return nearestMultipleInt; - }; + decorateInterval(interval, duration); + return _.assign(scaled, { + preScaled: interval, + scale: interval / scaled, + scaled: true + }); + } - // Appends some MlTimeBuckets specific properties to the momentjs duration interval. - // Uses the originalDuration from which the time bucket was created to calculate the overflow - // property (i.e. difference between the supplied duration and the calculated bucket interval). - function decorateInterval(interval, originalDuration) { - const esInterval = calcEsInterval(interval); - interval.esValue = esInterval.value; - interval.esUnit = esInterval.unit; - interval.expression = esInterval.expression; - interval.overflow = originalDuration > interval ? moment.duration(interval - originalDuration) : false; - - const prettyUnits = moment.normalizeUnits(esInterval.unit); - if (esInterval.value === 1) { - interval.description = prettyUnits; - } else { - interval.description = `${esInterval.value} ${prettyUnits}s`; - } +}; + +// Returns an interval which in the last step of calculation is rounded to +// the closest multiple of the supplied divisor (in seconds). +MlTimeBuckets.prototype.getIntervalToNearestMultiple = function (divisorSecs) { + const interval = this.getInterval(); + const intervalSecs = interval.asSeconds(); + const remainder = intervalSecs % divisorSecs; + if (remainder === 0) { return interval; } + // Create a new interval which is a multiple of the supplied divisor (not zero). + let nearestMultiple = remainder > (divisorSecs / 2) ? + intervalSecs + divisorSecs - remainder : intervalSecs - remainder; + nearestMultiple = nearestMultiple === 0 ? divisorSecs : nearestMultiple; + const nearestMultipleInt = moment.duration(nearestMultiple, 'seconds'); + decorateInterval(nearestMultipleInt, this.getDuration()); + + // Check to see if the new interval is scaled compared to the original. + const preScaled = _.get(interval, 'preScaled'); + if (preScaled !== undefined && preScaled < nearestMultipleInt) { + nearestMultipleInt.preScaled = preScaled; + nearestMultipleInt.scale = preScaled / nearestMultipleInt; + nearestMultipleInt.scaled = true; + } + + return nearestMultipleInt; +}; + +// Appends some MlTimeBuckets specific properties to the momentjs duration interval. +// Uses the originalDuration from which the time bucket was created to calculate the overflow +// property (i.e. difference between the supplied duration and the calculated bucket interval). +function decorateInterval(interval, originalDuration) { + const esInterval = calcEsInterval(interval); + interval.esValue = esInterval.value; + interval.esUnit = esInterval.unit; + interval.expression = esInterval.expression; + interval.overflow = originalDuration > interval ? moment.duration(interval - originalDuration) : false; + + const prettyUnits = moment.normalizeUnits(esInterval.unit); + if (esInterval.value === 1) { + interval.description = prettyUnits; + } else { + interval.description = `${esInterval.value} ${prettyUnits}s`; + } - return MlTimeBuckets; + return interval; } export function getBoundsRoundedToInterval(bounds, interval, inclusiveEnd = false) { diff --git a/x-pack/plugins/ml/server/client/elasticsearch_ml.js b/x-pack/plugins/ml/server/client/elasticsearch_ml.js index 1add25b2143e8..4a25036e0ad0f 100644 --- a/x-pack/plugins/ml/server/client/elasticsearch_ml.js +++ b/x-pack/plugins/ml/server/client/elasticsearch_ml.js @@ -116,6 +116,14 @@ export const elasticsearchJsPlugin = (Client, config, components) => { ml.getDataFrameTransformsStats = ca({ urls: [ + { + fmt: '/_data_frame/transforms/<%=jobId%>/_stats', + req: { + jobId: { + type: 'string' + } + } + }, { fmt: '/_data_frame/transforms/_stats', } @@ -179,10 +187,13 @@ export const elasticsearchJsPlugin = (Client, config, components) => { ml.stopDataFrameTransformsJob = ca({ urls: [ { - fmt: '/_data_frame/transforms/<%=jobId%>/_stop', + fmt: '/_data_frame/transforms/<%=jobId%>/_stop?&force=<%=force%>', req: { jobId: { type: 'string' + }, + force: { + type: 'boolean' } } } diff --git a/x-pack/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts b/x-pack/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts index fe0e11826246e..e012b7a06e91d 100644 --- a/x-pack/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts +++ b/x-pack/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts @@ -27,6 +27,7 @@ interface KibanaHapiServer extends Server { export function makeMlUsageCollector(server: KibanaHapiServer): void { const mlUsageCollector = server.usage.collectorSet.makeUsageCollector({ type: 'ml', + isReady: () => true, fetch: async (): Promise => { try { const savedObjectsClient = getSavedObjectsClient(server); diff --git a/x-pack/plugins/ml/server/lib/sample_data_sets/index.ts b/x-pack/plugins/ml/server/lib/sample_data_sets/index.ts new file mode 100644 index 0000000000000..c922c9eb7c029 --- /dev/null +++ b/x-pack/plugins/ml/server/lib/sample_data_sets/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { addLinksToSampleDatasets } from './sample_data_sets'; diff --git a/x-pack/plugins/ml/server/lib/sample_data_sets/sample_data_sets.ts b/x-pack/plugins/ml/server/lib/sample_data_sets/sample_data_sets.ts new file mode 100644 index 0000000000000..2082538adfed1 --- /dev/null +++ b/x-pack/plugins/ml/server/lib/sample_data_sets/sample_data_sets.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export function addLinksToSampleDatasets(server: any) { + const sampleDataLinkLabel = i18n.translate('xpack.ml.sampleDataLinkLabel', { + defaultMessage: 'ML jobs', + }); + + server.addAppLinksToSampleDataset('ecommerce', { + path: + '/app/ml#/modules/check_view_or_create?id=sample_data_ecommerce&index=ff959d40-b880-11e8-a6d9-e546fe2bba5f', + label: sampleDataLinkLabel, + icon: 'machineLearningApp', + }); + + server.addAppLinksToSampleDataset('logs', { + path: + '/app/ml#/modules/check_view_or_create?id=sample_data_weblogs&index=90943e30-9a47-11e8-b64d-95841ca0b247', + label: sampleDataLinkLabel, + icon: 'machineLearningApp', + }); +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/__tests__/data_recognizer.js b/x-pack/plugins/ml/server/models/data_recognizer/__tests__/data_recognizer.js index b6cba3eb36f90..b96d586b4dd94 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/__tests__/data_recognizer.js +++ b/x-pack/plugins/ml/server/models/data_recognizer/__tests__/data_recognizer.js @@ -19,6 +19,8 @@ describe('ML - data recognizer', () => { 'auditbeat_process_hosts_ecs', 'metricbeat_system_ecs', 'nginx_ecs', + 'sample_data_ecommerce', + 'sample_data_weblogs', ]; // check all module IDs are the same as the list above diff --git a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.js b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.js index df343f88d2788..15b4e86ceafa8 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.js +++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.js @@ -10,6 +10,7 @@ import fs from 'fs'; import Boom from 'boom'; import { prefixDatafeedId } from '../../../common/util/job_utils'; import { mlLog } from '../../client/log'; +import { jobServiceProvider } from '../../models/job_service'; const ML_DIR = 'ml'; const KIBANA_DIR = 'kibana'; @@ -343,6 +344,47 @@ export class DataRecognizer { return results; } + async dataRecognizerJobsExist(moduleId) { + const results = {}; + + // Load the module with the specified ID and check if the jobs + // in the module have been created. + const module = await this.getModule(moduleId); + if (module && module.jobs) { + // Add a wildcard at the front of each of the job IDs in the module, + // as a prefix may have been supplied when creating the jobs in the module. + const jobIds = module.jobs.map(job => `*${job.id}`); + const { jobsExist } = jobServiceProvider(this.callWithRequest); + const jobInfo = await jobsExist(jobIds); + + // Check if the value for any of the jobs is false. + const doJobsExist = (Object.values(jobInfo).includes(false)) === false; + results.jobsExist = doJobsExist; + + if (doJobsExist === true) { + // Get the IDs of the jobs created from the module, and their earliest / latest timestamps. + const jobStats = await this.callWithRequest('ml.jobStats', { jobId: jobIds }); + const jobStatsJobs = []; + if (jobStats.jobs && jobStats.jobs.length > 0) { + jobStats.jobs.forEach((job) => { + const jobStat = { + id: job.job_id + }; + + if (job.data_counts) { + jobStat.earliestTimestampMs = job.data_counts.earliest_record_timestamp; + jobStat.latestTimestampMs = job.data_counts.latest_record_timestamp; + } + jobStatsJobs.push(jobStat); + }); + } + results.jobs = jobStatsJobs; + } + } + + return results; + } + async loadIndexPatterns() { return await this.savedObjectsClient.find({ type: 'index-pattern', perPage: 1000 }); } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_low_request_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_low_request_rate_ecs.json index b08a8526b17e5..f91e102a3f1c1 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_low_request_rate_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_low_request_rate_ecs.json @@ -14,7 +14,7 @@ "buckets": { "date_histogram": { "field": "@timestamp", - "interval": 900000, + "fixed_interval": "15m", "offset": 0, "order": { "_key": "asc" diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_visitor_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_visitor_rate_ecs.json index b88b66f15b329..0a98563f98817 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_visitor_rate_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_visitor_rate_ecs.json @@ -14,7 +14,7 @@ "buckets": { "date_histogram": { "field": "@timestamp", - "interval": 900000, + "fixed_interval": "15m", "offset": 0, "order": { "_key": "asc" diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_low_request_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_low_request_rate_ecs.json index cf143071aa519..d3333928299ea 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_low_request_rate_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_low_request_rate_ecs.json @@ -14,7 +14,7 @@ "buckets": { "date_histogram": { "field": "@timestamp", - "interval": 900000, + "fixed_interval": "15m", "offset": 0, "order": { "_key": "asc" diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_visitor_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_visitor_rate_ecs.json index 297bd16db4edf..e3faf85461938 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_visitor_rate_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_visitor_rate_ecs.json @@ -14,7 +14,7 @@ "buckets": { "date_histogram": { "field": "@timestamp", - "interval": 900000, + "fixed_interval": "15m", "offset": 0, "order": { "_key": "asc" diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/logo.json new file mode 100644 index 0000000000000..0229439e32d07 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/logo.json @@ -0,0 +1,3 @@ +{ + "icon": "logoKibana" +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/manifest.json new file mode 100644 index 0000000000000..dea2875b15160 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/manifest.json @@ -0,0 +1,27 @@ +{ + "id": "sample_data_ecommerce", + "title": "Kibana sample data eCommerce", + "description": "Find anomalies in eCommerce total sales data", + "type": "Sample Dataset", + "logoFile": "logo.json", + "defaultIndexPattern": "kibana_sample_data_ecommerce", + "query": { + "bool": { + "filter": [{ "term": { "_index": "kibana_sample_data_ecommerce" } }] + } + }, + "jobs": [ + { + "id": "high_sum_total_sales", + "file": "high_sum_total_sales.json" + } + ], + "datafeeds": [ + { + "id": "datafeed-high_sum_total_sales", + "file": "datafeed_high_sum_total_sales.json", + "job_id": "high_sum_total_sales" + } + ], + "kibana": {} +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/datafeed_high_sum_total_sales.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/datafeed_high_sum_total_sales.json new file mode 100644 index 0000000000000..193239995c6d3 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/datafeed_high_sum_total_sales.json @@ -0,0 +1,9 @@ +{ + "job_id": "JOB_ID", + "indexes": ["INDEX_PATTERN_NAME"], + "query": { + "bool": { + "filter": [{ "term": { "_index": "kibana_sample_data_ecommerce" } }] + } + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/high_sum_total_sales.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/high_sum_total_sales.json new file mode 100644 index 0000000000000..e0230e2a06373 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/high_sum_total_sales.json @@ -0,0 +1,38 @@ +{ + "groups": ["kibana_sample_data", "kibana_sample_ecommerce"], + "description": "Find customers spending an unusually high amount in an hour", + "analysis_config": { + "bucket_span": "1h", + "detectors": [ + { + "detector_description": "High total sales", + "function": "high_sum", + "field_name": "taxful_total_price", + "over_field_name": "customer_full_name.keyword" + } + ], + "influencers": ["customer_full_name.keyword", "category.keyword"] + }, + "analysis_limits": { + "model_memory_limit": "10mb" + }, + "data_description": { + "time_field": "order_date" + }, + "model_plot_config": { + "enabled": true + }, + "custom_settings": { + "created_by": "ml-module-sample", + "custom_urls": [ + { + "url_name": "Raw data", + "url_value": "kibana#/discover?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(index:ff959d40-b880-11e8-a6d9-e546fe2bba5f,query:(language:kuery,query:\u0027customer_full_name.keyword:\u0022$customer_full_name.keyword$\u0022\u0027),sort:!('@timestamp',desc))" + }, + { + "url_name": "Data dashboard", + "url_value": "kibana#/dashboard/722b74f0-b882-11e8-a6d9-e546fe2bba5f?_g=(filters:!(),time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(filters:!((\u0027$state\u0027:(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:customer_full_name.keyword,negate:!f,params:(query:\u0027$customer_full_name.keyword$\u0027),type:phrase,value:\u0027$customer_full_name.keyword$\u0027),query:(match:(customer_full_name.keyword:(query:\u0027$customer_full_name.keyword$\u0027,type:phrase))))),query:(language:kuery,query:\u0027\u0027))" + } + ] + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/logo.json new file mode 100644 index 0000000000000..0229439e32d07 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/logo.json @@ -0,0 +1,3 @@ +{ + "icon": "logoKibana" +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/manifest.json new file mode 100644 index 0000000000000..9c3aceba33f38 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/manifest.json @@ -0,0 +1,45 @@ +{ + "id": "sample_data_weblogs", + "title": "Kibana sample data web logs", + "description": "Find anomalies in Kibana sample web logs data", + "type": "Sample Dataset", + "logoFile": "logo.json", + "defaultIndexPattern": "kibana_sample_data_logs", + "query": { + "bool": { + "filter": [{ "term": { "_index": "kibana_sample_data_logs" } }] + } + }, + "jobs": [ + { + "id": "low_request_rate", + "file": "low_request_rate.json" + }, + { + "id": "response_code_rates", + "file": "response_code_rates.json" + }, + { + "id": "url_scanning", + "file": "url_scanning.json" + } + ], + "datafeeds": [ + { + "id": "datafeed-low_request_rate", + "file": "datafeed_low_request_rate.json", + "job_id": "low_request_rate" + }, + { + "id": "datafeed-response_code_rates", + "file": "datafeed_response_code_rates.json", + "job_id": "response_code_rates" + }, + { + "id": "datafeed-url_scanning", + "file": "datafeed_url_scanning.json", + "job_id": "url_scanning" + } + ], + "kibana": {} +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_low_request_rate.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_low_request_rate.json new file mode 100644 index 0000000000000..e2682e2c15008 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_low_request_rate.json @@ -0,0 +1,24 @@ +{ + "job_id": "JOB_ID", + "indexes": ["INDEX_PATTERN_NAME"], + "query": { + "bool": { + "filter": [{ "term": { "_index": "kibana_sample_data_logs" } }] + } + }, + "aggregations": { + "buckets": { + "date_histogram": { + "field": "timestamp", + "interval": 3600000 + }, + "aggregations": { + "timestamp": { + "max": { + "field": "timestamp" + } + } + } + } + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_response_code_rates.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_response_code_rates.json new file mode 100644 index 0000000000000..3afbfdefc31e8 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_response_code_rates.json @@ -0,0 +1,9 @@ +{ + "job_id": "JOB_ID", + "indexes": ["INDEX_PATTERN_NAME"], + "query": { + "bool": { + "filter": [{ "term": { "_index": "kibana_sample_data_logs" } }] + } + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_url_scanning.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_url_scanning.json new file mode 100644 index 0000000000000..3afbfdefc31e8 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_url_scanning.json @@ -0,0 +1,9 @@ +{ + "job_id": "JOB_ID", + "indexes": ["INDEX_PATTERN_NAME"], + "query": { + "bool": { + "filter": [{ "term": { "_index": "kibana_sample_data_logs" } }] + } + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/low_request_rate.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/low_request_rate.json new file mode 100644 index 0000000000000..dc33276a3cfcd --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/low_request_rate.json @@ -0,0 +1,37 @@ +{ + "groups": ["kibana_sample_data", "kibana_sample_web_logs"], + "description": "Find unusually low request rates", + "analysis_config": { + "bucket_span": "1h", + "summary_count_field_name": "doc_count", + "detectors": [ + { + "detector_description": "Low request rates", + "function": "low_count" + } + ], + "influencers": [] + }, + "analysis_limits": { + "model_memory_limit": "10mb" + }, + "data_description": { + "time_field": "timestamp" + }, + "model_plot_config": { + "enabled": true + }, + "custom_settings": { + "created_by": "ml-module-sample", + "custom_urls": [ + { + "url_name": "Raw data", + "url_value": "kibana#/discover?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(index:\u002790943e30-9a47-11e8-b64d-95841ca0b247\u0027)" + }, + { + "url_name": "Data dashboard", + "url_value": "kibana#/dashboard/edf84fe0-e1a0-11e7-b6d5-4dc382ef7f5b?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(filters:!(),query:(language:kuery,query:\u0027\u0027))" + } + ] + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/response_code_rates.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/response_code_rates.json new file mode 100644 index 0000000000000..58b68de7df896 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/response_code_rates.json @@ -0,0 +1,37 @@ +{ + "groups": ["kibana_sample_data", "kibana_sample_web_logs"], + "description": "Find unusual event rates by HTTP response code (high and low)", + "analysis_config": { + "bucket_span": "1h", + "detectors": [ + { + "detector_description": "Event rate by response code", + "function": "count", + "partition_field_name": "response.keyword" + } + ], + "influencers": ["clientip", "response.keyword"] + }, + "analysis_limits": { + "model_memory_limit": "10mb" + }, + "data_description": { + "time_field": "timestamp" + }, + "model_plot_config": { + "enabled": true + }, + "custom_settings": { + "created_by": "ml-module-sample", + "custom_urls": [ + { + "url_name": "Raw data", + "url_value": "kibana#/discover?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(index:\u002790943e30-9a47-11e8-b64d-95841ca0b247\u0027,query:(language:kuery,query:\u0027response.keyword:\u0022$response.keyword$\u0022\u0027),sort:!('@timestamp',desc))" + }, + { + "url_name": "Data dashboard", + "url_value": "kibana#/dashboard/edf84fe0-e1a0-11e7-b6d5-4dc382ef7f5b?_g=(filters:!(),time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(filters:!((\u0027$state\u0027:(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:response.keyword,negate:!f,params:(query:\u0027$response.keyword$\u0027),type:phrase,value:\u0027$response.keyword$\u0027),query:(match:(response.keyword:(query:\u0027$response.keyword$\u0027,type:phrase))))),query:(language:kuery,query:\u0027\u0027))" + } + ] + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/url_scanning.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/url_scanning.json new file mode 100644 index 0000000000000..042c236dc6d2d --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/url_scanning.json @@ -0,0 +1,38 @@ +{ + "groups": ["kibana_sample_data", "kibana_sample_web_logs"], + "description": "Find client IPs accessing an unusually high distinct count of URLs", + "analysis_config": { + "bucket_span": "1h", + "detectors": [ + { + "detector_description": "High distinct count of URLs for a client IPs", + "function": "high_distinct_count", + "field_name": "url.keyword", + "over_field_name": "clientip" + } + ], + "influencers": ["clientip"] + }, + "analysis_limits": { + "model_memory_limit": "10mb" + }, + "data_description": { + "time_field": "timestamp" + }, + "model_plot_config": { + "enabled": true + }, + "custom_settings": { + "created_by": "ml-module-sample", + "custom_urls": [ + { + "url_name": "Raw data", + "url_value": "kibana#/discover?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(index:\u002790943e30-9a47-11e8-b64d-95841ca0b247\u0027)" + }, + { + "url_name": "Data dashboard", + "url_value": "kibana#/dashboard/edf84fe0-e1a0-11e7-b6d5-4dc382ef7f5b?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(filters:!(),query:(language:kuery,query:\u0027\u0027))" + } + ] + } +} diff --git a/x-pack/plugins/ml/server/models/fields_service/fields_service.js b/x-pack/plugins/ml/server/models/fields_service/fields_service.js index 545d5e263b485..b178a37586ba2 100644 --- a/x-pack/plugins/ml/server/models/fields_service/fields_service.js +++ b/x-pack/plugins/ml/server/models/fields_service/fields_service.js @@ -13,6 +13,8 @@ export function fieldsServiceProvider(callWithRequest) { // Obtains the cardinality of one or more fields. // Returns an Object whose keys are the names of the fields, // with values equal to the cardinality of the field. + // Any of the supplied fieldNames which are not aggregatable will + // be omitted from the returned Object. function getCardinalityOfFields( index, fieldNames, @@ -21,63 +23,95 @@ export function fieldsServiceProvider(callWithRequest) { earliestMs, latestMs) { - // Build the criteria to use in the bool filter part of the request. - // Add criteria for the time range and the datafeed config query. - const mustCriteria = [ - { - range: { - [timeFieldName]: { - gte: earliestMs, - lte: latestMs, - format: 'epoch_millis' - } - } - } - ]; - - if (query) { - mustCriteria.push(query); - } - - const aggs = fieldNames.reduce((obj, field) => { - obj[field] = { cardinality: { field } }; - return obj; - }, {}); - - const body = { - query: { - bool: { - must: mustCriteria - } - }, - size: 0, - _source: { - excludes: [] - }, - aggs - }; - + // First check that each of the supplied fieldNames are aggregatable, + // then obtain the cardinality for each of the aggregatable fields. return new Promise((resolve, reject) => { - callWithRequest('search', { + callWithRequest('fieldCaps', { index, - body + fields: fieldNames }) - .then((resp) => { - const aggregations = resp.aggregations; - if (aggregations !== undefined) { - const results = fieldNames.reduce((obj, field) => { - obj[field] = (aggregations[field] || { value: 0 }).value; + .then((fieldCapsResp) => { + const aggregatableFields = []; + + fieldNames.forEach((fieldName) => { + const fieldInfo = fieldCapsResp.fields[fieldName]; + const typeKeys = Object.keys(fieldInfo); + if (typeKeys.length > 0) { + const fieldType = typeKeys[0]; + const isFieldAggregatable = fieldInfo[fieldType].aggregatable; + if (isFieldAggregatable === true) { + aggregatableFields.push(fieldName); + } + } + }); + + if (aggregatableFields.length > 0) { + // Build the criteria to use in the bool filter part of the request. + // Add criteria for the time range and the datafeed config query. + const mustCriteria = [ + { + range: { + [timeFieldName]: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis' + } + } + } + ]; + + if (query) { + mustCriteria.push(query); + } + + const aggs = aggregatableFields.reduce((obj, field) => { + obj[field] = { cardinality: { field } }; return obj; }, {}); - resolve(results); + + const body = { + query: { + bool: { + must: mustCriteria + } + }, + size: 0, + _source: { + excludes: [] + }, + aggs + }; + + callWithRequest('search', { + index, + body + }) + .then((resp) => { + const aggregations = resp.aggregations; + if (aggregations !== undefined) { + const results = aggregatableFields.reduce((obj, field) => { + obj[field] = (aggregations[field] || { value: 0 }).value; + return obj; + }, {}); + resolve(results); + } else { + resolve({}); + } + }) + .catch((resp) => { + reject(resp); + }); } else { + // None of the fields are aggregatable. Return empty Object. resolve({}); } + }) .catch((resp) => { reject(resp); }); }); + } function getTimeFieldRange( diff --git a/x-pack/plugins/ml/server/models/job_service/jobs.js b/x-pack/plugins/ml/server/models/job_service/jobs.js index 3c157d454cc7c..84c7c1550ef6d 100644 --- a/x-pack/plugins/ml/server/models/job_service/jobs.js +++ b/x-pack/plugins/ml/server/models/job_service/jobs.js @@ -6,6 +6,7 @@ import { i18n } from '@kbn/i18n'; +import { ML_CONFIG_INDEX_PATTERN } from '../../../common/constants/index_patterns'; import { JOB_STATE, DATAFEED_STATE } from '../../../common/constants/states'; import { datafeedsProvider } from './datafeeds'; import { jobAuditMessagesProvider } from '../job_audit_messages'; @@ -322,6 +323,46 @@ export function jobsProvider(callWithRequest) { return { jobIds }; } + // Checks if each of the jobs in the specified list of IDs exist. + // Job IDs in supplied array may contain wildcard '*' characters + // e.g. *_low_request_rate_ecs + async function jobsExist(jobIds = []) { + // Get the list of job IDs. + // Use size of 10000, matching anomaly_detectors endpoint. + const maxJobsSize = 10000; + const resp = await callWithRequest('search', { + index: ML_CONFIG_INDEX_PATTERN, + size: maxJobsSize, + body: { + _source: 'job_id', + query: { + term: { job_type: 'anomaly_detector' } + } + } + }); + + const results = {}; + let allJobIds = []; + if (resp.hits.total.value > 0) { + const hits = resp.hits.hits; + allJobIds = hits.map(hit => hit._source.job_id); + + // Check if each of the supplied IDs match existing jobs. + jobIds.forEach((jobId) => { + // Create a Regex for each supplied ID as wildcard * is allowed. + const regexp = new RegExp(`^${jobId.replace(/\*+/g, '.*')}$`); + const exists = allJobIds.some(existsJobId => regexp.test(existsJobId)); + results[jobId] = exists; + }); + } else { + jobIds.forEach((jobId) => { + results[jobId] = false; + }); + } + + return results; + } + return { forceDeleteJob, deleteJobs, @@ -330,5 +371,6 @@ export function jobsProvider(callWithRequest) { jobsWithTimerange, createFullJobsList, deletingJobTasks, + jobsExist, }; } diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_model_memory_limit.js b/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_model_memory_limit.js index 2184445481eb1..41edfc7639234 100644 --- a/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_model_memory_limit.js +++ b/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_model_memory_limit.js @@ -29,6 +29,22 @@ describe('ML - validateModelMemoryLimit', () => { } }; + // mock field caps response + const fieldCapsResponse = { + indices: [ + 'cloudwatch' + ], + fields: { + instance: { + keyword: { + type: 'keyword', + searchable: true, + aggregatable: true + } + } + } + }; + // mock cardinality search response const cardinalitySearchResponse = { took: 8, @@ -52,9 +68,10 @@ describe('ML - validateModelMemoryLimit', () => { }; // mock callWithRequest - // used in two places: + // used in three places: // - to retrieve the info endpoint // - to search for cardinality of split field + // - to retrieve field capabilities used in search for split field cardinality function callWithRequest(call) { if (typeof call === undefined) { return Promise.reject(); @@ -65,6 +82,8 @@ describe('ML - validateModelMemoryLimit', () => { response = mlInfoResponse; } else if(call === 'search') { response = cardinalitySearchResponse; + } else if (call === 'fieldCaps') { + response = fieldCapsResponse; } return Promise.resolve(response); } diff --git a/x-pack/plugins/ml/server/routes/data_frame.js b/x-pack/plugins/ml/server/routes/data_frame.js index b1768b193f363..336e386bb728f 100644 --- a/x-pack/plugins/ml/server/routes/data_frame.js +++ b/x-pack/plugins/ml/server/routes/data_frame.js @@ -35,6 +35,20 @@ export function dataFrameRoutes(server, commonRouteConfig) { } }); + server.route({ + method: 'GET', + path: '/api/ml/_data_frame/transforms/{jobId}/_stats', + handler(request) { + const callWithRequest = callWithRequestFactory(server, request); + const { jobId } = request.params; + return callWithRequest('ml.getDataFrameTransformsStats', { jobId }) + .catch(resp => wrapError(resp)); + }, + config: { + ...commonRouteConfig + } + }); + server.route({ method: 'PUT', path: '/api/ml/_data_frame/transforms/{jobId}', @@ -95,8 +109,14 @@ export function dataFrameRoutes(server, commonRouteConfig) { path: '/api/ml/_data_frame/transforms/{jobId}/_stop', handler(request) { const callWithRequest = callWithRequestFactory(server, request); - const { jobId } = request.params; - return callWithRequest('ml.stopDataFrameTransformsJob', { jobId }) + const options = { + jobId: request.params.jobId + }; + const force = request.query.force; + if (force !== undefined) { + options.force = force; + } + return callWithRequest('ml.stopDataFrameTransformsJob', options) .catch(resp => wrapError(resp)); }, config: { diff --git a/x-pack/plugins/ml/server/routes/job_service.js b/x-pack/plugins/ml/server/routes/job_service.js index d4a37c18e655c..7851274af43cb 100644 --- a/x-pack/plugins/ml/server/routes/job_service.js +++ b/x-pack/plugins/ml/server/routes/job_service.js @@ -165,4 +165,19 @@ export function jobServiceRoutes(server, commonRouteConfig) { } }); + server.route({ + method: 'POST', + path: '/api/ml/jobs/jobs_exist', + handler(request) { + const callWithRequest = callWithRequestFactory(server, request); + const { jobsExist } = jobServiceProvider(callWithRequest); + const { jobIds } = request.payload; + return jobsExist(jobIds) + .catch(resp => wrapError(resp)); + }, + config: { + ...commonRouteConfig + } + }); + } diff --git a/x-pack/plugins/ml/server/routes/modules.js b/x-pack/plugins/ml/server/routes/modules.js index fdde363480b0e..67052f331dd0a 100644 --- a/x-pack/plugins/ml/server/routes/modules.js +++ b/x-pack/plugins/ml/server/routes/modules.js @@ -52,6 +52,11 @@ function saveModuleItems( request); } +function dataRecognizerJobsExist(callWithRequest, moduleId) { + const dr = new DataRecognizer(callWithRequest); + return dr.dataRecognizerJobsExist(moduleId); +} + export function dataRecognizer(server, commonRouteConfig) { server.route({ @@ -124,4 +129,18 @@ export function dataRecognizer(server, commonRouteConfig) { ...commonRouteConfig } }); + + server.route({ + method: 'GET', + path: '/api/ml/modules/jobs_exist/{moduleId}', + handler(request) { + const callWithRequest = callWithRequestFactory(server, request); + const moduleId = request.params.moduleId; + return dataRecognizerJobsExist(callWithRequest, moduleId) + .catch(resp => wrapError(resp)); + }, + config: { + ...commonRouteConfig + } + }); } diff --git a/x-pack/plugins/monitoring/common/cancel_promise.ts b/x-pack/plugins/monitoring/common/cancel_promise.ts new file mode 100644 index 0000000000000..f100edda50796 --- /dev/null +++ b/x-pack/plugins/monitoring/common/cancel_promise.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export enum Status { + Canceled, + Failed, + Resolved, + Awaiting, + Idle, +} + +/** + * Simple [PromiseWithCancel] factory + */ +export class PromiseWithCancel { + private _promise: Promise; + private _status: Status = Status.Idle; + + /** + * @param {Promise} promise Promise you want to cancel / track + */ + constructor(promise: Promise) { + this._promise = promise; + } + + /** + * Cancel the promise in any state + */ + public cancel = (): void => { + this._status = Status.Canceled; + }; + + /** + * @returns status based on [Status] + */ + public status = (): Status => { + return this._status; + }; + + /** + * @returns promise passed in [constructor] + * This sets the state to Status.Awaiting + */ + public promise = (): Promise => { + if (this._status === Status.Canceled) { + throw Error('Getting a canceled promise is not allowed'); + } else if (this._status !== Status.Idle) { + return this._promise; + } + return new Promise((resolve, reject) => { + this._status = Status.Awaiting; + return this._promise + .then(response => { + if (this._status !== Status.Canceled) { + this._status = Status.Resolved; + return resolve(response); + } + }) + .catch(error => { + if (this._status !== Status.Canceled) { + this._status = Status.Failed; + return reject(error); + } + }); + }); + }; +} diff --git a/x-pack/plugins/monitoring/public/components/alerts/alerts.js b/x-pack/plugins/monitoring/public/components/alerts/alerts.js index 089314d083889..4fa708c6b67cd 100644 --- a/x-pack/plugins/monitoring/public/components/alerts/alerts.js +++ b/x-pack/plugins/monitoring/public/components/alerts/alerts.js @@ -13,7 +13,6 @@ import { mapSeverity } from './map_severity'; import { FormattedAlert } from 'plugins/monitoring/components/alerts/formatted_alert'; import { EuiMonitoringTable } from 'plugins/monitoring/components/table'; import { EuiHealth, EuiIcon, EuiToolTip } from '@elastic/eui'; -import { injectI18n } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; const linkToCategories = { @@ -130,7 +129,7 @@ const getColumns = (kbnUrl, scope) => ([ }, ]); -const AlertsUI = ({ alerts, angular, sorting, pagination, onTableChange, intl }) => { +export const Alerts = ({ alerts, angular, sorting, pagination, onTableChange }) => { const alertsFlattened = alerts.map(alert => ({ ...alert, status: alert.metadata.severity, @@ -154,8 +153,7 @@ const AlertsUI = ({ alerts, angular, sorting, pagination, onTableChange, intl }) search={{ box: { incremental: true, - placeholder: intl.formatMessage({ - id: 'xpack.monitoring.alerts.filterAlertsPlaceholder', + placeholder: i18n.translate('xpack.monitoring.alerts.filterAlertsPlaceholder', { defaultMessage: 'Filter Alerts…' }) @@ -168,5 +166,3 @@ const AlertsUI = ({ alerts, angular, sorting, pagination, onTableChange, intl }) /> ); }; - -export const Alerts = injectI18n(AlertsUI); diff --git a/x-pack/plugins/monitoring/public/components/apm/instance/status.js b/x-pack/plugins/monitoring/public/components/apm/instance/status.js index c53339b917c18..13b8758494673 100644 --- a/x-pack/plugins/monitoring/public/components/apm/instance/status.js +++ b/x-pack/plugins/monitoring/public/components/apm/instance/status.js @@ -11,9 +11,10 @@ import { ApmStatusIcon } from '../status_icon'; import { formatMetric } from '../../../lib/format_number'; import { formatTimestampToDuration } from '../../../../common'; import { CALCULATE_DURATION_SINCE } from '../../../../common/constants'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; -function StatusUI({ stats, intl }) { +export function Status({ stats }) { const { name, output, @@ -24,46 +25,42 @@ function StatusUI({ stats, intl }) { const metrics = [ { - label: intl.formatMessage({ - id: 'xpack.monitoring.apm.instance.status.nameLabel', - defaultMessage: 'Name', + label: i18n.translate('xpack.monitoring.apm.instance.status.nameLabel', { + defaultMessage: 'Name' }), value: name, 'data-test-subj': 'name' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.apm.instance.status.outputLabel', - defaultMessage: 'Output', + label: i18n.translate('xpack.monitoring.apm.instance.status.outputLabel', { + defaultMessage: 'Output' }), value: output, 'data-test-subj': 'output' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.apm.instance.status.versionLabel', - defaultMessage: 'Version', + label: i18n.translate('xpack.monitoring.apm.instance.status.versionLabel', { + defaultMessage: 'Version' }), value: version, 'data-test-subj': 'version' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.apm.instance.status.uptimeLabel', - defaultMessage: 'Uptime', + label: i18n.translate('xpack.monitoring.apm.instance.status.uptimeLabel', { + defaultMessage: 'Uptime' }), value: formatMetric(uptime, 'time_since'), 'data-test-subj': 'uptime' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.apm.instance.status.lastEventLabel', - defaultMessage: 'Last Event', + label: i18n.translate('xpack.monitoring.apm.instance.status.lastEventLabel', { + defaultMessage: 'Last Event' }), - value: intl.formatMessage({ - id: 'xpack.monitoring.apm.instance.status.lastEventDescription', - defaultMessage: '{timeOfLastEvent} ago' }, { - timeOfLastEvent: formatTimestampToDuration(+moment(timeOfLastEvent), CALCULATE_DURATION_SINCE) + value: i18n.translate('xpack.monitoring.apm.instance.status.lastEventDescription', { + defaultMessage: '{timeOfLastEvent} ago', + values: { + timeOfLastEvent: formatTimestampToDuration(+moment(timeOfLastEvent), CALCULATE_DURATION_SINCE) + } }), 'data-test-subj': 'timeOfLastEvent', } @@ -91,5 +88,3 @@ function StatusUI({ stats, intl }) { /> ); } - -export const Status = injectI18n(StatusUI); diff --git a/x-pack/plugins/monitoring/public/components/apm/instances/instances.js b/x-pack/plugins/monitoring/public/components/apm/instances/instances.js index f43e896d89ce6..b4a1d5ea85c16 100644 --- a/x-pack/plugins/monitoring/public/components/apm/instances/instances.js +++ b/x-pack/plugins/monitoring/public/components/apm/instances/instances.js @@ -13,7 +13,6 @@ import { Status } from './status'; import { formatMetric } from '../../../lib/format_number'; import { formatTimestampToDuration } from '../../../../common'; import { i18n } from '@kbn/i18n'; -import { injectI18n } from '@kbn/i18n/react'; const columns = [ { @@ -84,7 +83,7 @@ const columns = [ }, ]; -export function ApmServerInstancesUI({ apms, intl }) { +export function ApmServerInstances({ apms }) { const { pagination, sorting, @@ -111,8 +110,7 @@ export function ApmServerInstancesUI({ apms, intl }) { search={{ box: { incremental: true, - placeholder: intl.formatMessage({ - id: 'xpack.monitoring.apm.instances.filterInstancesPlaceholder', + placeholder: i18n.translate('xpack.monitoring.apm.instances.filterInstancesPlaceholder', { defaultMessage: 'Filter Instances…' }) }, @@ -120,8 +118,7 @@ export function ApmServerInstancesUI({ apms, intl }) { { type: 'field_value_selection', field: 'version', - name: intl.formatMessage({ - id: 'xpack.monitoring.apm.instances.versionFilter', + name: i18n.translate('xpack.monitoring.apm.instances.versionFilter', { defaultMessage: 'Version' }), options: versions, @@ -139,5 +136,3 @@ export function ApmServerInstancesUI({ apms, intl }) { ); } - -export const ApmServerInstances = injectI18n(ApmServerInstancesUI); diff --git a/x-pack/plugins/monitoring/public/components/apm/instances/status.js b/x-pack/plugins/monitoring/public/components/apm/instances/status.js index e7cc42e8971f5..2364a286d3408 100644 --- a/x-pack/plugins/monitoring/public/components/apm/instances/status.js +++ b/x-pack/plugins/monitoring/public/components/apm/instances/status.js @@ -11,9 +11,10 @@ import { ApmStatusIcon } from '../status_icon'; import { formatMetric } from '../../../lib/format_number'; import { formatTimestampToDuration } from '../../../../common'; import { CALCULATE_DURATION_SINCE } from '../../../../common/constants'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; -function StatusUI({ stats, intl }) { +export function Status({ stats }) { const { apms: { total @@ -24,30 +25,28 @@ function StatusUI({ stats, intl }) { const metrics = [ { - label: intl.formatMessage({ - id: 'xpack.monitoring.apm.instances.status.serversLabel', - defaultMessage: 'Servers', + label: i18n.translate('xpack.monitoring.apm.instances.status.serversLabel', { + defaultMessage: 'Servers' }), value: total, 'data-test-subj': 'total' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.apm.instances.status.totalEventsLabel', - defaultMessage: 'Total Events', + label: i18n.translate('xpack.monitoring.apm.instances.status.totalEventsLabel', { + defaultMessage: 'Total Events' }), value: formatMetric(totalEvents, '0.[0]a'), 'data-test-subj': 'totalEvents' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.apm.instances.status.lastEventLabel', - defaultMessage: 'Last Event', + label: i18n.translate('xpack.monitoring.apm.instances.status.lastEventLabel', { + defaultMessage: 'Last Event' }), - value: intl.formatMessage({ - id: 'xpack.monitoring.apm.instances.status.lastEventDescription', - defaultMessage: '{timeOfLastEvent} ago' }, { - timeOfLastEvent: formatTimestampToDuration(+moment(timeOfLastEvent), CALCULATE_DURATION_SINCE) + value: i18n.translate('xpack.monitoring.apm.instances.status.lastEventDescription', { + defaultMessage: '{timeOfLastEvent} ago', + values: { + timeOfLastEvent: formatTimestampToDuration(+moment(timeOfLastEvent), CALCULATE_DURATION_SINCE) + } }), 'data-test-subj': 'timeOfLastEvent', } @@ -75,5 +74,3 @@ function StatusUI({ stats, intl }) { /> ); } - -export const Status = injectI18n(StatusUI); diff --git a/x-pack/plugins/monitoring/public/components/apm/status_icon.js b/x-pack/plugins/monitoring/public/components/apm/status_icon.js index 8d193daa9b062..adb333d8545e4 100644 --- a/x-pack/plugins/monitoring/public/components/apm/status_icon.js +++ b/x-pack/plugins/monitoring/public/components/apm/status_icon.js @@ -6,9 +6,9 @@ import React from 'react'; import { StatusIcon } from 'plugins/monitoring/components/status_icon'; -import { injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; -function ApmStatusIconUI({ status, availability = true, intl }) { +export function ApmStatusIcon({ status, availability = true }) { const type = (() => { if (!availability) { return StatusIcon.TYPES.GRAY; @@ -21,13 +21,12 @@ function ApmStatusIconUI({ status, availability = true, intl }) { return ( ); } - -export const ApmStatusIcon = injectI18n(ApmStatusIconUI); diff --git a/x-pack/plugins/monitoring/public/components/beats/beat/beat.js b/x-pack/plugins/monitoring/public/components/beats/beat/beat.js index e6a868d934331..86e9bb6f41dd2 100644 --- a/x-pack/plugins/monitoring/public/components/beats/beat/beat.js +++ b/x-pack/plugins/monitoring/public/components/beats/beat/beat.js @@ -8,10 +8,10 @@ import React from 'react'; import { MonitoringTimeseriesContainer } from '../../chart'; import { formatMetric } from '../../../lib/format_number'; import { EuiFlexItem, EuiPage, EuiPageBody, EuiFlexGrid, EuiSpacer, EuiPageContent, EuiPanel } from '@elastic/eui'; -import { injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { SummaryStatus } from '../../summary_status'; -function BeatUi({ summary, metrics, intl, ...props }) { +export function Beat({ summary, metrics, ...props }) { const metricsToShow = [ metrics.beat_event_rates, @@ -26,37 +26,51 @@ function BeatUi({ summary, metrics, intl, ...props }) { const summarytStatsTop = [ { - label: intl.formatMessage({ id: 'xpack.monitoring.beats.instance.nameLabel', defaultMessage: 'Name' }), + label: i18n.translate('xpack.monitoring.beats.instance.nameLabel', { + defaultMessage: 'Name' + }), value: summary.name, 'data-test-subj': 'name' }, { - label: intl.formatMessage({ id: 'xpack.monitoring.beats.instance.hostLabel', defaultMessage: 'Host' }), + label: i18n.translate('xpack.monitoring.beats.instance.hostLabel', { + defaultMessage: 'Host' + }), value: summary.transportAddress, 'data-test-subj': 'host' }, { - label: intl.formatMessage({ id: 'xpack.monitoring.beats.instance.versionLabel', defaultMessage: 'Version' }), + label: i18n.translate('xpack.monitoring.beats.instance.versionLabel', { + defaultMessage: 'Version' + }), value: summary.version, 'data-test-subj': 'version' }, { - label: intl.formatMessage({ id: 'xpack.monitoring.beats.instance.typeLabel', defaultMessage: 'Type' }), + label: i18n.translate('xpack.monitoring.beats.instance.typeLabel', { + defaultMessage: 'Type' + }), value: summary.type, 'data-test-subj': 'type' }, { - label: intl.formatMessage({ id: 'xpack.monitoring.beats.instance.outputLabel', defaultMessage: 'Output' }), + label: i18n.translate('xpack.monitoring.beats.instance.outputLabel', { + defaultMessage: 'Output' + }), value: summary.output, 'data-test-subj': 'output' }, { - label: intl.formatMessage({ id: 'xpack.monitoring.beats.instance.configReloadsLabel', defaultMessage: 'Config reloads' }), + label: i18n.translate('xpack.monitoring.beats.instance.configReloadsLabel', { + defaultMessage: 'Config reloads' + }), value: formatMetric(summary.configReloads, 'int_commas'), 'data-test-subj': 'configReloads' }, { - label: intl.formatMessage({ id: 'xpack.monitoring.beats.instance.uptimeLabel', defaultMessage: 'Uptime' }), + label: i18n.translate('xpack.monitoring.beats.instance.uptimeLabel', { + defaultMessage: 'Uptime' + }), value: formatMetric(summary.uptime, 'time_since'), 'data-test-subj': 'uptime' }, @@ -64,32 +78,44 @@ function BeatUi({ summary, metrics, intl, ...props }) { const summarytStatsBot = [ { - label: intl.formatMessage({ id: 'xpack.monitoring.beats.instance.eventsTotalLabel', defaultMessage: 'Events total' }), + label: i18n.translate('xpack.monitoring.beats.instance.eventsTotalLabel', { + defaultMessage: 'Events total' + }), value: formatMetric(summary.eventsTotal, 'int_commas'), 'data-test-subj': 'eventsTotal' }, { - label: intl.formatMessage({ id: 'xpack.monitoring.beats.instance.eventsEmittedLabel', defaultMessage: 'Events emitted' }), + label: i18n.translate('xpack.monitoring.beats.instance.eventsEmittedLabel', { + defaultMessage: 'Events emitted' + }), value: formatMetric(summary.eventsEmitted, 'int_commas'), 'data-test-subj': 'eventsEmitted' }, { - label: intl.formatMessage({ id: 'xpack.monitoring.beats.instance.eventsDroppedLabel', defaultMessage: 'Events dropped' }), + label: i18n.translate('xpack.monitoring.beats.instance.eventsDroppedLabel', { + defaultMessage: 'Events dropped' + }), value: formatMetric(summary.eventsDropped, 'int_commas'), 'data-test-subj': 'eventsDropped' }, { - label: intl.formatMessage({ id: 'xpack.monitoring.beats.instance.bytesSentLabel', defaultMessage: 'Bytes sent' }), + label: i18n.translate('xpack.monitoring.beats.instance.bytesSentLabel', { + defaultMessage: 'Bytes sent' + }), value: formatMetric(summary.bytesWritten, 'byte'), 'data-test-subj': 'bytesWritten' }, { - label: intl.formatMessage({ id: 'xpack.monitoring.beats.instance.handlesLimitSoftLabel', defaultMessage: 'Handles limit (soft)' }), + label: i18n.translate('xpack.monitoring.beats.instance.handlesLimitSoftLabel', { + defaultMessage: 'Handles limit (soft)' + }), value: formatMetric(summary.handlesSoftLimit, 'byte'), 'data-test-subj': 'handlesLimitSoft' }, { - label: intl.formatMessage({ id: 'xpack.monitoring.beats.instance.handlesLimitHardLabel', defaultMessage: 'Handles limit (hard)' }), + label: i18n.translate('xpack.monitoring.beats.instance.handlesLimitHardLabel', { + defaultMessage: 'Handles limit (hard)' + }), value: formatMetric(summary.handlesHardLimit, 'byte'), 'data-test-subj': 'handlesLimitHard' }, @@ -126,5 +152,3 @@ function BeatUi({ summary, metrics, intl, ...props }) { ); } - -export const Beat = injectI18n(BeatUi); diff --git a/x-pack/plugins/monitoring/public/components/beats/listing/listing.js b/x-pack/plugins/monitoring/public/components/beats/listing/listing.js index 54a6d3f4dd2a5..cce3379fca5b4 100644 --- a/x-pack/plugins/monitoring/public/components/beats/listing/listing.js +++ b/x-pack/plugins/monitoring/public/components/beats/listing/listing.js @@ -11,9 +11,8 @@ import { Stats } from 'plugins/monitoring/components/beats'; import { formatMetric } from 'plugins/monitoring/lib/format_number'; import { EuiMonitoringTable } from 'plugins/monitoring/components/table'; import { i18n } from '@kbn/i18n'; -import { injectI18n } from '@kbn/i18n/react'; -class ListingUI extends PureComponent { +export class Listing extends PureComponent { getColumns() { const { kbnUrl, scope } = this.props.angular; @@ -136,5 +135,3 @@ class ListingUI extends PureComponent { ); } } - -export const Listing = injectI18n(ListingUI); diff --git a/x-pack/plugins/monitoring/public/components/beats/overview/__snapshots__/overview.test.js.snap b/x-pack/plugins/monitoring/public/components/beats/overview/__snapshots__/overview.test.js.snap index 8ce2d94069786..21f909e9d73d5 100644 --- a/x-pack/plugins/monitoring/public/components/beats/overview/__snapshots__/overview.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/beats/overview/__snapshots__/overview.test.js.snap @@ -36,7 +36,7 @@ exports[`Overview that overview page renders normally 1`] = ` - - @@ -178,7 +178,7 @@ exports[`Overview that overview page renders normally 1`] = ` hasShadow={false} paddingSize="m" > - @@ -196,7 +196,7 @@ exports[`Overview that overview page renders normally 1`] = ` hasShadow={false} paddingSize="m" > - @@ -214,7 +214,7 @@ exports[`Overview that overview page renders normally 1`] = ` hasShadow={false} paddingSize="m" > - @@ -264,7 +264,7 @@ exports[`Overview that overview page shows a message if there is no beats data 1 hasShadow={false} paddingSize="m" > - @@ -282,7 +282,7 @@ exports[`Overview that overview page shows a message if there is no beats data 1 hasShadow={false} paddingSize="m" > - @@ -300,7 +300,7 @@ exports[`Overview that overview page shows a message if there is no beats data 1 hasShadow={false} paddingSize="m" > - @@ -318,7 +318,7 @@ exports[`Overview that overview page shows a message if there is no beats data 1 hasShadow={false} paddingSize="m" > - diff --git a/x-pack/plugins/monitoring/public/components/beats/overview/latest_active.js b/x-pack/plugins/monitoring/public/components/beats/overview/latest_active.js index 0438c89dd6d3c..f622a056b6581 100644 --- a/x-pack/plugins/monitoring/public/components/beats/overview/latest_active.js +++ b/x-pack/plugins/monitoring/public/components/beats/overview/latest_active.js @@ -9,18 +9,28 @@ import PropTypes from 'prop-types'; import { EuiBasicTable } from '@elastic/eui'; -import { injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; -function LatestActiveUi({ latestActive, intl }) { +export function LatestActive({ latestActive }) { const rangeMap = { 'last1m': - intl.formatMessage({ id: 'xpack.monitoring.beats.overview.latestActive.last1MinuteLabel', defaultMessage: 'Last 1 minute' }), + i18n.translate('xpack.monitoring.beats.overview.latestActive.last1MinuteLabel', { + defaultMessage: 'Last 1 minute' + }), 'last5m': - intl.formatMessage({ id: 'xpack.monitoring.beats.overview.latestActive.last5MinutesLabel', defaultMessage: 'Last 5 minutes' }), + i18n.translate('xpack.monitoring.beats.overview.latestActive.last5MinutesLabel', { + defaultMessage: 'Last 5 minutes' + }), 'last20m': - intl.formatMessage({ id: 'xpack.monitoring.beats.overview.latestActive.last20MinutesLabel', defaultMessage: 'Last 20 minutes' }), - 'last1h': intl.formatMessage({ id: 'xpack.monitoring.beats.overview.latestActive.last1HourLabel', defaultMessage: 'Last 1 hour' }), - 'last1d': intl.formatMessage({ id: 'xpack.monitoring.beats.overview.latestActive.last1DayLabel', defaultMessage: 'Last 1 day' }), + i18n.translate('xpack.monitoring.beats.overview.latestActive.last20MinutesLabel', { + defaultMessage: 'Last 20 minutes' + }), + 'last1h': i18n.translate('xpack.monitoring.beats.overview.latestActive.last1HourLabel', { + defaultMessage: 'Last 1 hour' + }), + 'last1d': i18n.translate('xpack.monitoring.beats.overview.latestActive.last1DayLabel', { + defaultMessage: 'Last 1 day' + }), }; const activity = latestActive.map(({ range, count }) => ({ @@ -46,11 +56,9 @@ function LatestActiveUi({ latestActive, intl }) { ); } -LatestActiveUi.propTypes = { +LatestActive.propTypes = { latestActive: PropTypes.arrayOf(PropTypes.shape({ range: PropTypes.string.isRequired, count: PropTypes.number.isRequired, })).isRequired }; - -export const LatestActive = injectI18n(LatestActiveUi); diff --git a/x-pack/plugins/monitoring/public/components/beats/overview/latest_active.test.js b/x-pack/plugins/monitoring/public/components/beats/overview/latest_active.test.js index 233f933202222..2afbe2bebb448 100644 --- a/x-pack/plugins/monitoring/public/components/beats/overview/latest_active.test.js +++ b/x-pack/plugins/monitoring/public/components/beats/overview/latest_active.test.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { shallowWithIntl } from '../../../../../../test_utils/enzyme_helpers'; +import { shallow } from 'enzyme'; import { LatestActive } from './latest_active'; @@ -19,8 +19,8 @@ describe('Latest Active', () => { { range: 'last1d', count: 10 }, ]; - const component = shallowWithIntl( - ); diff --git a/x-pack/plugins/monitoring/public/components/beats/overview/overview.js b/x-pack/plugins/monitoring/public/components/beats/overview/overview.js index 88b801f657570..4442f8021eec9 100644 --- a/x-pack/plugins/monitoring/public/components/beats/overview/overview.js +++ b/x-pack/plugins/monitoring/public/components/beats/overview/overview.js @@ -21,9 +21,10 @@ import { EuiPanel, EuiPageContent } from '@elastic/eui'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; -function renderLatestActive(latestActive, latestTypes, latestVersions, intl) { +function renderLatestActive(latestActive, latestTypes, latestVersions) { if (latestTypes && latestTypes.length > 0) { return ( @@ -73,10 +74,9 @@ function renderLatestActive(latestActive, latestTypes, latestVersions, intl) { ); } - const calloutMsg = intl.formatMessage({ - id: 'xpack.monitoring.beats.overview.noActivityDescription', - // eslint-disable-next-line max-len - defaultMessage: `Hi there! This area is where your latest Beats activity would show up, but you don't seem to have any activity within the last day.` + const calloutMsg = i18n.translate('xpack.monitoring.beats.overview.noActivityDescription', { + defaultMessage: 'Hi there! This area is where your latest Beats activity would show up, ' + + `but you don't seem to have any activity within the last day.` }); @@ -89,13 +89,12 @@ function renderLatestActive(latestActive, latestTypes, latestVersions, intl) { ); } -export function BeatsOverviewUI({ +export function BeatsOverview({ latestActive, latestTypes, latestVersions, stats, metrics, - intl, ...props }) { const seriesToShow = [ @@ -121,7 +120,7 @@ export function BeatsOverviewUI({ - {renderLatestActive(latestActive, latestTypes, latestVersions, intl)} + {renderLatestActive(latestActive, latestTypes, latestVersions)} {charts} @@ -131,5 +130,3 @@ export function BeatsOverviewUI({ ); } - -export const BeatsOverview = injectI18n(BeatsOverviewUI); diff --git a/x-pack/plugins/monitoring/public/components/beats/overview/overview.test.js b/x-pack/plugins/monitoring/public/components/beats/overview/overview.test.js index 6cf814ea90d42..3e849f9da63d1 100644 --- a/x-pack/plugins/monitoring/public/components/beats/overview/overview.test.js +++ b/x-pack/plugins/monitoring/public/components/beats/overview/overview.test.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { shallowWithIntl } from '../../../../../../test_utils/enzyme_helpers'; +import { shallow } from 'enzyme'; jest.mock('../stats', () => ({ Stats: () => 'Stats', @@ -41,8 +41,8 @@ describe('Overview', () => { beat_output_errors: 1 }; - const component = shallowWithIntl( - { beat_output_errors: 1 }; - const component = shallowWithIntl( - diff --git a/x-pack/plugins/monitoring/public/components/chart/horizontal_legend.js b/x-pack/plugins/monitoring/public/components/chart/horizontal_legend.js index 057658a9fa10e..9ce4d6224c45e 100644 --- a/x-pack/plugins/monitoring/public/components/chart/horizontal_legend.js +++ b/x-pack/plugins/monitoring/public/components/chart/horizontal_legend.js @@ -9,9 +9,10 @@ import { includes, isFunction } from 'lodash'; import { EuiKeyboardAccessible, } from '@elastic/eui'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; -class HorizontalLegendUI extends React.Component { +export class HorizontalLegend extends React.Component { constructor() { super(); this.formatter = this.formatter.bind(this); @@ -19,27 +20,47 @@ class HorizontalLegendUI extends React.Component { } /** - * @param {Number} value The value to format and show in the horizontal - * legend. A null means no data for the time bucket and will be formatted as - * 'N/A' + * @param {Number} value Final value to display */ - formatter(value) { - if (value === null) { + displayValue(value) { + return ( + + { value } + + ); + } + + /** + * @param {Number} value True if value is falsy and/or not a number + */ + validValue(value) { + return value !== null && value !== undefined && (typeof value === 'string' || !isNaN(value)); + } + + /** + * @param {Number} value The value to format and show in the horizontallegend. + * A null means no data for the time bucket and will be formatted as 'N/A' + * @param {Object} row Props passed form a parent by row index + */ + formatter(value, row) { + if (!this.validValue(value)) { return (); } + + if (row && row.tickFormatter) { + return this.displayValue(row.tickFormatter(value)); + } + if (isFunction(this.props.tickFormatter)) { - return this.props.tickFormatter(value); + return this.displayValue(this.props.tickFormatter(value)); } - return value; + return this.displayValue(value); } createSeries(row, rowIdx) { - const { intl } = this.props; - const formatter = row.tickFormatter || this.formatter; - const value = formatter(this.props.seriesValues[row.id]); const classes = ['col-md-4 col-xs-6 monRhythmChart__legendItem']; if (!includes(this.props.seriesFilter, row.id)) { @@ -64,16 +85,13 @@ class HorizontalLegendUI extends React.Component { - { ' ' + row.label } - - - { ' ' + value } + { ' ' + row.label + ' ' } + { this.formatter(this.props.seriesValues[row.id], row) }
); @@ -91,5 +109,3 @@ class HorizontalLegendUI extends React.Component { ); } } - -export const HorizontalLegend = injectI18n(HorizontalLegendUI); diff --git a/x-pack/plugins/monitoring/public/components/chart/monitoring_timeseries_container.js b/x-pack/plugins/monitoring/public/components/chart/monitoring_timeseries_container.js index c0e485a0d00c8..44e16cc302e39 100644 --- a/x-pack/plugins/monitoring/public/components/chart/monitoring_timeseries_container.js +++ b/x-pack/plugins/monitoring/public/components/chart/monitoring_timeseries_container.js @@ -14,9 +14,10 @@ import { InfoTooltip } from './info_tooltip'; import { EuiIconTip, EuiFlexGroup, EuiFlexItem, EuiTitle, EuiScreenReaderOnly } from '@elastic/eui'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; -function MonitoringTimeseriesContainerUI({ series, onBrush, intl }) { +export function MonitoringTimeseriesContainer({ series, onBrush }) { if (series === undefined) { return null; // still loading } @@ -27,10 +28,11 @@ function MonitoringTimeseriesContainerUI({ series, onBrush, intl }) { const bucketSize = get(first(series), 'bucket_size'); // bucket size will be the same for all metrics in all series const seriesScreenReaderTextList = [ - intl.formatMessage({ - id: 'xpack.monitoring.chart.seriesScreenReaderListDescription', - defaultMessage: 'Interval: {bucketSize}' }, { - bucketSize + i18n.translate('xpack.monitoring.chart.seriesScreenReaderListDescription', { + defaultMessage: 'Interval: {bucketSize}', + values: { + bucketSize + } }) ] .concat(series.map(item => `${item.metric.label}: ${item.metric.description}`)); @@ -80,6 +82,3 @@ function MonitoringTimeseriesContainerUI({ series, onBrush, intl }) {
); } - -export const MonitoringTimeseriesContainer = injectI18n(MonitoringTimeseriesContainerUI); - diff --git a/x-pack/plugins/monitoring/public/components/cluster/listing/alerts_indicator.js b/x-pack/plugins/monitoring/public/components/cluster/listing/alerts_indicator.js index 71c809b579255..4d167da5c4722 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/listing/alerts_indicator.js +++ b/x-pack/plugins/monitoring/public/components/cluster/listing/alerts_indicator.js @@ -7,13 +7,14 @@ import React from 'react'; import { mapSeverity } from 'plugins/monitoring/components/alerts/map_severity'; import { EuiHealth, EuiToolTip } from '@elastic/eui'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; const HIGH_SEVERITY = 2000; const MEDIUM_SEVERITY = 1000; const LOW_SEVERITY = 0; -function AlertsIndicatorUi({ alerts, intl }) { +export function AlertsIndicator({ alerts }) { if (alerts && alerts.count > 0) { const severity = (() => { if (alerts.high > 0) { return HIGH_SEVERITY; } @@ -24,18 +25,21 @@ function AlertsIndicatorUi({ alerts, intl }) { const tooltipText = (() => { switch (severity) { case HIGH_SEVERITY: - return intl.formatMessage({ - id: 'xpack.monitoring.cluster.listing.alertsInticator.highSeverityTooltip', - defaultMessage: 'There are some critical cluster issues that require your immediate attention!' }); + return i18n.translate('xpack.monitoring.cluster.listing.alertsInticator.highSeverityTooltip', { + defaultMessage: 'There are some critical cluster issues that require your immediate attention!' + }); case MEDIUM_SEVERITY: - return intl.formatMessage({ - id: 'xpack.monitoring.cluster.listing.alertsInticator.mediumSeverityTooltip', - defaultMessage: 'There are some issues that might have impact on your cluster.' }); + return i18n.translate( + 'xpack.monitoring.cluster.listing.alertsInticator.mediumSeverityTooltip', + { + defaultMessage: 'There are some issues that might have impact on your cluster.' + } + ); default: // might never show - return intl.formatMessage({ - id: 'xpack.monitoring.cluster.listing.alertsInticator.lowSeverityTooltip', - defaultMessage: 'There are some low-severity cluster issues' }); + return i18n.translate('xpack.monitoring.cluster.listing.alertsInticator.lowSeverityTooltip', { + defaultMessage: 'There are some low-severity cluster issues' + }); } })(); @@ -53,9 +57,9 @@ function AlertsIndicatorUi({ alerts, intl }) { return ( @@ -67,5 +71,3 @@ function AlertsIndicatorUi({ alerts, intl }) { ); } - -export const AlertsIndicator = injectI18n(AlertsIndicatorUi); diff --git a/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js b/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js index 1b39341a5f7cb..7124faa3bf052 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js +++ b/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js @@ -292,12 +292,12 @@ const handleClickIncompatibleLicense = (scope, clusterName) => { defaultMessage="Need to monitor multiple clusters? {getLicenseInfoLink} to enjoy multi-cluster monitoring." values={{ getLicenseInfoLink: ( - + - + ) }} /> @@ -332,20 +332,20 @@ const handleClickInvalidLicense = (scope, clusterName) => { defaultMessage="Need a license? {getBasicLicenseLink} or {getLicenseInfoLink} to enjoy multi-cluster monitoring." values={{ getBasicLicenseLink: ( - + - + ), getLicenseInfoLink: ( - + - + ) }} /> diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/alerts_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/alerts_panel.js index 726e680a6bac7..3a98db16b9360 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/alerts_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/alerts_panel.js @@ -10,7 +10,8 @@ import { mapSeverity } from 'plugins/monitoring/components/alerts/map_severity'; import { formatTimestampToDuration } from '../../../../common/format_timestamp_to_duration'; import { CALCULATE_DURATION_SINCE } from '../../../../common/constants'; import { formatDateTimeLocal } from '../../../../common/formatting'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, @@ -22,7 +23,7 @@ import { EuiCallOut, } from '@elastic/eui'; -function AlertsPanelUi({ alerts, changeUrl, intl }) { +export function AlertsPanel({ alerts, changeUrl }) { const goToAlerts = () => changeUrl('/alerts'); if (!alerts || !alerts.length) { @@ -35,10 +36,11 @@ function AlertsPanelUi({ alerts, changeUrl, intl }) { const severityIcon = mapSeverity(item.metadata.severity); if (item.resolved_timestamp) { - severityIcon.title = intl.formatMessage({ - id: 'xpack.monitoring.cluster.overview.alertsPanel.severityIconTitle', - defaultMessage: '{severityIconTitle} (resolved {time} ago)' }, - { severityIconTitle: severityIcon.title, time: formatTimestampToDuration(item.resolved_timestamp, CALCULATE_DURATION_SINCE) + severityIcon.title = i18n.translate('xpack.monitoring.cluster.overview.alertsPanel.severityIconTitle', { + defaultMessage: '{severityIconTitle} (resolved {time} ago)', + values: { + severityIconTitle: severityIcon.title, time: formatTimestampToDuration(item.resolved_timestamp, CALCULATE_DURATION_SINCE) + } }); severityIcon.color = 'success'; severityIcon.iconType = 'check'; @@ -106,5 +108,3 @@ function AlertsPanelUi({ alerts, changeUrl, intl }) {
); } - -export const AlertsPanel = injectI18n(AlertsPanelUi); diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js index 3983b6d7aa7a5..1726e21f92a9b 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js @@ -9,7 +9,8 @@ import moment from 'moment'; import { get } from 'lodash'; import { formatMetric } from 'plugins/monitoring/lib/format_number'; import { ClusterItemContainer, BytesPercentageUsage } from './helpers'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { EuiFlexGrid, @@ -25,7 +26,7 @@ import { import { formatTimestampToDuration } from '../../../../common'; import { CALCULATE_DURATION_SINCE } from '../../../../common/constants'; -function ApmPanelUi(props) { +export function ApmPanel(props) { if (!get(props, 'apms.total', 0) > 0) { return null; } @@ -37,7 +38,9 @@ function ApmPanelUi(props) { @@ -46,8 +49,9 @@ function ApmPanelUi(props) {

); } - -export const ApmPanel = injectI18n(ApmPanelUi); diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/beats_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/beats_panel.js index 93d1bdf747a36..f4da1a0e8737b 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/beats_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/beats_panel.js @@ -19,9 +19,10 @@ import { EuiHorizontalRule, } from '@elastic/eui'; import { ClusterItemContainer } from './helpers'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; -function BeatsPanelUi(props) { +export function BeatsPanel(props) { if (!get(props, 'beats.total', 0) > 0) { return null; } @@ -50,7 +51,9 @@ function BeatsPanelUi(props) { @@ -59,8 +62,9 @@ function BeatsPanelUi(props) {

); } - -export const BeatsPanel = injectI18n(BeatsPanelUi); diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js index f9c34bbb94ee1..01cfe25546c50 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js @@ -24,7 +24,7 @@ import { } from '@elastic/eui'; import { LicenseText } from './license_text'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { Reason } from '../../logs/reason'; const calculateShards = shards => { @@ -135,7 +135,7 @@ function renderLog(log) { ); } -function ElasticsearchPanelUi(props) { +export function ElasticsearchPanel(props) { const clusterStats = props.cluster_stats || {}; const nodes = clusterStats.nodes; const indices = clusterStats.indices; @@ -184,8 +184,9 @@ function ElasticsearchPanelUi(props) {

- { props.version || props.intl.formatMessage({ - id: 'xpack.monitoring.cluster.overview.esPanel.versionNotAvailableDescription', defaultMessage: 'N/A' }) } + { props.version || i18n.translate( + 'xpack.monitoring.cluster.overview.esPanel.versionNotAvailableDescription', + { + defaultMessage: 'N/A' + } + ) } ); } - -export const ElasticsearchPanel = injectI18n(ElasticsearchPanelUi); diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js index d8cc9478e2191..ccbc62968b134 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js @@ -19,9 +19,10 @@ import { EuiDescriptionListDescription, EuiHorizontalRule, } from '@elastic/eui'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; -function KibanaPanelUi(props) { +export function KibanaPanel(props) { if (!props.count) { return null; } @@ -38,8 +39,9 @@ function KibanaPanelUi(props) { {...props} statusIndicator={statusIndicator} url="kibana" - title={props.intl.formatMessage({ - id: 'xpack.monitoring.cluster.overview.kibanaPanel.kibanaTitle', defaultMessage: 'Kibana' })} + title={i18n.translate('xpack.monitoring.cluster.overview.kibanaPanel.kibanaTitle', { + defaultMessage: 'Kibana' + })} > @@ -48,8 +50,9 @@ function KibanaPanelUi(props) {

); } - -export const KibanaPanel = injectI18n(KibanaPanelUi); diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js index 6d47ff8381e90..073dfbcbd6f7f 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js @@ -22,9 +22,10 @@ import { EuiHorizontalRule, EuiIconTip, } from '@elastic/eui'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; -function LogstashPanelUi(props) { +export function LogstashPanel(props) { if (!props.node_count) { return null; } @@ -37,8 +38,9 @@ function LogstashPanelUi(props) { @@ -47,8 +49,9 @@ function LogstashPanelUi(props) {

); } - -export const LogstashPanel = injectI18n(LogstashPanelUi); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr/ccr.js b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr/ccr.js index 33cc774824587..a46deb052a45c 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr/ccr.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr/ccr.js @@ -18,13 +18,14 @@ import { } from '@elastic/eui'; import './ccr.css'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; function toSeconds(ms) { return Math.floor(ms / 1000) + 's'; } -class CcrUI extends Component { +export class Ccr extends Component { constructor(props) { super(props); this.state = { @@ -33,7 +34,6 @@ class CcrUI extends Component { } toggleShards(index, shards) { - const { intl } = this.props; const itemIdToExpandedRowMap = { ...this.state.itemIdToExpandedRowMap }; @@ -56,8 +56,7 @@ class CcrUI extends Component { columns={[ { field: 'shardId', - name: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.ccr.shardsTable.shardColumnTitle', + name: i18n.translate('xpack.monitoring.elasticsearch.ccr.shardsTable.shardColumnTitle', { defaultMessage: 'Shard' }), render: shardId => { @@ -73,8 +72,7 @@ class CcrUI extends Component { }, { field: 'syncLagOps', - name: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.ccr.shardsTable.syncLagOpsColumnTitle', + name: i18n.translate('xpack.monitoring.elasticsearch.ccr.shardsTable.syncLagOpsColumnTitle', { defaultMessage: 'Sync Lag (ops)' }), render: (syncLagOps, data) => ( @@ -114,23 +112,23 @@ class CcrUI extends Component { }, { field: 'syncLagTime', - name: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.ccr.shardsTable.lastFetchTimeColumnTitle', - defaultMessage: 'Last fetch time' - }), + name: i18n.translate( + 'xpack.monitoring.elasticsearch.ccr.shardsTable.lastFetchTimeColumnTitle', + { + defaultMessage: 'Last fetch time' + } + ), render: syncLagTime => {toSeconds(syncLagTime)} }, { field: 'opsSynced', - name: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.ccr.shardsTable.opsSyncedColumnTitle', + name: i18n.translate('xpack.monitoring.elasticsearch.ccr.shardsTable.opsSyncedColumnTitle', { defaultMessage: 'Ops synced' }), }, { field: 'error', - name: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.ccr.shardsTable.errorColumnTitle', + name: i18n.translate('xpack.monitoring.elasticsearch.ccr.shardsTable.errorColumnTitle', { defaultMessage: 'Error' }), render: error => ( @@ -152,7 +150,7 @@ class CcrUI extends Component { } renderTable() { - const { data, intl } = this.props; + const { data } = this.props; const items = data; let pagination = { @@ -177,8 +175,7 @@ class CcrUI extends Component { columns={[ { field: 'index', - name: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.ccr.ccrListingTable.indexColumnTitle', + name: i18n.translate('xpack.monitoring.elasticsearch.ccr.ccrListingTable.indexColumnTitle', { defaultMessage: 'Index' }), sortable: true, @@ -196,41 +193,45 @@ class CcrUI extends Component { { field: 'follows', sortable: true, - name: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.ccr.ccrListingTable.followsColumnTitle', + name: i18n.translate('xpack.monitoring.elasticsearch.ccr.ccrListingTable.followsColumnTitle', { defaultMessage: 'Follows' }), }, { field: 'syncLagOps', sortable: true, - name: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.ccr.ccrListingTable.syncLagOpsColumnTitle', - defaultMessage: 'Sync Lag (ops)' - }), + name: i18n.translate( + 'xpack.monitoring.elasticsearch.ccr.ccrListingTable.syncLagOpsColumnTitle', + { + defaultMessage: 'Sync Lag (ops)' + } + ), }, { field: 'syncLagTime', sortable: true, - name: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.ccr.ccrListingTable.lastFetchTimeColumnTitle', - defaultMessage: 'Last fetch time' - }), + name: i18n.translate( + 'xpack.monitoring.elasticsearch.ccr.ccrListingTable.lastFetchTimeColumnTitle', + { + defaultMessage: 'Last fetch time' + } + ), render: syncLagTime => {toSeconds(syncLagTime)} }, { field: 'opsSynced', sortable: true, - name: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.ccr.ccrListingTable.opsSyncedColumnTitle', - defaultMessage: 'Ops synced' - }), + name: i18n.translate( + 'xpack.monitoring.elasticsearch.ccr.ccrListingTable.opsSyncedColumnTitle', + { + defaultMessage: 'Ops synced' + } + ), }, { field: 'error', sortable: true, - name: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.ccr.ccrListingTable.errorColumnTitle', + name: i18n.translate('xpack.monitoring.elasticsearch.ccr.ccrListingTable.errorColumnTitle', { defaultMessage: 'Error' }), render: error => ( @@ -266,5 +267,3 @@ class CcrUI extends Component { ); } } - -export const Ccr = injectI18n(CcrUI); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr/ccr.test.js b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr/ccr.test.js index 838d3fcb5c6c4..8df42974c6633 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr/ccr.test.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr/ccr.test.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { shallowWithIntl } from '../../../../../../test_utils/enzyme_helpers'; +import { shallow } from 'enzyme'; import { Ccr } from './ccr'; describe('Ccr', () => { @@ -67,7 +67,7 @@ describe('Ccr', () => { } ]; - const component = shallowWithIntl(); + const component = shallow(); expect(component).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard.test.js.snap b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard.test.js.snap index a8df18d619880..6c3c11979a465 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard.test.js.snap @@ -65,7 +65,7 @@ exports[`CcrShard that it renders normally 1`] = ` - - + - + diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.js b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.js index 872bfd94ca8fc..76cff78515189 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.js @@ -22,9 +22,10 @@ import { import { MonitoringTimeseriesContainer } from '../../chart'; import { Status } from './status'; import { formatDateTimeLocal } from '../../../../common/formatting'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; -class CcrShardUI extends PureComponent { +export class CcrShard extends PureComponent { renderCharts() { const { metrics } = this.props; const seriesToShow = [ @@ -50,7 +51,7 @@ class CcrShardUI extends PureComponent { } renderErrors() { - const { stat, intl } = this.props; + const { stat } = this.props; if (stat.read_exceptions && stat.read_exceptions.length > 0) { return ( @@ -70,15 +71,13 @@ class CcrShardUI extends PureComponent { items={stat.read_exceptions} columns={[ { - name: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.ccrShard.errorsTable.typeColumnTitle', + name: i18n.translate('xpack.monitoring.elasticsearch.ccrShard.errorsTable.typeColumnTitle', { defaultMessage: 'Type' }), field: 'exception.type' }, { - name: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.ccrShard.errorsTable.reasonColumnTitle', + name: i18n.translate('xpack.monitoring.elasticsearch.ccrShard.errorsTable.reasonColumnTitle', { defaultMessage: 'Reason' }), field: 'exception.reason', @@ -144,5 +143,3 @@ class CcrShardUI extends PureComponent { ); } } - -export const CcrShard = injectI18n(CcrShardUI); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.test.js b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.test.js index cbd1ce79a383c..40a0f32931d3e 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.test.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.test.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { shallowWithIntl } from '../../../../../../test_utils/enzyme_helpers'; +import { shallow } from 'enzyme'; import { CcrShard } from './ccr_shard'; describe('CcrShard', () => { @@ -45,7 +45,7 @@ describe('CcrShard', () => { }; test('that it renders normally', () => { - const component = shallowWithIntl(); + const component = shallow(); expect(component).toMatchSnapshot(); }); @@ -63,7 +63,7 @@ describe('CcrShard', () => { } }; - const component = shallowWithIntl(); + const component = shallow(); expect(component.find('EuiPanel').get(0)).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/status.js b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/status.js index 302aebf4667be..5383999f60dbc 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/status.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/status.js @@ -7,9 +7,9 @@ import React from 'react'; import { SummaryStatus } from '../../summary_status'; import { formatMetric } from '../../../lib/format_number'; -import { injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; -function StatusUI({ stat, formattedLeader, oldestStat, intl }) { +export function Status({ stat, formattedLeader, oldestStat }) { const { follower_index: followerIndex, shard_id: shardId, @@ -24,41 +24,36 @@ function StatusUI({ stat, formattedLeader, oldestStat, intl }) { const metrics = [ { - label: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.ccrShard.status.followerIndexLabel', - defaultMessage: 'Follower Index', + label: i18n.translate('xpack.monitoring.elasticsearch.ccrShard.status.followerIndexLabel', { + defaultMessage: 'Follower Index' }), value: followerIndex, 'data-test-subj': 'followerIndex' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.ccrShard.status.shardIdLabel', - defaultMessage: 'Shard Id', + label: i18n.translate('xpack.monitoring.elasticsearch.ccrShard.status.shardIdLabel', { + defaultMessage: 'Shard Id' }), value: shardId, 'data-test-subj': 'shardId' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.ccrShard.status.leaderIndexLabel', - defaultMessage: 'Leader Index', + label: i18n.translate('xpack.monitoring.elasticsearch.ccrShard.status.leaderIndexLabel', { + defaultMessage: 'Leader Index' }), value: formattedLeader, 'data-test-subj': 'leaderIndex' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.ccrShard.status.opsSyncedLabel', - defaultMessage: 'Ops Synced', + label: i18n.translate('xpack.monitoring.elasticsearch.ccrShard.status.opsSyncedLabel', { + defaultMessage: 'Ops Synced' }), value: formatMetric(operationsReceived - oldestOperationsReceived, 'int_commas'), 'data-test-subj': 'operationsReceived' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.ccrShard.status.failedFetchesLabel', - defaultMessage: 'Failed Fetches', + label: i18n.translate('xpack.monitoring.elasticsearch.ccrShard.status.failedFetchesLabel', { + defaultMessage: 'Failed Fetches' }), value: formatMetric(failedFetches - oldestFailedFetches, 'int_commas'), 'data-test-subj': 'failedFetches' @@ -72,5 +67,3 @@ function StatusUI({ stat, formattedLeader, oldestStat, intl }) { /> ); } - -export const Status = injectI18n(StatusUI); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/cluster_status/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/cluster_status/index.js index 2426b47d1787e..6c5562e22debb 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/cluster_status/index.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/cluster_status/index.js @@ -8,9 +8,9 @@ import React from 'react'; import { SummaryStatus } from '../../summary_status'; import { ElasticsearchStatusIcon } from '../status_icon'; import { formatMetric } from '../../../lib/format_number'; -import { injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; -function ClusterStatusUI({ stats, intl }) { +export function ClusterStatus({ stats }) { const { dataSize, nodesCount, @@ -25,57 +25,50 @@ function ClusterStatusUI({ stats, intl }) { const metrics = [ { - label: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.clusterStatus.nodesLabel', - defaultMessage: 'Nodes', + label: i18n.translate('xpack.monitoring.elasticsearch.clusterStatus.nodesLabel', { + defaultMessage: 'Nodes' }), value: nodesCount, 'data-test-subj': 'nodesCount' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.clusterStatus.indicesLabel', - defaultMessage: 'Indices', + label: i18n.translate('xpack.monitoring.elasticsearch.clusterStatus.indicesLabel', { + defaultMessage: 'Indices' }), value: indicesCount, 'data-test-subj': 'indicesCount' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.clusterStatus.memoryLabel', - defaultMessage: 'Memory', + label: i18n.translate('xpack.monitoring.elasticsearch.clusterStatus.memoryLabel', { + defaultMessage: 'Memory' }), value: formatMetric(memUsed, 'byte') + ' / ' + formatMetric(memMax, 'byte'), 'data-test-subj': 'memory' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.clusterStatus.totalShardsLabel', - defaultMessage: 'Total Shards', + label: i18n.translate('xpack.monitoring.elasticsearch.clusterStatus.totalShardsLabel', { + defaultMessage: 'Total Shards' }), value: totalShards, 'data-test-subj': 'totalShards' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.clusterStatus.unassignedShardsLabel', - defaultMessage: 'Unassigned Shards', + label: i18n.translate('xpack.monitoring.elasticsearch.clusterStatus.unassignedShardsLabel', { + defaultMessage: 'Unassigned Shards' }), value: unassignedShards, 'data-test-subj': 'unassignedShards' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.clusterStatus.documentsLabel', - defaultMessage: 'Documents', + label: i18n.translate('xpack.monitoring.elasticsearch.clusterStatus.documentsLabel', { + defaultMessage: 'Documents' }), value: formatMetric(documentCount, 'int_commas'), 'data-test-subj': 'documentCount' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.clusterStatus.dataLabel', - defaultMessage: 'Data', + label: i18n.translate('xpack.monitoring.elasticsearch.clusterStatus.dataLabel', { + defaultMessage: 'Data' }), value: formatMetric(dataSize, 'byte'), 'data-test-subj': 'dataSize' @@ -95,5 +88,3 @@ function ClusterStatusUI({ stats, intl }) { /> ); } - -export const ClusterStatus = injectI18n(ClusterStatusUI); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/index_detail_status/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/index_detail_status/index.js index 78e43538eaa91..ecdf4eb7f9455 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/index_detail_status/index.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/index_detail_status/index.js @@ -8,9 +8,10 @@ import React, { Fragment } from 'react'; import { SummaryStatus } from '../../summary_status'; import { ElasticsearchStatusIcon } from '../status_icon'; import { formatMetric } from '../../../lib/format_number'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; -function IndexDetailStatusUI({ stats, intl }) { +export function IndexDetailStatus({ stats }) { const { dataSize, documents: documentCount, @@ -21,42 +22,40 @@ function IndexDetailStatusUI({ stats, intl }) { const metrics = [ { - label: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.indexDetailStatus.totalTitle', - defaultMessage: 'Total', + label: i18n.translate('xpack.monitoring.elasticsearch.indexDetailStatus.totalTitle', { + defaultMessage: 'Total' }), value: formatMetric(dataSize.total, '0.0 b'), 'data-test-subj': 'dataSize' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.indexDetailStatus.primariesTitle', - defaultMessage: 'Primaries', + label: i18n.translate('xpack.monitoring.elasticsearch.indexDetailStatus.primariesTitle', { + defaultMessage: 'Primaries' }), value: formatMetric(dataSize.primaries, '0.0 b'), 'data-test-subj': 'dataSizePrimaries' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.indexDetailStatus.documentsTitle', - defaultMessage: 'Documents', + label: i18n.translate('xpack.monitoring.elasticsearch.indexDetailStatus.documentsTitle', { + defaultMessage: 'Documents' }), value: formatMetric(documentCount, '0.[0]a'), 'data-test-subj': 'documentCount' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.indexDetailStatus.totalShardsTitle', - defaultMessage: 'Total Shards', + label: i18n.translate('xpack.monitoring.elasticsearch.indexDetailStatus.totalShardsTitle', { + defaultMessage: 'Total Shards' }), value: formatMetric(totalShards, 'int_commas'), 'data-test-subj': 'totalShards' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.indexDetailStatus.unassignedShardsTitle', - defaultMessage: 'Unassigned Shards', - }), + label: i18n.translate( + 'xpack.monitoring.elasticsearch.indexDetailStatus.unassignedShardsTitle', + { + defaultMessage: 'Unassigned Shards' + } + ), value: formatMetric(unassignedShards, 'int_commas'), 'data-test-subj': 'unassignedShards' } @@ -85,5 +84,3 @@ function IndexDetailStatusUI({ stats, intl }) { /> ); } - -export const IndexDetailStatus = injectI18n(IndexDetailStatusUI); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/indices/indices.js b/x-pack/plugins/monitoring/public/components/elasticsearch/indices/indices.js index 8c25ffab8d0a0..d53f267865232 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/indices/indices.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/indices/indices.js @@ -21,7 +21,7 @@ import { EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; const columns = [ { @@ -136,10 +136,9 @@ const getNoDataMessage = () => { ); }; -const ElasticsearchIndicesUI = ({ +export const ElasticsearchIndices = ({ clusterStatus, indices, - intl, sorting, pagination, onTableChange, @@ -175,9 +174,8 @@ const ElasticsearchIndicesUI = ({ search={{ box: { incremental: true, - placeholder: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.indices.monitoringTablePlaceholder', - defaultMessage: 'Filter Indices…', + placeholder: i18n.translate('xpack.monitoring.elasticsearch.indices.monitoringTablePlaceholder', { + defaultMessage: 'Filter Indices…' }) }, }} @@ -191,5 +189,3 @@ const ElasticsearchIndicesUI = ({ ); }; - -export const ElasticsearchIndices = injectI18n(ElasticsearchIndicesUI); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/ml_job_listing/status_icon.js b/x-pack/plugins/monitoring/public/components/elasticsearch/ml_job_listing/status_icon.js index 110cfc9f06e2a..c91d84ac572b1 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/ml_job_listing/status_icon.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/ml_job_listing/status_icon.js @@ -6,9 +6,9 @@ import React from 'react'; import { StatusIcon } from 'plugins/monitoring/components/status_icon'; -import { injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; -export function MachineLearningJobStatusIconUI({ status, intl }) { +export function MachineLearningJobStatusIcon({ status }) { const type = (() => { const statusKey = status.toUpperCase(); @@ -27,12 +27,10 @@ export function MachineLearningJobStatusIconUI({ status, intl }) { return ( ); } - -export const MachineLearningJobStatusIcon = injectI18n(MachineLearningJobStatusIconUI); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/node/status_icon.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node/status_icon.js index 73d9abcf59904..dfd64af64f178 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/node/status_icon.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/node/status_icon.js @@ -6,20 +6,18 @@ import React from 'react'; import { StatusIcon } from '../../status_icon'; -import { injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; -export function NodeStatusIconUI({ isOnline, status, intl }) { +export function NodeStatusIcon({ isOnline, status }) { const type = isOnline ? StatusIcon.TYPES.GREEN : StatusIcon.TYPES.GRAY; return ( ); } - -export const NodeStatusIcon = injectI18n(NodeStatusIconUI); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/node_detail_status/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node_detail_status/index.js index 9184b45eb159c..b9043529c9853 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/node_detail_status/index.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/node_detail_status/index.js @@ -8,9 +8,9 @@ import React, { Fragment } from 'react'; import { SummaryStatus } from '../../summary_status'; import { NodeStatusIcon } from '../node'; import { formatMetric } from '../../../lib/format_number'; -import { injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; -function NodeDetailStatusUI({ stats, intl }) { +export function NodeDetailStatus({ stats }) { const { transport_address: transportAddress, usedHeap, @@ -26,66 +26,61 @@ function NodeDetailStatusUI({ stats, intl }) { const metrics = [ { - label: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.nodeDetailStatus.transportAddress', - defaultMessage: 'Transport Address', + label: i18n.translate('xpack.monitoring.elasticsearch.nodeDetailStatus.transportAddress', { + defaultMessage: 'Transport Address' }), value: transportAddress, 'data-test-subj': 'transportAddress' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.nodeDetailStatus.jvmHeapLabel', - defaultMessage: '{javaVirtualMachine} Heap' }, { - javaVirtualMachine: 'JVM' + label: i18n.translate('xpack.monitoring.elasticsearch.nodeDetailStatus.jvmHeapLabel', { + defaultMessage: '{javaVirtualMachine} Heap', + + values: { + javaVirtualMachine: 'JVM' + } }), value: formatMetric(usedHeap, '0,0.[00]', '%', { prependSpace: false }), 'data-test-subj': 'jvmHeap' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.nodeDetailStatus.freeDiskSpaceLabel', - defaultMessage: 'Free Disk Space', + label: i18n.translate('xpack.monitoring.elasticsearch.nodeDetailStatus.freeDiskSpaceLabel', { + defaultMessage: 'Free Disk Space' }), value: formatMetric(freeSpace, '0.0 b'), 'data-test-subj': 'freeDiskSpace' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.nodeDetailStatus.documentsLabel', - defaultMessage: 'Documents', + label: i18n.translate('xpack.monitoring.elasticsearch.nodeDetailStatus.documentsLabel', { + defaultMessage: 'Documents' }), value: formatMetric(documents, '0.[0]a'), 'data-test-subj': 'documentCount' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.nodeDetailStatus.dataLabel', - defaultMessage: 'Data', + label: i18n.translate('xpack.monitoring.elasticsearch.nodeDetailStatus.dataLabel', { + defaultMessage: 'Data' }), value: formatMetric(dataSize, '0.0 b'), 'data-test-subj': 'dataSize' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.nodeDetailStatus.indicesLabel', - defaultMessage: 'Indices', + label: i18n.translate('xpack.monitoring.elasticsearch.nodeDetailStatus.indicesLabel', { + defaultMessage: 'Indices' }), value: formatMetric(indexCount, 'int_commas'), 'data-test-subj': 'indicesCount' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.nodeDetailStatus.shardsLabel', - defaultMessage: 'Shards', + label: i18n.translate('xpack.monitoring.elasticsearch.nodeDetailStatus.shardsLabel', { + defaultMessage: 'Shards' }), value: formatMetric(totalShards, 'int_commas'), 'data-test-subj': 'shardsCount' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.nodeDetailStatus.typeLabel', - defaultMessage: 'Type', + label: i18n.translate('xpack.monitoring.elasticsearch.nodeDetailStatus.typeLabel', { + defaultMessage: 'Type' }), value: nodeTypeLabel, 'data-test-subj': 'nodeType' @@ -108,5 +103,3 @@ function NodeDetailStatusUI({ stats, intl }) { /> ); } - -export const NodeDetailStatus = injectI18n(NodeDetailStatusUI); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/__snapshots__/cells.test.js.snap b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/__snapshots__/cells.test.js.snap index 678e96ee0a878..789e2a5756b48 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/__snapshots__/cells.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/__snapshots__/cells.test.js.snap @@ -16,18 +16,15 @@ exports[`Node Listing Metric Cell should format a non-percentage metric 1`] = `
-

206.3 GB  -

-
-
+
@@ -50,18 +47,15 @@ exports[`Node Listing Metric Cell should format a percentage metric 1`] = `
-

0%  -

-
-
+
diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/nodes.test.js b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/nodes.test.js new file mode 100644 index 0000000000000..8ab69e13bdc05 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/nodes.test.js @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiInMemoryTable } from '@elastic/eui'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { ElasticsearchNodes } from '../index'; + +const randValue = [5, 2, 9, 30, 11, 15, 21, 37, 45, 66, 3, 6, 92, 1, 1, 7, 3, 10, 31]; +const valAsc = ['n1', 'n1', 'n2', 'n3', 'n3', 'n5', 'n6', 'n7', 'n9', 'n10']; +const valDesc = ['n92', 'n66', 'n45', 'n37', 'n31', 'n30', 'n21', 'n15', 'n11', 'n10']; + +const metricTypes = ['node_cpu_utilization', 'node_load_average', 'node_jvm_mem_percent', 'node_free_space']; + +const getProps = (field) => { + const metricObj = { + metric: { + app: 'elasticsearch', + description: 'Free disk space available on the node.', + field: 'node_stats.fs.total.available_in_bytes', + format: '0.0 b', + hasCalculation: false, + isDerivative: false, + label: 'Disk Free Space', + metricAgg: 'max', + units: '' + }, + summary: { + lastVal: 0, + maxVal: 0, + minVal: 0, + slope: 1 + } + }; + + const node = { + isOnline: true, + name: 'n', + nodeTypeClass: 'fa-star', + nodeTypeLabel: 'Master Node', + node_cpu_utilization: { ...metricObj }, + node_load_average: { ...metricObj }, + node_jvm_mem_percent: { ...metricObj }, + node_free_space: { ...metricObj }, + resolver: '8FNpj67bS7aRd2D6GLHD7A', + shardCount: 30, + transport_address: '127.0.0.1:9300', + type: 'master' + }; + + const nodes = []; + + for (let i = 0; i < 20; ++i) { + const copyNode = JSON.parse(JSON.stringify(node)); + const value = randValue[i]; + copyNode.name = `${copyNode.name}${value}`; + copyNode[field].summary.lastVal = value; + nodes.push(copyNode); + } + + return { + clusterStatus: { + dataSize: 86843790, + documentCount: 200079, + indicesCount: 15, + memMax: 1037959168, + memUsed: 394748136, + nodesCount: 1, + status: 'yellow', + totalShards: 17, + unassignedShards: 2, + upTime: 28056934, + version: ['8.0.0'] + }, + nodes, + sorting: { + sort: { field: 'name', direction: 'asc' } + }, + pagination: { + initialPageSize: 10, + pageSizeOptions: [5, 10, 20, 50] + }, + onTableChange: () => void 0 + }; +}; + +const getSortedValues = (field, direction) => { + const props = getProps(field); + props.sorting = { sort: { field, direction } }; + const wrapper = mountWithIntl( + + ); + + const table = wrapper.find(EuiInMemoryTable); + const rows = table.find('.euiLink--primary'); + return rows.map((_, i) => rows.get(i).props.children); +}; + +describe('Node Listing Metric Cell sorting', () => { + for (const type of metricTypes) { + it(`should correctly sort ${type}`, () => { + expect(getSortedValues(type, 'asc')).toEqual(valAsc); + expect(getSortedValues(type, 'desc')).toEqual(valDesc); + }); + } +}); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/cells.js b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/cells.js index 9b462d00d287f..0655a6790a027 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/cells.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/cells.js @@ -32,6 +32,8 @@ const metricVal = (metric, format, isPercent) => { return formatMetric(metric, format); }; +const noWrapStyle = { overflowX: 'hidden', whiteSpace: 'nowrap' }; + function MetricCell({ isOnline, metric = {}, isPercent, ...props }) { if (isOnline) { const { lastVal, maxVal, minVal, slope } = get(metric, 'summary', {}); @@ -40,15 +42,13 @@ function MetricCell({ isOnline, metric = {}, isPercent, ...props }) { return ( - -

- { metricVal(lastVal, format, isPercent) } + + + {metricVal(lastVal, format, isPercent)}   -

+
-
- {i18n.translate('xpack.monitoring.elasticsearch.nodes.cells.maxText', { defaultMessage: '{metric} max', diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js index 755d35358e37e..3e6411b31e9ae 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js @@ -18,10 +18,12 @@ import { EuiPageContent, EuiPageBody, EuiPanel, + EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { injectI18n } from '@kbn/i18n/react'; +import _ from 'lodash'; +const getSortHandler = (type) => (item) => _.get(item, [type, 'summary', 'lastVal']); const getColumns = showCgroupMetricsElasticsearch => { const cols = []; @@ -33,26 +35,29 @@ const getColumns = showCgroupMetricsElasticsearch => { name: i18n.translate('xpack.monitoring.elasticsearch.nodes.nameColumnTitle', { defaultMessage: 'Name', }), + width: '20%', field: 'name', sortable: true, render: (value, node) => (
- - - -   - - + - {value} - - + + +   + + + {value} + + +
{extractIp(node.transport_address)} @@ -85,11 +90,26 @@ const getColumns = showCgroupMetricsElasticsearch => { } }); + cols.push({ + name: i18n.translate('xpack.monitoring.elasticsearch.nodes.shardsColumnTitle', { + defaultMessage: 'Shards', + }), + field: 'shardCount', + sortable: true, + render: (value, node) => { + return node.isOnline ? ( +
+ {value} +
+ ) : ; + } + }); + if (showCgroupMetricsElasticsearch) { cols.push({ name: cpuUsageColumnTitle, field: 'node_cgroup_quota', - sortable: true, + sortable: getSortHandler('node_cgroup_quota'), render: (value, node) => ( { defaultMessage: 'CPU Throttling', }), field: 'node_cgroup_throttled', - sortable: true, + sortable: getSortHandler('node_cgroup_throttled'), render: (value, node) => ( { cols.push({ name: cpuUsageColumnTitle, field: 'node_cpu_utilization', - sortable: true, + sortable: getSortHandler('node_cpu_utilization'), render: (value, node) => ( { defaultMessage: 'Load Average', }), field: 'node_load_average', - sortable: true, + sortable: getSortHandler('node_load_average'), render: (value, node) => ( { } }), field: 'node_jvm_mem_percent', - sortable: true, + sortable: getSortHandler('node_jvm_mem_percent'), render: (value, node) => ( { defaultMessage: 'Disk Free Space', }), field: 'node_free_space', - sortable: true, - width: '300px', + sortable: getSortHandler('node_free_space'), render: (value, node) => ( { ) }); - cols.push({ - name: i18n.translate('xpack.monitoring.elasticsearch.nodes.shardsColumnTitle', { - defaultMessage: 'Shards', - }), - field: 'shardCount', - sortable: true, - render: (value, node) => { - return node.isOnline ? ( -
- {value} -
- ) : ; - } - }); - return cols; }; -function ElasticsearchNodesUI({ clusterStatus, nodes, showCgroupMetricsElasticsearch, intl, ...props }) { +export function ElasticsearchNodes({ clusterStatus, nodes, showCgroupMetricsElasticsearch, ...props }) { const columns = getColumns(showCgroupMetricsElasticsearch); const { sorting, pagination, onTableChange } = props; @@ -222,9 +226,8 @@ function ElasticsearchNodesUI({ clusterStatus, nodes, showCgroupMetricsElasticse search={{ box: { incremental: true, - placeholder: intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.nodes.monitoringTablePlaceholder', - defaultMessage: 'Filter Nodes…', + placeholder: i18n.translate('xpack.monitoring.elasticsearch.nodes.monitoringTablePlaceholder', { + defaultMessage: 'Filter Nodes…' }), }, }} @@ -238,5 +241,3 @@ function ElasticsearchNodesUI({ clusterStatus, nodes, showCgroupMetricsElasticse ); } - -export const ElasticsearchNodes = injectI18n(ElasticsearchNodesUI); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/shard_activity.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/shard_activity.js index 06daffdbc173e..cc8991fe54218 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/shard_activity.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/shard_activity.js @@ -13,7 +13,7 @@ import { SourceDestination } from './source_destination'; import { FilesProgress, BytesProgress, TranslogProgress } from './progress'; import { parseProps } from './parse_props'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; const columns = [ { @@ -67,7 +67,7 @@ const columns = [ ]; -class ShardActivityUI extends React.Component { +export class ShardActivity extends React.Component { constructor(props) { super(props); this.getNoDataMessage = this.getNoDataMessage.bind(this); @@ -75,8 +75,7 @@ class ShardActivityUI extends React.Component { getNoDataMessage() { if (this.props.showShardActivityHistory) { - return this.props.intl.formatMessage({ - id: 'xpack.monitoring.elasticsearch.shardActivity.noDataMessage', + return i18n.translate('xpack.monitoring.elasticsearch.shardActivity.noDataMessage', { defaultMessage: 'There are no historical shard activity records for the selected time range.' }); } @@ -163,5 +162,3 @@ class ShardActivityUI extends React.Component { ); } } - -export const ShardActivity = injectI18n(ShardActivityUI); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/status_icon.js b/x-pack/plugins/monitoring/public/components/elasticsearch/status_icon.js index 0015071d08228..5e8656a33e407 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/status_icon.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/status_icon.js @@ -6,9 +6,9 @@ import React from 'react'; import { StatusIcon } from '../status_icon'; -import { injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; -function ElasticsearchStatusIconUI({ intl, status }) { +export function ElasticsearchStatusIcon({ status }) { const type = (() => { const statusKey = status.toUpperCase(); return StatusIcon.TYPES[statusKey] || StatusIcon.TYPES.GRAY; @@ -17,14 +17,13 @@ function ElasticsearchStatusIconUI({ intl, status }) { return ( ); } - -export const ElasticsearchStatusIcon = injectI18n(ElasticsearchStatusIconUI); diff --git a/x-pack/plugins/monitoring/public/components/kibana/cluster_status/index.js b/x-pack/plugins/monitoring/public/components/kibana/cluster_status/index.js index b1293fdd86388..557f482a0e1f4 100644 --- a/x-pack/plugins/monitoring/public/components/kibana/cluster_status/index.js +++ b/x-pack/plugins/monitoring/public/components/kibana/cluster_status/index.js @@ -8,9 +8,9 @@ import React from 'react'; import { SummaryStatus } from '../../summary_status'; import { KibanaStatusIcon } from '../status_icon'; import { formatMetric } from '../../../lib/format_number'; -import { injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; -function ClusterStatusUI({ stats, intl }) { +export function ClusterStatus({ stats }) { const { concurrent_connections: connections, count: instances, @@ -23,40 +23,35 @@ function ClusterStatusUI({ stats, intl }) { const metrics = [ { - label: intl.formatMessage({ - id: 'xpack.monitoring.kibana.clusterStatus.instancesLabel', + label: i18n.translate('xpack.monitoring.kibana.clusterStatus.instancesLabel', { defaultMessage: 'Instances' }), value: instances, 'data-test-subj': 'instances' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.kibana.clusterStatus.memoryLabel', + label: i18n.translate('xpack.monitoring.kibana.clusterStatus.memoryLabel', { defaultMessage: 'Memory' }), value: formatMetric(memSize, 'byte') + ' / ' + formatMetric(memLimit, 'byte'), 'data-test-subj': 'memory' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.kibana.clusterStatus.requestsLabel', + label: i18n.translate('xpack.monitoring.kibana.clusterStatus.requestsLabel', { defaultMessage: 'Requests' }), value: requests, 'data-test-subj': 'requests' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.kibana.clusterStatus.connectionsLabel', + label: i18n.translate('xpack.monitoring.kibana.clusterStatus.connectionsLabel', { defaultMessage: 'Connections' }), value: connections, 'data-test-subj': 'connections' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.kibana.clusterStatus.maxResponseTimeLabel', + label: i18n.translate('xpack.monitoring.kibana.clusterStatus.maxResponseTimeLabel', { defaultMessage: 'Max. Response Time' }), value: formatMetric(maxResponseTime, '0', 'ms'), @@ -77,5 +72,3 @@ function ClusterStatusUI({ stats, intl }) { /> ); } - -export const ClusterStatus = injectI18n(ClusterStatusUI); diff --git a/x-pack/plugins/monitoring/public/components/kibana/detail_status/index.js b/x-pack/plugins/monitoring/public/components/kibana/detail_status/index.js index 2bc90e9ff9b87..90acfa8d1df4c 100644 --- a/x-pack/plugins/monitoring/public/components/kibana/detail_status/index.js +++ b/x-pack/plugins/monitoring/public/components/kibana/detail_status/index.js @@ -8,9 +8,9 @@ import React from 'react'; import { SummaryStatus } from '../../summary_status'; import { KibanaStatusIcon } from '../status_icon'; import { formatMetric } from '../../../lib/format_number'; -import { injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; -function DetailStatusUI({ stats, intl }) { +export function DetailStatus({ stats }) { const { transport_address: transportAddress, os_memory_free: osFreeMemory, @@ -21,32 +21,28 @@ function DetailStatusUI({ stats, intl }) { const metrics = [ { - label: intl.formatMessage({ - id: 'xpack.monitoring.kibana.detailStatus.transportAddressLabel', + label: i18n.translate('xpack.monitoring.kibana.detailStatus.transportAddressLabel', { defaultMessage: 'Transport Address' }), value: transportAddress, 'data-test-subj': 'transportAddress' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.kibana.detailStatus.osFreeMemoryLabel', + label: i18n.translate('xpack.monitoring.kibana.detailStatus.osFreeMemoryLabel', { defaultMessage: 'OS Free Memory' }), value: formatMetric(osFreeMemory, 'byte'), 'data-test-subj': 'osFreeMemory' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.kibana.detailStatus.versionLabel', + label: i18n.translate('xpack.monitoring.kibana.detailStatus.versionLabel', { defaultMessage: 'Version' }), value: version, 'data-test-subj': 'version' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.kibana.detailStatus.uptimeLabel', + label: i18n.translate('xpack.monitoring.kibana.detailStatus.uptimeLabel', { defaultMessage: 'Uptime' }), value: formatMetric(uptime, 'time_since'), @@ -67,5 +63,3 @@ function DetailStatusUI({ stats, intl }) { /> ); } - -export const DetailStatus = injectI18n(DetailStatusUI); diff --git a/x-pack/plugins/monitoring/public/components/kibana/status_icon.js b/x-pack/plugins/monitoring/public/components/kibana/status_icon.js index 6ce9f2dc5bea6..04f5d7e71624d 100644 --- a/x-pack/plugins/monitoring/public/components/kibana/status_icon.js +++ b/x-pack/plugins/monitoring/public/components/kibana/status_icon.js @@ -6,9 +6,9 @@ import React from 'react'; import { StatusIcon } from 'plugins/monitoring/components/status_icon'; -import { injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; -function KibanaStatusIconUI({ status, availability = true, intl }) { +export function KibanaStatusIcon({ status, availability = true }) { const type = (() => { if (!availability) { return StatusIcon.TYPES.GRAY; @@ -21,13 +21,12 @@ function KibanaStatusIconUI({ status, availability = true, intl }) { return ( ); } - -export const KibanaStatusIcon = injectI18n(KibanaStatusIconUI); diff --git a/x-pack/plugins/monitoring/public/components/license/index.js b/x-pack/plugins/monitoring/public/components/license/index.js index ebd898ee5a217..4547c4f6bb722 100644 --- a/x-pack/plugins/monitoring/public/components/license/index.js +++ b/x-pack/plugins/monitoring/public/components/license/index.js @@ -10,10 +10,15 @@ import { EuiPageBody, EuiSpacer, EuiCodeBlock, - EuiPanel + EuiPanel, + EuiText, + EuiLink } from '@elastic/eui'; import { LicenseStatus, AddLicense } from 'plugins/xpack_main/components'; import { FormattedMessage } from '@kbn/i18n/react'; +import chrome from 'ui/chrome'; + +const licenseManagement = `${chrome.getBasePath()}/app/kibana#/management/elasticsearch/license_management`; const LicenseUpdateInfoForPrimary = ({ isPrimaryCluster, uploadLicensePath }) => { if (!isPrimaryCluster) { @@ -53,21 +58,27 @@ const LicenseUpdateInfoForRemote = ({ isPrimaryCluster }) => { export function License(props) { const { status, type, isExpired, expiryDate } = props; return ( - + -
- - - + + - - -
+ + + + +

+ For more license options please visit  + + License Management + +

+
); diff --git a/x-pack/plugins/monitoring/public/components/logstash/cluster_status/index.js b/x-pack/plugins/monitoring/public/components/logstash/cluster_status/index.js index e27d57d1a8ff0..12a6d531803d9 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/cluster_status/index.js +++ b/x-pack/plugins/monitoring/public/components/logstash/cluster_status/index.js @@ -7,9 +7,9 @@ import React from 'react'; import { SummaryStatus } from '../../summary_status'; import { formatMetric } from '../../../lib/format_number'; -import { injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; -function ClusterStatusUi({ stats, intl }) { +export function ClusterStatus({ stats }) { const { node_count: nodeCount, avg_memory_used: avgMemoryUsed, @@ -20,29 +20,29 @@ function ClusterStatusUi({ stats, intl }) { const metrics = [ { - label: intl.formatMessage({ - id: 'xpack.monitoring.logstash.clusterStatus.nodesLabel', defaultMessage: 'Nodes' + label: i18n.translate('xpack.monitoring.logstash.clusterStatus.nodesLabel', { + defaultMessage: 'Nodes' }), value: nodeCount, 'data-test-subj': 'node_count' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.logstash.clusterStatus.memoryLabel', defaultMessage: 'Memory' + label: i18n.translate('xpack.monitoring.logstash.clusterStatus.memoryLabel', { + defaultMessage: 'Memory' }), value: formatMetric(avgMemoryUsed, 'byte') + ' / ' + formatMetric(avgMemory, 'byte'), 'data-test-subj': 'memory_used' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.logstash.clusterStatus.eventsReceivedLabel', defaultMessage: 'Events Received' + label: i18n.translate('xpack.monitoring.logstash.clusterStatus.eventsReceivedLabel', { + defaultMessage: 'Events Received' }), value: formatMetric(eventsInTotal, '0.[0]a'), 'data-test-subj': 'events_in_total' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.logstash.clusterStatus.eventsEmittedLabel', defaultMessage: 'Events Emitted' + label: i18n.translate('xpack.monitoring.logstash.clusterStatus.eventsEmittedLabel', { + defaultMessage: 'Events Emitted' }), value: formatMetric(eventsOutTotal, '0.[0]a'), 'data-test-subj': 'events_out_total' @@ -56,5 +56,3 @@ function ClusterStatusUi({ stats, intl }) { /> ); } - -export const ClusterStatus = injectI18n(ClusterStatusUi); diff --git a/x-pack/plugins/monitoring/public/components/logstash/detail_status/index.js b/x-pack/plugins/monitoring/public/components/logstash/detail_status/index.js index 4d96a42e2259a..facdfdbebccdf 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/detail_status/index.js +++ b/x-pack/plugins/monitoring/public/components/logstash/detail_status/index.js @@ -7,9 +7,9 @@ import React from 'react'; import { SummaryStatus } from '../../summary_status'; import { formatMetric } from '../../../lib/format_number'; -import { injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; -function DetailStatusUi({ stats, intl }) { +export function DetailStatus({ stats }) { const { http_address: httpAddress, events, @@ -22,43 +22,43 @@ function DetailStatusUi({ stats, intl }) { const firstMetrics = [ { - label: intl.formatMessage({ - id: 'xpack.monitoring.logstash.detailStatus.transportAddressLabel', defaultMessage: 'Transport Address' + label: i18n.translate('xpack.monitoring.logstash.detailStatus.transportAddressLabel', { + defaultMessage: 'Transport Address' }), value: httpAddress, 'data-test-subj': 'httpAddress' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.logstash.detailStatus.eventsReceivedLabel', defaultMessage: 'Events Received' + label: i18n.translate('xpack.monitoring.logstash.detailStatus.eventsReceivedLabel', { + defaultMessage: 'Events Received' }), value: formatMetric(events.in, '0.[0]a'), 'data-test-subj': 'eventsIn' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.logstash.detailStatus.eventsEmittedLabel', defaultMessage: 'Events Emitted' + label: i18n.translate('xpack.monitoring.logstash.detailStatus.eventsEmittedLabel', { + defaultMessage: 'Events Emitted' }), value: formatMetric(events.out, '0.[0]a'), 'data-test-subj': 'eventsOut' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.logstash.detailStatus.configReloadsLabel', defaultMessage: 'Config Reloads' + label: i18n.translate('xpack.monitoring.logstash.detailStatus.configReloadsLabel', { + defaultMessage: 'Config Reloads' }), value: reloads.successes, 'data-test-subj': 'numReloads' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.logstash.detailStatus.pipelineWorkersLabel', defaultMessage: 'Pipeline Workers' + label: i18n.translate('xpack.monitoring.logstash.detailStatus.pipelineWorkersLabel', { + defaultMessage: 'Pipeline Workers' }), value: pipeline.workers, 'data-test-subj': 'pipelineWorkers' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.logstash.detailStatus.batchSizeLabel', defaultMessage: 'Batch Size' + label: i18n.translate('xpack.monitoring.logstash.detailStatus.batchSizeLabel', { + defaultMessage: 'Batch Size' }), value: pipeline.batch_size, 'data-test-subj': 'pipelineBatchSize' @@ -67,15 +67,15 @@ function DetailStatusUi({ stats, intl }) { const lastMetrics = [ { - label: intl.formatMessage({ - id: 'xpack.monitoring.logstash.detailStatus.versionLabel', defaultMessage: 'Version' + label: i18n.translate('xpack.monitoring.logstash.detailStatus.versionLabel', { + defaultMessage: 'Version' }), value: version, 'data-test-subj': 'version' }, { - label: intl.formatMessage({ - id: 'xpack.monitoring.logstash.detailStatus.uptimeLabel', defaultMessage: 'Uptime' + label: i18n.translate('xpack.monitoring.logstash.detailStatus.uptimeLabel', { + defaultMessage: 'Uptime' }), value: formatMetric(uptime, 'time_since'), 'data-test-subj': 'uptime' @@ -86,8 +86,8 @@ function DetailStatusUi({ stats, intl }) { const metrics = [...firstMetrics]; if (queueType) { metrics.push({ - label: intl.formatMessage({ - id: 'xpack.monitoring.logstash.detailStatus.queueTypeLabel', defaultMessage: 'Queue Type' + label: i18n.translate('xpack.monitoring.logstash.detailStatus.queueTypeLabel', { + defaultMessage: 'Queue Type' }), value: queueType, 'data-test-subj': 'queueType' @@ -102,5 +102,3 @@ function DetailStatusUi({ stats, intl }) { /> ); } - -export const DetailStatus = injectI18n(DetailStatusUi); diff --git a/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js b/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js index 15e72af763913..c1f682c67119d 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js +++ b/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js @@ -10,10 +10,10 @@ import { EuiPage, EuiLink, EuiPageBody, EuiPageContent, EuiPanel, EuiSpacer } fr import { formatPercentageUsage, formatNumber } from '../../../lib/format_number'; import { ClusterStatus } from '..//cluster_status'; import { EuiMonitoringTable } from '../../table'; -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -class ListingUI extends PureComponent { +export class Listing extends PureComponent { getColumns() { const { kbnUrl, scope } = this.props.angular; @@ -111,7 +111,7 @@ class ListingUI extends PureComponent { ]; } render() { - const { data, stats, sorting, pagination, onTableChange, intl } = this.props; + const { data, stats, sorting, pagination, onTableChange } = this.props; const columns = this.getColumns(); const flattenedData = data.map(item => ({ ...item, @@ -146,8 +146,7 @@ class ListingUI extends PureComponent { search={{ box: { incremental: true, - placeholder: intl.formatMessage({ - id: 'xpack.monitoring.logstash.filterNodesPlaceholder', + placeholder: i18n.translate('xpack.monitoring.logstash.filterNodesPlaceholder', { defaultMessage: 'Filter Nodes…' }) }, @@ -163,5 +162,3 @@ class ListingUI extends PureComponent { ); } } - -export const Listing = injectI18n(ListingUI); diff --git a/x-pack/plugins/monitoring/public/components/logstash/listing/listing.test.js b/x-pack/plugins/monitoring/public/components/logstash/listing/listing.test.js index 5530c76d3c126..59642a1f0a6a2 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/listing/listing.test.js +++ b/x-pack/plugins/monitoring/public/components/logstash/listing/listing.test.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { shallow } from 'enzyme'; import { Listing } from './listing'; const expectedData = [ @@ -63,7 +63,7 @@ describe('Listing', () => { } }; - const component = shallowWithIntl(); + const component = shallow(); expect(component.find('EuiMonitoringTable')).toMatchSnapshot(); }); @@ -82,7 +82,7 @@ describe('Listing', () => { } }; - const component = shallowWithIntl(); + const component = shallow(); expect(component.find('EuiMonitoringTable')).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_listing/pipeline_listing.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_listing/pipeline_listing.js index 9a2d12925e833..9dc336f24a40b 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/pipeline_listing/pipeline_listing.js +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_listing/pipeline_listing.js @@ -12,10 +12,9 @@ import { formatMetric } from '../../../lib/format_number'; import { ClusterStatus } from '../cluster_status'; import { Sparkline } from 'plugins/monitoring/components/sparkline'; import { EuiMonitoringTable } from '../../table'; -import { injectI18n } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -class PipelineListingUI extends Component { +export class PipelineListing extends Component { tooltipXValueFormatter(xValue, dateFormat) { return moment(xValue).format(dateFormat); } @@ -139,8 +138,7 @@ class PipelineListingUI extends Component { pagination, onTableChange, upgradeMessage, - className, - intl + className } = this.props; const columns = this.getColumns(); @@ -169,8 +167,7 @@ class PipelineListingUI extends Component { search={{ box: { incremental: true, - placeholder: intl.formatMessage({ - id: 'xpack.monitoring.logstash.filterPipelinesPlaceholder', + placeholder: i18n.translate('xpack.monitoring.logstash.filterPipelinesPlaceholder', { defaultMessage: 'Filter Pipelines…' }) }, @@ -186,5 +183,3 @@ class PipelineListingUI extends Component { ); } } - -export const PipelineListing = injectI18n(PipelineListingUI); diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement.test.js.snap index 306802c8d012b..59c27fc11fcf5 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement.test.js.snap @@ -91,7 +91,7 @@ exports[`Statement component renders a PluginStatement component for plugin mode
- { let pipeline; @@ -43,20 +43,20 @@ describe('PipelineViewer component', () => { }, }; - component = ; + component = ; }); it('passes expected props', () => { - const renderedComponent = shallowWithIntl(component); + const renderedComponent = shallow(component); expect(renderedComponent).toMatchSnapshot(); }); it('renders DetailDrawer when selected vertex is not null', () => { const vertex = { id: 'stdin' }; - component = ; + component = ; - const renderedComponent = shallowWithIntl(component); + const renderedComponent = shallow(component); expect(renderedComponent).toMatchSnapshot(); }); diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/plugin_statement.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/plugin_statement.test.js index 4da97d09678ff..f0e2eecca70f5 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/plugin_statement.test.js +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/plugin_statement.test.js @@ -6,7 +6,7 @@ import React from 'react'; import { PluginStatement } from '../plugin_statement'; -import { shallowWithIntl } from '../../../../../../../../test_utils/enzyme_helpers'; +import { shallow } from 'enzyme'; import { EuiButtonEmpty, EuiBadge } from '@elastic/eui'; @@ -49,7 +49,7 @@ describe('PluginStatement component', () => { }; }); - const render = props => shallowWithIntl(); + const render = props => shallow(); it('renders input metrics and explicit id fields', () => { expect(render(props)).toMatchSnapshot(); diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/pipeline_viewer.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/pipeline_viewer.js index 64970a846f0bf..c88180a85863f 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/pipeline_viewer.js +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/pipeline_viewer.js @@ -9,7 +9,7 @@ import PropTypes from 'prop-types'; import { DetailDrawer } from './detail_drawer'; import { Queue } from './queue'; import { StatementSection } from './statement_section'; -import { injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiPage, @@ -17,7 +17,7 @@ import { EuiPageBody, } from '@elastic/eui'; -class PipelineViewerUi extends React.Component { +export class PipelineViewer extends React.Component { constructor() { super(); this.state = { @@ -48,7 +48,6 @@ class PipelineViewerUi extends React.Component { outputs, queue } = this.props.pipeline; - const { intl } = this.props; return ( @@ -56,7 +55,9 @@ class PipelineViewerUi extends React.Component { @@ -65,14 +66,18 @@ class PipelineViewerUi extends React.Component { @@ -84,7 +89,7 @@ class PipelineViewerUi extends React.Component { } } -PipelineViewerUi.propTypes = { +PipelineViewer.propTypes = { pipeline: PropTypes.shape({ inputs: PropTypes.array.isRequired, filters: PropTypes.array.isRequired, @@ -92,5 +97,3 @@ PipelineViewerUi.propTypes = { queue: PropTypes.object.isRequired, }).isRequired }; - -export const PipelineViewer = injectI18n(PipelineViewerUi); diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/plugin_statement.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/plugin_statement.js index 1e47572aea6e8..217937e4e6784 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/plugin_statement.js +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/plugin_statement.js @@ -14,7 +14,7 @@ import { } from '@elastic/eui'; import { formatMetric } from '../../../../lib/format_number'; import { Metric } from './metric'; -import { injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; function getInputStatementMetrics({ latestEventsPerSecond }) { return [ @@ -65,10 +65,9 @@ function renderPluginStatementMetrics(pluginType, vertex) { : getProcessorStatementMetrics(vertex); } -function PluginStatementUi({ +export function PluginStatement({ statement: { hasExplicitId, id, name, pluginType, vertex }, onShowVertexDetails, - intl, }) { const statementMetrics = renderPluginStatementMetrics(pluginType, vertex); const onNameButtonClick = () => { @@ -101,8 +100,8 @@ function PluginStatementUi({ {id} @@ -120,7 +119,7 @@ function PluginStatementUi({ ); } -PluginStatementUi.propTypes = { +PluginStatement.propTypes = { onShowVertexDetails: PropTypes.func.isRequired, statement: PropTypes.shape({ hasExplicitId: PropTypes.bool.isRequired, @@ -134,5 +133,3 @@ PluginStatementUi.propTypes = { }).isRequired, }).isRequired, }; - -export const PluginStatement = injectI18n(PluginStatementUi); 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 a4ed9a6fa987f..b7c4d4cd03812 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/monitoring/public/components/summary_status/__snapshots__/summary_status.test.js.snap b/x-pack/plugins/monitoring/public/components/summary_status/__snapshots__/summary_status.test.js.snap index 805cf8ee6dc55..fb8e92f7e1905 100644 --- a/x-pack/plugins/monitoring/public/components/summary_status/__snapshots__/summary_status.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/summary_status/__snapshots__/summary_status.test.js.snap @@ -2,6 +2,7 @@ exports[`Summary Status Component should allow label to be optional 1`] = `

Status:

127.0.0.1:9300

@@ -80,7 +81,7 @@ exports[`Summary Status Component should allow label to be optional 1`] = `

24.8k

@@ -92,6 +93,7 @@ exports[`Summary Status Component should allow label to be optional 1`] = ` exports[`Summary Status Component should allow status to be optional 1`] = `

173.9 GB

@@ -139,7 +141,7 @@ exports[`Summary Status Component should allow status to be optional 1`] = `

24.8k

@@ -151,6 +153,7 @@ exports[`Summary Status Component should allow status to be optional 1`] = ` exports[`Summary Status Component should render metrics in a summary bar 1`] = `

Status:

173.9 GB

@@ -231,7 +234,7 @@ exports[`Summary Status Component should render metrics in a summary bar 1`] = `

24.8k

diff --git a/x-pack/plugins/monitoring/public/components/summary_status/_summary_status.scss b/x-pack/plugins/monitoring/public/components/summary_status/_summary_status.scss index 752fb6d922977..4bda98876d651 100644 --- a/x-pack/plugins/monitoring/public/components/summary_status/_summary_status.scss +++ b/x-pack/plugins/monitoring/public/components/summary_status/_summary_status.scss @@ -1,5 +1,13 @@ -.monSummaryStatus { - background-color: $euiColorLightestShade; - border-bottom: $euiBorderThin; - padding: $euiSize; +.monSummaryStatusNoWrap { + margin-left: $euiSizeM; + margin-right: $euiSizeM; + .euiTitle { + overflow-x: hidden; + white-space: nowrap; + @include euiFontSizeXS; + } + + .euiFlexItem { + margin: $euiSizeS; + } } diff --git a/x-pack/plugins/monitoring/public/components/summary_status/summary_status.js b/x-pack/plugins/monitoring/public/components/summary_status/summary_status.js index c5e859f2a790e..1449141057b2f 100644 --- a/x-pack/plugins/monitoring/public/components/summary_status/summary_status.js +++ b/x-pack/plugins/monitoring/public/components/summary_status/summary_status.js @@ -20,7 +20,7 @@ const wrapChild = ({ label, value, ...props }, index) => ( > @@ -67,7 +67,7 @@ const StatusIndicator = ({ status, isOnline, IconComponent }) => { {capitalize(status)} )} - titleSize="s" + titleSize="xs" textAlign="left" description={i18n.translate('xpack.monitoring.summaryStatus.statusDescription', { defaultMessage: 'Status', @@ -79,7 +79,7 @@ const StatusIndicator = ({ status, isOnline, IconComponent }) => { export function SummaryStatus({ metrics, status, isOnline, IconComponent = DefaultIconComponent, ...props }) { return ( -
+
{metrics.map(wrapChild)} diff --git a/x-pack/plugins/monitoring/public/components/table/_table.scss b/x-pack/plugins/monitoring/public/components/table/_table.scss index a9d78d9616210..5d3b0db28c10a 100644 --- a/x-pack/plugins/monitoring/public/components/table/_table.scss +++ b/x-pack/plugins/monitoring/public/components/table/_table.scss @@ -26,13 +26,18 @@ @include euiFontSizeM; } +.monTableCell__status { + overflow-x: hidden; + white-space: nowrap; +} + .monTableCell__transportAddress { color: $euiColorDarkShade; @include euiFontSizeS; } .monTableCell__number { - @include euiFontSizeXL; + @include euiFontSizeL; } .monTableCell__splitNumber { diff --git a/x-pack/plugins/monitoring/public/components/table/table.js b/x-pack/plugins/monitoring/public/components/table/table.js index 1265cf8c487f2..b016e2a59b680 100644 --- a/x-pack/plugins/monitoring/public/components/table/table.js +++ b/x-pack/plugins/monitoring/public/components/table/table.js @@ -22,7 +22,8 @@ import { MonitoringTableToolBar } from './toolbar'; import { MonitoringTableNoData } from './no_data'; import { MonitoringTableFooter } from './footer'; import classNames from 'classnames'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; /* * State and data management for Monitoring Tables @@ -47,7 +48,7 @@ import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; * } * ]; */ -class MonitoringTableUI extends React.Component { +export class MonitoringTable extends React.Component { constructor(props) { super(props); @@ -123,10 +124,12 @@ class MonitoringTableUI extends React.Component { break; default: throw new Error( - this.props.intl.formatMessage({ - id: 'xpack.monitoring.table.unknownTableActionTypeErrorMessage', - defaultMessage: `Unknown table action type {action}! This shouldn't happen!` }, { - action + i18n.translate('xpack.monitoring.table.unknownTableActionTypeErrorMessage', { + defaultMessage: `Unknown table action type {action}! This shouldn't happen!`, + + values: { + action + } }) ); } @@ -464,12 +467,10 @@ const defaultGetNoDataMessage = filterText => { return DEFAULT_NO_DATA_MESSAGE; }; -MonitoringTableUI.defaultProps = { +MonitoringTable.defaultProps = { rows: [], filterFields: [], getNoDataMessage: defaultGetNoDataMessage, alwaysShowPageControls: false, rowsPerPage: 20 }; - -export const MonitoringTable = injectI18n(MonitoringTableUI); diff --git a/x-pack/plugins/monitoring/public/directives/kibana/listing/index.js b/x-pack/plugins/monitoring/public/directives/kibana/listing/index.js index 303ca3fe4a546..382347c01023b 100644 --- a/x-pack/plugins/monitoring/public/directives/kibana/listing/index.js +++ b/x-pack/plugins/monitoring/public/directives/kibana/listing/index.js @@ -23,7 +23,7 @@ import { EuiLink, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { I18nContext } from 'ui/i18n'; const filterFields = [ 'kibana.name', 'kibana.host', 'kibana.status', 'kibana.transport_address' ]; @@ -73,7 +73,7 @@ const instanceRowFactory = (scope, kbnUrl) => { }); }; - return injectI18n(function KibanaRow(props) { + return function KibanaRow(props) { return ( @@ -91,13 +91,12 @@ const instanceRowFactory = (scope, kbnUrl) => {
  @@ -134,11 +133,11 @@ const instanceRowFactory = (scope, kbnUrl) => { ); - }); + }; }; const uiModule = uiModules.get('monitoring/directives', []); -uiModule.directive('monitoringKibanaListing', (kbnUrl, i18n) => { +uiModule.directive('monitoringKibanaListing', kbnUrl => { return { restrict: 'E', scope: { @@ -151,7 +150,7 @@ uiModule.directive('monitoringKibanaListing', (kbnUrl, i18n) => { }, link(scope, $el) { scope.$on('$destroy', () => $el && $el[0] && unmountComponentAtNode($el[0])); - const filterInstancesPlaceholder = i18n('xpack.monitoring.kibana.listing.filterInstancesPlaceholder', { + const filterInstancesPlaceholder = i18n.translate('xpack.monitoring.kibana.listing.filterInstancesPlaceholder', { defaultMessage: 'Filter Instances…' }); diff --git a/x-pack/plugins/monitoring/public/directives/main/index.js b/x-pack/plugins/monitoring/public/directives/main/index.js index b9af69d55c729..555e437622f93 100644 --- a/x-pack/plugins/monitoring/public/directives/main/index.js +++ b/x-pack/plugins/monitoring/public/directives/main/index.js @@ -152,7 +152,7 @@ uiModule.directive('monitoringMain', (breadcrumbs, license, kbnUrl, $injector) = pipelineId: attributes.pipelineId, pipelineHash: attributes.pipelineHash, pipelineVersions: get(scope, 'pageData.versions'), - isCcrEnabled: attributes.isCcrEnabled + isCcrEnabled: attributes.isCcrEnabled === 'true' || attributes.isCcrEnabled === true }, clusterName: get(scope, 'cluster.cluster_name') }; diff --git a/x-pack/plugins/monitoring/public/hacks/toggle_app_link_in_nav.js b/x-pack/plugins/monitoring/public/hacks/toggle_app_link_in_nav.js index c68d0d37b77c5..1df6f545a6e69 100644 --- a/x-pack/plugins/monitoring/public/hacks/toggle_app_link_in_nav.js +++ b/x-pack/plugins/monitoring/public/hacks/toggle_app_link_in_nav.js @@ -5,12 +5,12 @@ */ import { uiModules } from 'ui/modules'; -import { onStart } from 'ui/new_platform'; +import { npStart } from 'ui/new_platform'; uiModules.get('monitoring/hacks').run((monitoringUiEnabled) => { if (monitoringUiEnabled) { return; } - onStart(({ core }) => core.chrome.navLinks.update('monitoring', { hidden: true })); + npStart.core.chrome.navLinks.update('monitoring', { hidden: true }); }); diff --git a/x-pack/plugins/monitoring/public/lib/ajax_error_handler.js b/x-pack/plugins/monitoring/public/lib/ajax_error_handler.js index 8bad9b9221580..004910960d28e 100644 --- a/x-pack/plugins/monitoring/public/lib/ajax_error_handler.js +++ b/x-pack/plugins/monitoring/public/lib/ajax_error_handler.js @@ -40,13 +40,12 @@ export function formatMonitoringError(err) { export function ajaxErrorHandlersProvider($injector) { const kbnUrl = $injector.get('kbnUrl'); - const $window = $injector.get('$window'); return (err) => { if (err.status === 403) { // redirect to error message view kbnUrl.redirect('access-denied'); - } else if (err.status === 404 && !contains($window.location.hash, 'no-data')) { // pass through if this is a 404 and we're already on the no-data page + } else if (err.status === 404 && !contains(window.location.hash, 'no-data')) { // pass through if this is a 404 and we're already on the no-data page toastNotifications.addDanger({ title: ( $window.location.reload()} + onClick={() => window.location.reload()} > { + FeatureCatalogueRegistryProvider.register(() => { return { id: 'monitoring', - title: i18n('xpack.monitoring.monitoringTitle', { + title: i18n.translate('xpack.monitoring.monitoringTitle', { defaultMessage: 'Monitoring' }), - description: i18n('xpack.monitoring.monitoringDescription', { + description: i18n.translate('xpack.monitoring.monitoringDescription', { defaultMessage: 'Track the real-time health and performance of your Elastic Stack.' }), icon: 'monitoringApp', diff --git a/x-pack/plugins/monitoring/public/services/title.js b/x-pack/plugins/monitoring/public/services/title.js index 15e9b0a6c8c4c..f0a603c95e140 100644 --- a/x-pack/plugins/monitoring/public/services/title.js +++ b/x-pack/plugins/monitoring/public/services/title.js @@ -5,18 +5,19 @@ */ import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; import { uiModules } from 'ui/modules'; import { DocTitleProvider } from 'ui/doc_title'; const uiModule = uiModules.get('monitoring/title', []); -uiModule.service('title', (Private, i18n) => { +uiModule.service('title', Private => { const docTitle = Private(DocTitleProvider); return function changeTitle(cluster, suffix) { let clusterName = _.get(cluster, 'cluster_name'); clusterName = (clusterName) ? `- ${clusterName}` : ''; suffix = (suffix) ? `- ${suffix}` : ''; docTitle.change( - i18n('xpack.monitoring.stackMonitoringDocTitle', { + i18n.translate('xpack.monitoring.stackMonitoringDocTitle', { defaultMessage: 'Stack Monitoring {clusterName} {suffix}', values: { clusterName, suffix } }), true); diff --git a/x-pack/plugins/monitoring/public/views/__tests__/base_controller.js b/x-pack/plugins/monitoring/public/views/__tests__/base_controller.js index ed9899183463c..94ce9f890163f 100644 --- a/x-pack/plugins/monitoring/public/views/__tests__/base_controller.js +++ b/x-pack/plugins/monitoring/public/views/__tests__/base_controller.js @@ -8,6 +8,7 @@ import { spy, stub } from 'sinon'; import expect from '@kbn/expect'; import { MonitoringViewBaseController } from '../'; import { timefilter } from 'ui/timefilter'; +import { PromiseWithCancel, Status } from '../../../common/cancel_promise'; /* * Mostly copied from base_table_controller test, with modifications @@ -20,6 +21,7 @@ describe('MonitoringViewBaseController', function () { let opts; let titleService; let executorService; + const httpCall = (ms) => new Promise((resolve) => setTimeout(() => resolve(), ms)); before(() => { titleService = spy(); @@ -36,7 +38,8 @@ describe('MonitoringViewBaseController', function () { $scope = { cluster: { cluster_uuid: 'foo' }, - $on: stub() + $on: stub(), + $apply: stub() }; opts = { @@ -73,17 +76,15 @@ describe('MonitoringViewBaseController', function () { let counter = 0; const opts = { title: 'testo', - getPageData: () => Promise.resolve(++counter), + getPageData: (ms) => httpCall(ms), $injector, $scope }; const ctrl = new MonitoringViewBaseController(opts); - Promise.all([ - ctrl.updateData(), - ctrl.updateData(), - ]).then(() => { - expect(counter).to.be(1); + ctrl.updateData(30).then(() => ++counter); + ctrl.updateData(60).then(() => { + expect(counter).to.be(0); done(); }); }); @@ -140,6 +141,29 @@ describe('MonitoringViewBaseController', function () { expect(timefilter.isTimeRangeSelectorEnabled).to.be(false); expect(timefilter.isAutoRefreshSelectorEnabled).to.be(false); }); + + it('disables timepicker and auto refresh', (done) => { + opts = { + title: 'test', + getPageData: () => httpCall(60), + $injector, + $scope + }; + + ctrl = new MonitoringViewBaseController({ ...opts }); + ctrl.updateDataPromise = new PromiseWithCancel(httpCall(50)); + + let shouldBeFalse = false; + ctrl.updateDataPromise.promise().then(() => (shouldBeFalse = true)); + + const lastUpdateDataPromise = ctrl.updateDataPromise; + + ctrl.updateData().then(() => { + expect(shouldBeFalse).to.be(false); + expect(lastUpdateDataPromise.status()).to.be(Status.Canceled); + done(); + }); + }); }); }); diff --git a/x-pack/plugins/monitoring/public/views/alerts/index.js b/x-pack/plugins/monitoring/public/views/alerts/index.js index 0f329b601d850..a4bea1af11c16 100644 --- a/x-pack/plugins/monitoring/public/views/alerts/index.js +++ b/x-pack/plugins/monitoring/public/views/alerts/index.js @@ -5,6 +5,7 @@ */ import React from 'react'; +import { i18n } from '@kbn/i18n'; import { render } from 'react-dom'; import { find, get } from 'lodash'; import uiRoutes from 'ui/routes'; @@ -51,7 +52,7 @@ uiRoutes.when('/alerts', { }, controllerAs: 'alerts', controller: class AlertsView extends MonitoringViewBaseEuiTableController { - constructor($injector, $scope, i18n) { + constructor($injector, $scope) { const $route = $injector.get('$route'); const globalState = $injector.get('globalState'); const kbnUrl = $injector.get('kbnUrl'); @@ -60,7 +61,7 @@ uiRoutes.when('/alerts', { $scope.cluster = find($route.current.locals.clusters, { cluster_uuid: globalState.cluster_uuid }); super({ - title: i18n('xpack.monitoring.alerts.clusterAlertsTitle', { defaultMessage: 'Cluster Alerts' }), + title: i18n.translate('xpack.monitoring.alerts.clusterAlertsTitle', { defaultMessage: 'Cluster Alerts' }), getPageData, $scope, $injector, diff --git a/x-pack/plugins/monitoring/public/views/apm/instance/index.js b/x-pack/plugins/monitoring/public/views/apm/instance/index.js index ec662d7c96aab..59262c281e754 100644 --- a/x-pack/plugins/monitoring/public/views/apm/instance/index.js +++ b/x-pack/plugins/monitoring/public/views/apm/instance/index.js @@ -11,6 +11,7 @@ */ import React from 'react'; +import { i18n } from '@kbn/i18n'; import moment from 'moment'; import { find, get } from 'lodash'; import uiRoutes from'ui/routes'; @@ -31,7 +32,7 @@ uiRoutes.when('/apm/instances/:uuid', { }, controller: class extends MonitoringViewBaseController { - constructor($injector, $scope, i18n) { + constructor($injector, $scope) { const $route = $injector.get('$route'); const title = $injector.get('title'); const globalState = $injector.get('globalState'); @@ -40,7 +41,7 @@ uiRoutes.when('/apm/instances/:uuid', { }); super({ - title: i18n('xpack.monitoring.apm.instance.routeTitle', { + title: i18n.translate('xpack.monitoring.apm.instance.routeTitle', { defaultMessage: '{apm} - Instance', values: { apm: 'APM' diff --git a/x-pack/plugins/monitoring/public/views/apm/instances/index.js b/x-pack/plugins/monitoring/public/views/apm/instances/index.js index 3a104452076bc..fc75386501fe1 100644 --- a/x-pack/plugins/monitoring/public/views/apm/instances/index.js +++ b/x-pack/plugins/monitoring/public/views/apm/instances/index.js @@ -5,6 +5,7 @@ */ import React from 'react'; +import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; import uiRoutes from'ui/routes'; import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; @@ -22,7 +23,7 @@ uiRoutes.when('/apm/instances', { }, }, controller: class extends MonitoringViewBaseEuiTableController { - constructor($injector, $scope, i18n) { + constructor($injector, $scope) { const $route = $injector.get('$route'); const globalState = $injector.get('globalState'); $scope.cluster = find($route.current.locals.clusters, { @@ -30,7 +31,7 @@ uiRoutes.when('/apm/instances', { }); super({ - title: i18n('xpack.monitoring.apm.instances.routeTitle', { + title: i18n.translate('xpack.monitoring.apm.instances.routeTitle', { defaultMessage: '{apm} - Instances', values: { apm: 'APM' diff --git a/x-pack/plugins/monitoring/public/views/base_controller.js b/x-pack/plugins/monitoring/public/views/base_controller.js index 3c49dfa81f1e3..0d7ad6e4352a4 100644 --- a/x-pack/plugins/monitoring/public/views/base_controller.js +++ b/x-pack/plugins/monitoring/public/views/base_controller.js @@ -11,6 +11,7 @@ import { getPageData } from '../lib/get_page_data'; import { PageLoading } from 'plugins/monitoring/components'; import { timefilter } from 'ui/timefilter'; import { I18nContext } from 'ui/i18n'; +import { PromiseWithCancel } from '../../common/cancel_promise'; /** * Class to manage common instantiation behaviors in a view controller @@ -96,23 +97,21 @@ export class MonitoringViewBaseController { timefilter.enableAutoRefreshSelector(); } - this.updateDataPromise = null; this.updateData = () => { if (this.updateDataPromise) { // Do not sent another request if one is inflight // See https://github.com/elastic/kibana/issues/24082 - return this.updateDataPromise; + this.updateDataPromise.cancel(); + this.updateDataPromise = null; } const _api = apiUrlFn ? apiUrlFn() : api; - return this.updateDataPromise = _getPageData($injector, _api) - .then(pageData => { + this.updateDataPromise = new PromiseWithCancel(_getPageData($injector, _api)); + return this.updateDataPromise.promise().then((pageData) => { + $scope.$apply(() => { this._isDataInitialized = true; // render will replace loading screen with the react component $scope.pageData = this.data = pageData; // update the view's data with the fetch result - this.updateDataPromise = null; - }) - .catch(() => { - this.updateDataPromise = null; }); + }); }; this.updateData(); diff --git a/x-pack/plugins/monitoring/public/views/beats/beat/index.js b/x-pack/plugins/monitoring/public/views/beats/beat/index.js index be764ad7e7f9b..79d12b51afabb 100644 --- a/x-pack/plugins/monitoring/public/views/beats/beat/index.js +++ b/x-pack/plugins/monitoring/public/views/beats/beat/index.js @@ -5,6 +5,7 @@ */ import { find } from 'lodash'; +import { i18n } from '@kbn/i18n'; import uiRoutes from'ui/routes'; import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; import { MonitoringViewBaseController } from '../../'; @@ -22,7 +23,7 @@ uiRoutes.when('/beats/beat/:beatUuid', { }, controllerAs: 'beat', controller: class BeatDetail extends MonitoringViewBaseController { - constructor($injector, $scope, i18n) { + constructor($injector, $scope) { // breadcrumbs + page title const $route = $injector.get('$route'); const globalState = $injector.get('globalState'); @@ -30,7 +31,7 @@ uiRoutes.when('/beats/beat/:beatUuid', { const pageData = $route.current.locals.pageData; super({ - title: i18n('xpack.monitoring.beats.instance.routeTitle', { + title: i18n.translate('xpack.monitoring.beats.instance.routeTitle', { defaultMessage: 'Beats - {instanceName} - Overview', values: { instanceName: pageData.summary.name diff --git a/x-pack/plugins/monitoring/public/views/beats/listing/index.js b/x-pack/plugins/monitoring/public/views/beats/listing/index.js index 7429da7b22cd3..49d7aef9beff6 100644 --- a/x-pack/plugins/monitoring/public/views/beats/listing/index.js +++ b/x-pack/plugins/monitoring/public/views/beats/listing/index.js @@ -5,6 +5,7 @@ */ import { find } from 'lodash'; +import { i18n } from '@kbn/i18n'; import uiRoutes from'ui/routes'; import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; import { MonitoringViewBaseEuiTableController } from '../../'; @@ -25,14 +26,14 @@ uiRoutes.when('/beats/beats', { }, controllerAs: 'beats', controller: class BeatsListing extends MonitoringViewBaseEuiTableController { - constructor($injector, $scope, i18n) { + constructor($injector, $scope) { // breadcrumbs + page title const $route = $injector.get('$route'); const globalState = $injector.get('globalState'); $scope.cluster = find($route.current.locals.clusters, { cluster_uuid: globalState.cluster_uuid }); super({ - title: i18n('xpack.monitoring.beats.routeTitle', { defaultMessage: 'Beats' }), + title: i18n.translate('xpack.monitoring.beats.routeTitle', { defaultMessage: 'Beats' }), storageKey: 'beats.beats', getPageData, reactNodeId: 'monitoringBeatsInstancesApp', diff --git a/x-pack/plugins/monitoring/public/views/beats/overview/index.js b/x-pack/plugins/monitoring/public/views/beats/overview/index.js index 7d06e32469e06..4a251c0c9d27a 100644 --- a/x-pack/plugins/monitoring/public/views/beats/overview/index.js +++ b/x-pack/plugins/monitoring/public/views/beats/overview/index.js @@ -5,6 +5,7 @@ */ import { find } from 'lodash'; +import { i18n } from '@kbn/i18n'; import uiRoutes from'ui/routes'; import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; import { MonitoringViewBaseController } from '../../'; @@ -22,14 +23,14 @@ uiRoutes.when('/beats', { }, controllerAs: 'beats', controller: class BeatsOverview extends MonitoringViewBaseController { - constructor($injector, $scope, i18n) { + constructor($injector, $scope) { // breadcrumbs + page title const $route = $injector.get('$route'); const globalState = $injector.get('globalState'); $scope.cluster = find($route.current.locals.clusters, { cluster_uuid: globalState.cluster_uuid }); super({ - title: i18n('xpack.monitoring.beats.overview.routeTitle', { defaultMessage: 'Beats - Overview' }), + title: i18n.translate('xpack.monitoring.beats.overview.routeTitle', { defaultMessage: 'Beats - Overview' }), getPageData, $scope, $injector diff --git a/x-pack/plugins/monitoring/public/views/cluster/overview/index.js b/x-pack/plugins/monitoring/public/views/cluster/overview/index.js index e8c9e1e067d80..9c854f61fc8b1 100644 --- a/x-pack/plugins/monitoring/public/views/cluster/overview/index.js +++ b/x-pack/plugins/monitoring/public/views/cluster/overview/index.js @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; +import { i18n } from '@kbn/i18n'; import uiRoutes from 'ui/routes'; import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; import template from './index.html'; @@ -24,13 +25,13 @@ uiRoutes.when('/overview', { } }, controller: class extends MonitoringViewBaseController { - constructor($injector, $scope, i18n) { + constructor($injector, $scope) { const kbnUrl = $injector.get('kbnUrl'); const monitoringClusters = $injector.get('monitoringClusters'); const globalState = $injector.get('globalState'); super({ - title: i18n('xpack.monitoring.cluster.overviewTitle', { + title: i18n.translate('xpack.monitoring.cluster.overviewTitle', { defaultMessage: 'Overview' }), defaultData: {}, diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/index.js index 5e8c6d265403d..70db3dfa2415e 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/index.js @@ -5,6 +5,7 @@ */ import React from 'react'; +import { i18n } from '@kbn/i18n'; import uiRoutes from 'ui/routes'; import { getPageData } from './get_page_data'; import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; @@ -24,9 +25,9 @@ uiRoutes.when('/elasticsearch/ccr', { }, controllerAs: 'elasticsearchCcr', controller: class ElasticsearchCcrController extends MonitoringViewBaseController { - constructor($injector, $scope, i18n) { + constructor($injector, $scope) { super({ - title: i18n('xpack.monitoring.elasticsearch.ccr.routeTitle', { + title: i18n.translate('xpack.monitoring.elasticsearch.ccr.routeTitle', { defaultMessage: 'Elasticsearch - Ccr' }), reactNodeId: 'elasticsearchCcrReact', diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js index aa4f89846eee7..8ff67f176c8a8 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js @@ -5,6 +5,7 @@ */ import React from 'react'; +import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; import uiRoutes from 'ui/routes'; import { getPageData } from './get_page_data'; @@ -25,9 +26,9 @@ uiRoutes.when('/elasticsearch/ccr/:index/shard/:shardId', { }, controllerAs: 'elasticsearchCcr', controller: class ElasticsearchCcrController extends MonitoringViewBaseController { - constructor($injector, $scope, pageData, i18n) { + constructor($injector, $scope, pageData) { super({ - title: i18n('xpack.monitoring.elasticsearch.ccr.shard.routeTitle', { + title: i18n.translate('xpack.monitoring.elasticsearch.ccr.shard.routeTitle', { defaultMessage: 'Elasticsearch - Ccr - Shard' }), reactNodeId: 'elasticsearchCcrShardReact', @@ -36,7 +37,7 @@ uiRoutes.when('/elasticsearch/ccr/:index/shard/:shardId', { $injector }); - $scope.instance = i18n('xpack.monitoring.elasticsearch.ccr.shard.instanceTitle', { + $scope.instance = i18n.translate('xpack.monitoring.elasticsearch.ccr.shard.instanceTitle', { defaultMessage: 'Index: {followerIndex} Shard: {shardId}', values: { followerIndex: get(pageData, 'stat.follower_index'), diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js index 48033b1fe7d53..1a8b66de49d27 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js @@ -8,6 +8,7 @@ * Controller for Advanced Index Detail */ import React from 'react'; +import { i18n } from '@kbn/i18n'; import uiRoutes from 'ui/routes'; import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; @@ -51,12 +52,12 @@ uiRoutes.when('/elasticsearch/indices/:index/advanced', { }, controllerAs: 'monitoringElasticsearchAdvancedIndexApp', controller: class extends MonitoringViewBaseController { - constructor($injector, $scope, i18n) { + constructor($injector, $scope) { const $route = $injector.get('$route'); const indexName = $route.current.params.index; super({ - title: i18n('xpack.monitoring.elasticsearch.indices.advanced.routeTitle', { + title: i18n.translate('xpack.monitoring.elasticsearch.indices.advanced.routeTitle', { defaultMessage: 'Elasticsearch - Indices - {indexName} - Advanced', values: { indexName, diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.js index b6569e84c1095..037f5e235ad65 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.js @@ -8,6 +8,7 @@ * Controller for single index detail */ import React from 'react'; +import { i18n } from '@kbn/i18n'; import uiRoutes from 'ui/routes'; import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; @@ -53,13 +54,13 @@ uiRoutes.when('/elasticsearch/indices/:index', { }, controllerAs: 'monitoringElasticsearchIndexApp', controller: class extends MonitoringViewBaseController { - constructor($injector, $scope, i18n) { + constructor($injector, $scope) { const $route = $injector.get('$route'); const kbnUrl = $injector.get('kbnUrl'); const indexName = $route.current.params.index; super({ - title: i18n('xpack.monitoring.elasticsearch.indices.overview.routeTitle', { + title: i18n.translate('xpack.monitoring.elasticsearch.indices.overview.routeTitle', { defaultMessage: 'Elasticsearch - Indices - {indexName} - Overview', values: { indexName, diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.js index 35885f6c4211c..c194a08e8d89f 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.js @@ -5,6 +5,7 @@ */ import React from 'react'; +import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; import uiRoutes from 'ui/routes'; import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; @@ -23,7 +24,7 @@ uiRoutes.when('/elasticsearch/indices', { }, controllerAs: 'elasticsearchIndices', controller: class ElasticsearchIndicesController extends MonitoringViewBaseEuiTableController { - constructor($injector, $scope, i18n) { + constructor($injector, $scope) { const $route = $injector.get('$route'); const globalState = $injector.get('globalState'); const features = $injector.get('features'); @@ -34,7 +35,7 @@ uiRoutes.when('/elasticsearch/indices', { let showSystemIndices = features.isEnabled('showSystemIndices', false); super({ - title: i18n('xpack.monitoring.elasticsearch.indices.routeTitle', { + title: i18n.translate('xpack.monitoring.elasticsearch.indices.routeTitle', { defaultMessage: 'Elasticsearch - Indices' }), storageKey: 'elasticsearch.indices', diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.html b/x-pack/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.html index b20c927841d4f..6fdae46b6b6ed 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.html +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.html @@ -1,4 +1,4 @@ - + { }); }; -function makeUpgradeMessage(logstashVersion, i18n) { +function makeUpgradeMessage(logstashVersion) { if (isPipelineMonitoringSupportedInVersion(logstashVersion)) { return null; } - return i18n('xpack.monitoring.logstash.node.pipelines.notAvailableDescription', { + return i18n.translate('xpack.monitoring.logstash.node.pipelines.notAvailableDescription', { defaultMessage: 'Pipeline monitoring is only available in Logstash version 6.0.0 or higher. This node is running version {logstashVersion}.', values: { @@ -71,7 +72,7 @@ uiRoutes pageData: getPageData }, controller: class extends MonitoringViewBaseEuiTableController { - constructor($injector, $scope, i18n) { + constructor($injector, $scope) { const kbnUrl = $injector.get('kbnUrl'); const config = $injector.get('config'); @@ -88,7 +89,7 @@ uiRoutes return; } - this.setTitle(i18n('xpack.monitoring.logstash.node.pipelines.routeTitle', { + this.setTitle(i18n.translate('xpack.monitoring.logstash.node.pipelines.routeTitle', { defaultMessage: 'Logstash - {nodeName} - Pipelines', values: { nodeName: data.nodeSummary.name diff --git a/x-pack/plugins/monitoring/public/views/logstash/pipeline/index.js b/x-pack/plugins/monitoring/public/views/logstash/pipeline/index.js index 9ff09d731c48a..75c41f29f30e2 100644 --- a/x-pack/plugins/monitoring/public/views/logstash/pipeline/index.js +++ b/x-pack/plugins/monitoring/public/views/logstash/pipeline/index.js @@ -99,12 +99,12 @@ uiRoutes.when('/logstash/pipelines/:id/:hash?', { pageData: getPageData }, controller: class extends MonitoringViewBaseController { - constructor($injector, $scope, i18n) { + constructor($injector, $scope) { const config = $injector.get('config'); const dateFormat = config.get('dateFormat'); super({ - title: i18n('xpack.monitoring.logstash.pipeline.routeTitle', { + title: i18n.translate('xpack.monitoring.logstash.pipeline.routeTitle', { defaultMessage: 'Logstash - Pipeline' }), storageKey: 'logstash.pipelines', diff --git a/x-pack/plugins/monitoring/public/views/logstash/pipelines/index.js b/x-pack/plugins/monitoring/public/views/logstash/pipelines/index.js index 044f0dc2434b9..4e393e62528ab 100644 --- a/x-pack/plugins/monitoring/public/views/logstash/pipelines/index.js +++ b/x-pack/plugins/monitoring/public/views/logstash/pipelines/index.js @@ -5,6 +5,7 @@ */ import React from 'react'; +import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; import { render } from 'react-dom'; import uiRoutes from 'ui/routes'; @@ -67,7 +68,7 @@ uiRoutes pageData: getPageData }, controller: class LogstashPipelinesList extends MonitoringViewBaseEuiTableController { - constructor($injector, $scope, i18n) { + constructor($injector, $scope) { super({ title: 'Logstash Pipelines', storageKey: 'logstash.pipelines', diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/__tests__/bulk_uploader.js b/x-pack/plugins/monitoring/server/kibana_monitoring/__tests__/bulk_uploader.js index fe5a3f51dff46..5e2adbb2d65ab 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/__tests__/bulk_uploader.js +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/__tests__/bulk_uploader.js @@ -20,6 +20,9 @@ class MockCollectorSet { isUsageCollector(x) { return !!x.isUsageCollector; } + areAllCollectorsReady() { + return this.mockCollectors.every(collector => collector.isReady()); + } getCollectorByType(type) { return this.mockCollectors.find(collector => collector.type === type) || this.mockCollectors[0]; } @@ -29,6 +32,9 @@ class MockCollectorSet { async bulkFetch() { return this.mockCollectors.map(({ fetch }) => fetch()); } + some(someFn) { + return this.mockCollectors.some(someFn); + } } describe('BulkUploader', () => { @@ -61,6 +67,7 @@ describe('BulkUploader', () => { { type: 'type_collector_test', fetch: noop, // empty payloads, + isReady: () => true, formatForBulkUpload: result => result, } ]); @@ -94,10 +101,56 @@ describe('BulkUploader', () => { }, CHECK_DELAY); }); + it('should not upload if some collectors are not ready', done => { + const collectors = new MockCollectorSet(server, [ + { + type: 'type_collector_test', + fetch: noop, // empty payloads, + isReady: () => false, + formatForBulkUpload: result => result, + }, + { + type: 'type_collector_test2', + fetch: noop, // empty payloads, + isReady: () => true, + formatForBulkUpload: result => result, + } + ]); + + const uploader = new BulkUploader(server, { + interval: FETCH_INTERVAL + }); + + uploader.start(collectors); + + // allow interval to tick a few times + setTimeout(() => { + uploader.stop(); + + const loggingCalls = server.log.getCalls(); + expect(loggingCalls.length).to.be.greaterThan(2); // should be 3-5: start, fetch, skip, fetch, skip + expect(loggingCalls[0].args).to.eql([ + ['info', 'monitoring', 'kibana-monitoring'], + 'Starting monitoring stats collection', + ]); + expect(loggingCalls[1].args).to.eql([ + ['debug', 'monitoring', 'kibana-monitoring'], + 'Skipping bulk uploading because not all collectors are ready', + ]); + expect(loggingCalls[loggingCalls.length - 1].args).to.eql([ + ['info', 'monitoring', 'kibana-monitoring'], + 'Monitoring stats collection is stopped', + ]); + + done(); + }, CHECK_DELAY); + }); + it('should run the bulk upload handler', done => { const collectors = new MockCollectorSet(server, [ { fetch: () => ({ type: 'type_collector_test', result: { testData: 12345 } }), + isReady: () => true, formatForBulkUpload: result => result } ]); @@ -135,11 +188,13 @@ describe('BulkUploader', () => { const collectors = new MockCollectorSet(server, [ { fetch: usageCollectorFetch, + isReady: () => true, formatForBulkUpload: result => result, isUsageCollector: true, }, { fetch: collectorFetch, + isReady: () => true, formatForBulkUpload: result => result, isUsageCollector: false, } @@ -166,11 +221,13 @@ describe('BulkUploader', () => { const collectors = new MockCollectorSet(server, [ { fetch: usageCollectorFetch, + isReady: () => true, formatForBulkUpload: result => result, isUsageCollector: true, }, { fetch: collectorFetch, + isReady: () => true, formatForBulkUpload: result => result, isUsageCollector: false, } diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js b/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js index b16ca429f3407..ea4f8e4c46144 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js @@ -69,7 +69,6 @@ export class BulkUploader { this._log.info('Starting monitoring stats collection'); const filterCollectorSet = _collectorSet => { const filterUsage = this._lastFetchUsageTime && this._lastFetchUsageTime + this._usageInterval > Date.now(); - this._lastFetchWithUsage = !filterUsage; if (!filterUsage) { this._lastFetchUsageTime = Date.now(); } @@ -123,6 +122,16 @@ export class BulkUploader { * @return {Promise} - resolves to undefined */ async _fetchAndUpload(collectorSet) { + const collectorsReady = await collectorSet.areAllCollectorsReady(); + if (!collectorsReady) { + this._log.debug('Skipping bulk uploading because not all collectors are ready'); + if (collectorSet.some(collectorSet.isUsageCollector)) { + this._lastFetchUsageTime = null; + this._log.debug('Resetting lastFetchWithUsage because not all collectors are ready'); + } + return; + } + const data = await collectorSet.bulkFetch(this._callClusterWithInternalUser); const payload = this.toBulkUploadFormat(compact(data), collectorSet); diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_kibana_usage_collector.js b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_kibana_usage_collector.js index a81db5aca586a..96d0c98cf3f05 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_kibana_usage_collector.js +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_kibana_usage_collector.js @@ -23,7 +23,7 @@ export function getKibanaUsageCollector(server) { const { collectorSet } = server.usage; return collectorSet.makeUsageCollector({ type: KIBANA_USAGE_TYPE, - + isReady: () => true, async fetch(callCluster) { const index = server.config().get('kibana.index'); const savedObjectCountSearchParams = { diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_ops_stats_collector.js b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_ops_stats_collector.js index 0c202b1c9e365..b5116de7f3d49 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_ops_stats_collector.js +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_ops_stats_collector.js @@ -13,6 +13,7 @@ import { opsBuffer } from './ops_buffer'; import Oppsy from 'oppsy'; import { cloneDeep } from 'lodash'; +let bufferHadEvents = false; class OpsMonitor { constructor(server, buffer, interval) { @@ -80,6 +81,12 @@ export function getOpsStatsCollector(server, kbnServer) { return collectorSet.makeStatsCollector({ type: KIBANA_STATS_TYPE_MONITORING, init: opsMonitor.start, + isReady: () => { + if (!bufferHadEvents) { + bufferHadEvents = buffer.hasEvents(); + } + return bufferHadEvents; + }, fetch: async () => { return await buffer.flush(); } diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.js b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.js index 4b9fb5a51efb3..11129fe94b579 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.js +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.js @@ -86,6 +86,7 @@ export function getSettingsCollector(server) { return collectorSet.makeStatsCollector({ type: KIBANA_SETTINGS_TYPE, + isReady: () => true, async fetch(callCluster) { let kibanaSettingsData; const defaultAdminEmail = await checkForEmailValue(config, callCluster, this.log); diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/ops_buffer/event_roller.js b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/ops_buffer/event_roller.js index 4f88bd0b75093..4b25fd9b706cf 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/ops_buffer/event_roller.js +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/ops_buffer/event_roller.js @@ -22,6 +22,10 @@ export class EventRoller { return get(this.rollup, path); } + hasEvents() { + return this.rollup !== null; + } + rollupEvent(event) { const heapStats = v8.getHeapStatistics(); const requests = mapRequests(event.requests); diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/ops_buffer/ops_buffer.js b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/ops_buffer/ops_buffer.js index b8c670edf1297..457213c784ad6 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/ops_buffer/ops_buffer.js +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/ops_buffer/ops_buffer.js @@ -29,6 +29,10 @@ export function opsBuffer(server) { server.log(['debug', LOGGING_TAG, KIBANA_MONITORING_LOGGING_TAG], 'Received Kibana Ops event data'); }, + hasEvents() { + return eventRoller.hasEvents(); + }, + async flush() { let cloud; // a property that will be left out of the result if the details are undefined const cloudDetails = cloudDetector.getCloudDetails(); diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/ccr.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/ccr.js index 3bef2398b8928..4fd2437954d82 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/ccr.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/ccr.js @@ -28,7 +28,6 @@ export async function checkCcrEnabled(req, esIndexPattern) { const params = { index: esIndexPattern, size: 1, - terminate_after: 1, ignoreUnavailable: true, body: { query: createQuery({ diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/__test__/__snapshots__/get_metric_aggs.test.js.snap b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/__test__/__snapshots__/get_metric_aggs.test.js.snap index b5f8ad2b8b84b..8b5b445b93111 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/__test__/__snapshots__/get_metric_aggs.test.js.snap +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/__test__/__snapshots__/get_metric_aggs.test.js.snap @@ -18,7 +18,7 @@ Object { }, "date_histogram": Object { "field": "timestamp", - "interval": "30s", + "fixed_interval": "30s", "min_doc_count": 1, }, }, @@ -38,7 +38,7 @@ Object { }, "date_histogram": Object { "field": "timestamp", - "interval": "30s", + "fixed_interval": "30s", "min_doc_count": 1, }, }, @@ -76,7 +76,7 @@ Object { }, "date_histogram": Object { "field": "timestamp", - "interval": "30s", + "fixed_interval": "30s", "min_doc_count": 1, }, }, @@ -109,7 +109,7 @@ Object { }, "date_histogram": Object { "field": "timestamp", - "interval": "30s", + "fixed_interval": "30s", "min_doc_count": 1, }, }, diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_metric_aggs.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_metric_aggs.js index 9f135ae1da382..ff2d0470968b1 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_metric_aggs.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_metric_aggs.js @@ -47,7 +47,7 @@ export function getMetricAggs(listingMetrics, bucketSize) { date_histogram: { field: 'timestamp', min_doc_count: 1, - interval: bucketSize + 's' + fixed_interval: bucketSize + 's' }, aggs: metric.aggs || metricAgg }; diff --git a/x-pack/plugins/monitoring/server/lib/logs/detect_reason.js b/x-pack/plugins/monitoring/server/lib/logs/detect_reason.js index 926a38a555b7f..168135d92b0d3 100644 --- a/x-pack/plugins/monitoring/server/lib/logs/detect_reason.js +++ b/x-pack/plugins/monitoring/server/lib/logs/detect_reason.js @@ -88,7 +88,6 @@ async function doesFilebeatIndexExist(req, filebeatIndexPattern, { start, end, c const defaultParams = { size: 0, - terminate_after: 1, }; const body = [ diff --git a/x-pack/plugins/monitoring/server/lib/metrics/apm/classes.js b/x-pack/plugins/monitoring/server/lib/metrics/apm/classes.js index e827b4af4a1bc..f905615bd8396 100644 --- a/x-pack/plugins/monitoring/server/lib/metrics/apm/classes.js +++ b/x-pack/plugins/monitoring/server/lib/metrics/apm/classes.js @@ -62,14 +62,14 @@ export class ApmCpuUtilizationMetric extends ApmMetric { bucketSizeInSeconds ) => { if (metricDeriv) { - const { normalized_value: metricDerivNormalizedValue } = metricDeriv; + const { value: metricDerivValue } = metricDeriv; const bucketSizeInMillis = bucketSizeInSeconds * 1000; if ( - metricDerivNormalizedValue >= 0 && - metricDerivNormalizedValue !== null + metricDerivValue >= 0 && + metricDerivValue !== null ) { - return metricDerivNormalizedValue / bucketSizeInMillis * 100; + return metricDerivValue / bucketSizeInMillis * 100; } } return null; diff --git a/x-pack/plugins/oss_telemetry/server/lib/collectors/visualizations/get_usage_collector.ts b/x-pack/plugins/oss_telemetry/server/lib/collectors/visualizations/get_usage_collector.ts index 159ca7eec8063..8976cffc8ea40 100644 --- a/x-pack/plugins/oss_telemetry/server/lib/collectors/visualizations/get_usage_collector.ts +++ b/x-pack/plugins/oss_telemetry/server/lib/collectors/visualizations/get_usage_collector.ts @@ -8,30 +8,55 @@ import { get } from 'lodash'; import { HapiServer } from '../../../../'; import { PLUGIN_ID, VIS_TELEMETRY_TASK, VIS_USAGE_TYPE } from '../../../../constants'; -export function getUsageCollector(server: HapiServer) { +async function isTaskManagerReady(server: HapiServer) { + const result = await fetch(server); + return result !== null; +} + +async function fetch(server: HapiServer) { const { taskManager } = server; + + let docs; + try { + ({ docs } = await taskManager.fetch({ + query: { bool: { filter: { term: { _id: `${PLUGIN_ID}-${VIS_TELEMETRY_TASK}` } } } }, + })); + } catch (err) { + const errMessage = err && err.message ? err.message : err.toString(); + /* + The usage service WILL to try to fetch from this collector before the task manager has been initialized, because the task manager has to wait for all plugins to initialize first. It's fine to ignore it as next time around it will be initialized (or it will throw a different type of error) + */ + if (errMessage.includes('NotInitialized')) { + docs = null; + } else { + throw err; + } + } + + return docs; +} + +export function getUsageCollector(server: HapiServer) { + let isCollectorReady = false; + async function determineIfTaskManagerIsReady() { + let isReady = false; + try { + isReady = await isTaskManagerReady(server); + } catch (err) {} // eslint-disable-line + + if (isReady) { + isCollectorReady = true; + } else { + setTimeout(determineIfTaskManagerIsReady, 500); + } + } + determineIfTaskManagerIsReady(); + return { type: VIS_USAGE_TYPE, + isReady: () => isCollectorReady, fetch: async () => { - let docs; - try { - ({ docs } = await taskManager.fetch({ - query: { bool: { filter: { term: { _id: `${PLUGIN_ID}-${VIS_TELEMETRY_TASK}` } } } }, - })); - } catch (err) { - const errMessage = err && err.message ? err.message : err.toString(); - /* - * The usage service WILL to try to fetch from this collector before the task manager has been initialized, because the task manager - * has to wait for all plugins to initialize first. - * It's fine to ignore it as next time around it will be initialized (or it will throw a different type of error) - */ - if (errMessage.includes('NotInitialized')) { - docs = {}; - } else { - throw err; - } - } - + const docs = await fetch(server); // get the accumulated state from the recurring task return get(docs, '[0].state.stats'); }, diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.js index e528c60ca5fdc..957c61c316c18 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.js @@ -5,13 +5,14 @@ */ import axios from 'axios'; +import axiosXhrAdapter from 'axios/lib/adapters/xhr'; import { setHttpClient } from '../../../public/services/api'; import { init as initHttpRequests } from './http_requests'; export const setupEnvironment = () => { // axios has a $http like interface so using it to simulate $http - setHttpClient(axios.create()); + setHttpClient(axios.create({ adapter: axiosXhrAdapter })); const { server, httpRequestsMockHelpers } = initHttpRequests(); 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 dd897ac82965e..52354124e9ec5 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" > - - + /> - - + />

} - body={

{message}

} + body={message &&

{message}

} actions={ - - {actionLabel} - + + + + {actionPrimaryLabel} + + + + {actionSecondaryLabel && actionSecondaryUrl && ( + + + {actionSecondaryLabel} + + + )} + } {...rest} /> ) ); - -const CenteredEmptyPrompt = styled(EuiEmptyPrompt)` - align-self: center; -`; diff --git a/x-pack/plugins/siem/public/components/error_toast/index.test.tsx b/x-pack/plugins/siem/public/components/error_toast/index.test.tsx index d38f07a87b422..52aad5a72011e 100644 --- a/x-pack/plugins/siem/public/components/error_toast/index.test.tsx +++ b/x-pack/plugins/siem/public/components/error_toast/index.test.tsx @@ -9,17 +9,18 @@ import toJson from 'enzyme-to-json'; import * as React from 'react'; import { Provider } from 'react-redux'; -import { mockGlobalState } from '../../mock'; -import { createStore, State } from '../../store'; +import { apolloClientObservable, mockGlobalState } from '../../mock'; +import { createStore } from '../../store/store'; import { ErrorToast } from '.'; +import { State } from '../../store/reducer'; describe('Error Toast', () => { const state: State = mockGlobalState; - let store = createStore(state); + let store = createStore(state, apolloClientObservable); beforeEach(() => { - store = createStore(state); + store = createStore(state, apolloClientObservable); }); describe('rendering', () => { diff --git a/x-pack/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap b/x-pack/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap index b5494feca161a..f69bc85d5ed4b 100644 --- a/x-pack/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap +++ b/x-pack/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap @@ -3,6 +3,391 @@ exports[`EventDetails rendering should match snapshot 1`] = ` diff --git a/x-pack/plugins/siem/public/components/event_details/columns.tsx b/x-pack/plugins/siem/public/components/event_details/columns.tsx index b7e2a3b49d8ab..b14456dd5e527 100644 --- a/x-pack/plugins/siem/public/components/event_details/columns.tsx +++ b/x-pack/plugins/siem/public/components/event_details/columns.tsx @@ -4,19 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Draggable } from 'react-beautiful-dnd'; import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiPanel, EuiToolTip } from '@elastic/eui'; import * as React from 'react'; import styled from 'styled-components'; +import { BrowserFields } from '../../containers/source'; +import { DragEffects } from '../drag_and_drop/draggable_wrapper'; import { DefaultDraggable } from '../draggables'; import { DetailItem, ToStringArray } from '../../graphql/types'; +import { DroppableWrapper } from '../drag_and_drop/droppable_wrapper'; +import { DraggableFieldBadge } from '../draggables/field_badge'; import { FormattedFieldValue } from '../timeline/body/renderers/formatted_field'; -import { getIconFromType, getExampleText } from './helpers'; +import { FieldName } from '../fields_browser/field_name'; +import { getIconFromType, getExampleText, getColumnsWithTimestamp } from './helpers'; +import { getDroppableId, getDraggableFieldId, DRAG_TYPE_FIELD } from '../drag_and_drop/helpers'; +import { OnUpdateColumns } from '../timeline/events'; import { SelectableText } from '../selectable_text'; import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard'; import { WithHoverActions } from '../with_hover_actions'; import * as i18n from './translations'; +import { OverflowField } from '../tables/helpers'; +import { DATE_FIELD_TYPE, MESSAGE_FIELD_NAME } from '../timeline/body/renderers/constants'; +import { EVENT_DURATION_FIELD_NAME } from '../duration'; const HoverActionsContainer = styled(EuiPanel)` align-items: center; @@ -30,7 +41,24 @@ const HoverActionsContainer = styled(EuiPanel)` width: 30px; `; -export const getColumns = (eventId: string) => [ +const FieldTypeIcon = styled(EuiIcon)` + position: relative; + top: -2px; +`; + +export const getColumns = ({ + browserFields, + eventId, + isLoading, + onUpdateColumns, + timelineId, +}: { + browserFields: BrowserFields; + eventId: string; + isLoading: boolean; + onUpdateColumns: OnUpdateColumns; + timelineId: string; +}) => [ { field: 'type', name: '', @@ -39,7 +67,7 @@ export const getColumns = (eventId: string) => [ width: '30px', render: (type: string) => ( - + ), }, @@ -48,17 +76,48 @@ export const getColumns = (eventId: string) => [ name: i18n.FIELD, sortable: true, truncateText: false, - render: (field: string) => ( - - - - - - } - render={() => {field}} - /> + render: (field: string, data: DetailItem) => ( + + + {(provided, snapshot) => ( +
+ {!snapshot.isDragging ? ( + + ) : ( + + + + )} +
+ )} +
+
), }, { @@ -79,27 +138,35 @@ export const getColumns = (eventId: string) => [ hoverContent={ - + } - render={() => ( - - + data.field === MESSAGE_FIELD_NAME ? ( + + ) : ( + - - )} + > + + + ) + } />
))} @@ -110,10 +177,17 @@ export const getColumns = (eventId: string) => [ field: 'description', name: i18n.DESCRIPTION, render: (description: string | null | undefined, data: DetailItem) => ( - {`${description || ''} ${getExampleText(data)}`} + {`${description || ''} ${getExampleText(data.example)}`} ), sortable: true, truncateText: true, width: '50%', }, + { + field: 'valuesConcatenated', + sortable: false, + truncateText: true, + render: () => null, + width: '1px', + }, ]; diff --git a/x-pack/plugins/siem/public/components/event_details/event_details.test.tsx b/x-pack/plugins/siem/public/components/event_details/event_details.test.tsx index de64eebb2a082..78df9ab938690 100644 --- a/x-pack/plugins/siem/public/components/event_details/event_details.test.tsx +++ b/x-pack/plugins/siem/public/components/event_details/event_details.test.tsx @@ -12,6 +12,7 @@ import { mockDetailItemData, mockDetailItemDataId } from '../../mock/mock_detail import { TestProviders } from '../../mock/test_providers'; import { EventDetails } from './event_details'; +import { mockBrowserFields } from '../../containers/source/mock'; describe('EventDetails', () => { describe('rendering', () => { @@ -19,10 +20,14 @@ describe('EventDetails', () => { const wrapper = shallow( ); @@ -36,10 +41,14 @@ describe('EventDetails', () => { const wrapper = mount( ); @@ -57,10 +66,14 @@ describe('EventDetails', () => { const wrapper = mount( ); diff --git a/x-pack/plugins/siem/public/components/event_details/event_details.tsx b/x-pack/plugins/siem/public/components/event_details/event_details.tsx index 3c346e568d8c4..11169810c223a 100644 --- a/x-pack/plugins/siem/public/components/event_details/event_details.tsx +++ b/x-pack/plugins/siem/public/components/event_details/event_details.tsx @@ -9,7 +9,9 @@ import * as React from 'react'; import { pure } from 'recompose'; import styled from 'styled-components'; +import { BrowserFields } from '../../containers/source'; import { DetailItem } from '../../graphql/types'; +import { OnUpdateColumns } from '../timeline/events'; import { EventFieldsBrowser } from './event_fields_browser'; import { JsonView } from './json_view'; @@ -18,10 +20,14 @@ import * as i18n from './translations'; export type View = 'table-view' | 'json-view'; interface Props { + browserFields: BrowserFields; data: DetailItem[]; id: string; + isLoading: boolean; view: View; + onUpdateColumns: OnUpdateColumns; onViewSelected: (selected: View) => void; + timelineId: string; } const Details = styled.div` @@ -29,27 +35,38 @@ const Details = styled.div` width: 100%; `; -export const EventDetails = pure(({ data, id, view, onViewSelected }) => { - const tabs: EuiTabbedContentTab[] = [ - { - id: 'table-view', - name: i18n.TABLE, - content: , - }, - { - id: 'json-view', - name: i18n.JSON_VIEW, - content: , - }, - ]; - - return ( -
- onViewSelected(e.id as View)} - /> -
- ); -}); +export const EventDetails = pure( + ({ browserFields, data, id, isLoading, view, onUpdateColumns, onViewSelected, timelineId }) => { + const tabs: EuiTabbedContentTab[] = [ + { + id: 'table-view', + name: i18n.TABLE, + content: ( + + ), + }, + { + id: 'json-view', + name: i18n.JSON_VIEW, + content: , + }, + ]; + + return ( +
+ onViewSelected(e.id as View)} + /> +
+ ); + } +); diff --git a/x-pack/plugins/siem/public/components/event_details/event_fields_browser.test.tsx b/x-pack/plugins/siem/public/components/event_details/event_fields_browser.test.tsx index 6403ce2fd234e..2a38e0816dd12 100644 --- a/x-pack/plugins/siem/public/components/event_details/event_fields_browser.test.tsx +++ b/x-pack/plugins/siem/public/components/event_details/event_fields_browser.test.tsx @@ -11,6 +11,7 @@ import { mockDetailItemData, mockDetailItemDataId } from '../../mock/mock_detail import { TestProviders } from '../../mock/test_providers'; import { EventFieldsBrowser } from './event_fields_browser'; +import { mockBrowserFields } from '../../containers/source/mock'; describe('EventFieldsBrowser', () => { describe('column headers', () => { @@ -18,7 +19,14 @@ describe('EventFieldsBrowser', () => { test(`it renders the ${header} column header`, () => { const wrapper = mountWithIntl( - + ); @@ -31,7 +39,14 @@ describe('EventFieldsBrowser', () => { test('it renders a filter input with the expected placeholder', () => { const wrapper = mountWithIntl( - + ); @@ -45,7 +60,14 @@ describe('EventFieldsBrowser', () => { test('it renders the expected icon type for the data provided', () => { const wrapper = mountWithIntl( - + ); @@ -64,16 +86,22 @@ describe('EventFieldsBrowser', () => { test('it renders the field name for the data provided', () => { const wrapper = mountWithIntl( - + ); expect( wrapper - .find('.euiTableRow') - .find('.euiTableRowCell') - .at(1) - .containsMatchingElement(_id) - ).toEqual(true); + .find('[data-test-subj="field-name"]') + .at(0) + .text() + ).toEqual('_id'); }); }); @@ -81,7 +109,14 @@ describe('EventFieldsBrowser', () => { test('it renders the expected value for the data provided', () => { const wrapper = mountWithIntl( - + ); expect( @@ -97,7 +132,14 @@ describe('EventFieldsBrowser', () => { test('it renders the expected field description the data provided', () => { const wrapper = mountWithIntl( - + ); diff --git a/x-pack/plugins/siem/public/components/event_details/event_fields_browser.tsx b/x-pack/plugins/siem/public/components/event_details/event_fields_browser.tsx index 114f1e6b6d483..c10cb09d51ca1 100644 --- a/x-pack/plugins/siem/public/components/event_details/event_fields_browser.tsx +++ b/x-pack/plugins/siem/public/components/event_details/event_fields_browser.tsx @@ -11,23 +11,40 @@ import { import * as React from 'react'; import { pure } from 'recompose'; +import { BrowserFields } from '../../containers/source'; import { DetailItem } from '../../graphql/types'; +import { OnUpdateColumns } from '../timeline/events'; import { getColumns } from './columns'; import { search } from './helpers'; interface Props { + browserFields: BrowserFields; data: DetailItem[]; eventId: string; + isLoading: boolean; + onUpdateColumns: OnUpdateColumns; + timelineId: string; } /** Renders a table view or JSON view of the `ECS` `data` */ -export const EventFieldsBrowser = pure(({ data, eventId }) => ( - -)); +export const EventFieldsBrowser = pure( + ({ browserFields, data, eventId, isLoading, onUpdateColumns, timelineId }) => ( + ({ + ...item, + valuesConcatenated: item.values != null ? item.values.join() : '', + }))} + columns={getColumns({ + browserFields, + eventId, + isLoading, + onUpdateColumns, + timelineId, + })} + pagination={false} + search={search} + sorting={true} + /> + ) +); diff --git a/x-pack/plugins/siem/public/components/event_details/helpers.test.tsx b/x-pack/plugins/siem/public/components/event_details/helpers.test.tsx index 2399e9178e23a..2104a331198dd 100644 --- a/x-pack/plugins/siem/public/components/event_details/helpers.test.tsx +++ b/x-pack/plugins/siem/public/components/event_details/helpers.test.tsx @@ -13,7 +13,7 @@ const aField = mockDetailItemData[0]; describe('helpers', () => { describe('getExampleText', () => { test('it returns the expected example text when the field contains an example', () => { - expect(getExampleText(aField)).toEqual('Example: Y-6TfmcB0WOhS6qyMv3s'); + expect(getExampleText(aField.example)).toEqual('Example: Y-6TfmcB0WOhS6qyMv3s'); }); test(`it returns an empty string when the field's example is an empty string`, () => { @@ -22,7 +22,7 @@ describe('helpers', () => { example: '', }; - expect(getExampleText(fieldWithEmptyExample)).toEqual(''); + expect(getExampleText(fieldWithEmptyExample.example)).toEqual(''); }); }); diff --git a/x-pack/plugins/siem/public/components/event_details/helpers.tsx b/x-pack/plugins/siem/public/components/event_details/helpers.tsx index f4fff4c77e8fa..49486ea032481 100644 --- a/x-pack/plugins/siem/public/components/event_details/helpers.tsx +++ b/x-pack/plugins/siem/public/components/event_details/helpers.tsx @@ -4,9 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEmpty } from 'lodash/fp'; +import { get, getOr, isEmpty, uniqBy } from 'lodash/fp'; -import { DetailItem, ToStringArray } from '../../graphql/types'; +import { BrowserField, BrowserFields } from '../../containers/source'; +import { ColumnHeader } from '../timeline/body/column_headers/column_header'; +import { DEFAULT_DATE_COLUMN_MIN_WIDTH, DEFAULT_COLUMN_MIN_WIDTH } from '../timeline/body/helpers'; +import { ToStringArray } from '../../graphql/types'; import * as i18n from './translations'; @@ -18,10 +21,10 @@ export const search = { incremental: true, placeholder: i18n.PLACEHOLDER, schema: { - field: { + fieldId: { type: 'string', }, - value: { + valuesFlattened: { type: 'string', }, description: { @@ -40,20 +43,69 @@ export interface ItemValues { * An item rendered in the table */ export interface Item { - field: string; description: string; + field: JSX.Element; + fieldId: string; type: string; values: ToStringArray; } +export const getColumnHeaderFromBrowserField = ({ + browserField, + width = DEFAULT_COLUMN_MIN_WIDTH, +}: { + browserField: Partial; + width?: number; +}): ColumnHeader => ({ + category: browserField.category, + columnHeaderType: 'not-filtered', + description: browserField.description != null ? browserField.description : undefined, + example: browserField.example != null ? `${browserField.example}` : undefined, + id: browserField.name || '', + type: browserField.type, + aggregatable: browserField.aggregatable, + width, +}); + +/** + * Returns a collection of columns, where the first column in the collection + * is a timestamp, and the remaining columns are all the columns in the + * specified category + */ +export const getColumnsWithTimestamp = ({ + browserFields, + category, +}: { + browserFields: BrowserFields; + category: string; +}): ColumnHeader[] => { + const emptyFields: Record> = {}; + const timestamp = get('base.fields.@timestamp', browserFields); + const categoryFields: Array> = [ + ...Object.values(getOr(emptyFields, `${category}.fields`, browserFields)), + ]; + + return timestamp != null && categoryFields.length + ? uniqBy('id', [ + getColumnHeaderFromBrowserField({ + browserField: timestamp, + width: DEFAULT_DATE_COLUMN_MIN_WIDTH, + }), + ...categoryFields.map(f => getColumnHeaderFromBrowserField({ browserField: f })), + ]) + : []; +}; + /** Returns example text, or an empty string if the field does not have an example */ -export const getExampleText = (field: DetailItem): string => - !isEmpty(field.example) ? `Example: ${field.example}` : ''; +export const getExampleText = (example: string | number | null | undefined): string => + !isEmpty(example) ? `Example: ${example}` : ''; -export const getIconFromType = (type: string) => { +export const getIconFromType = (type: string | null) => { switch (type) { + case 'string': // fall through case 'keyword': return 'string'; + case 'number': // fall through case 'long': return 'number'; case 'date': diff --git a/x-pack/plugins/siem/public/components/event_details/json_view.tsx b/x-pack/plugins/siem/public/components/event_details/json_view.tsx index 1e9c8ca2fcea1..230ca10a7996f 100644 --- a/x-pack/plugins/siem/public/components/event_details/json_view.tsx +++ b/x-pack/plugins/siem/public/components/event_details/json_view.tsx @@ -37,7 +37,6 @@ export const JsonView = pure(({ data }) => ( )} width="100%" /> - } )); diff --git a/x-pack/plugins/siem/public/components/event_details/stateful_event_details.tsx b/x-pack/plugins/siem/public/components/event_details/stateful_event_details.tsx index 32dba478b5cfe..8908645ba0adb 100644 --- a/x-pack/plugins/siem/public/components/event_details/stateful_event_details.tsx +++ b/x-pack/plugins/siem/public/components/event_details/stateful_event_details.tsx @@ -6,13 +6,19 @@ import * as React from 'react'; +import { BrowserFields } from '../../containers/source'; import { DetailItem } from '../../graphql/types'; +import { OnUpdateColumns } from '../timeline/events'; import { EventDetails, View } from './event_details'; interface Props { + browserFields: BrowserFields; data: DetailItem[]; id: string; + isLoading: boolean; + onUpdateColumns: OnUpdateColumns; + timelineId: string; } interface State { @@ -31,14 +37,18 @@ export class StatefulEventDetails extends React.PureComponent { }; public render() { - const { data, id } = this.props; + const { browserFields, data, id, isLoading, onUpdateColumns, timelineId } = this.props; return ( ); } diff --git a/x-pack/plugins/siem/public/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap b/x-pack/plugins/siem/public/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap index 77cac35391c45..bb63abea61ceb 100644 --- a/x-pack/plugins/siem/public/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap +++ b/x-pack/plugins/siem/public/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap @@ -14,8 +14,7 @@ exports[`Field Renderers #autonomousSystemRenderer it renders correctly against id="ip-overview-source.autonomous_system.as_org" value="Test Org" /> - - / + / - - - - - raspberrypi - - - - + raspberrypi + + `; exports[`Field Renderers #hostNameRenderer it renders correctly against snapshot 1`] = ` - - - - - raspberrypi - - - - + raspberrypi + + `; @@ -228,12 +209,17 @@ exports[`Field Renderers #reputationRenderer it renders correctly against snapsh } } > + + virustotal.com + + , - View at iana.org + talosIntelligence.com - `; @@ -345,8 +331,7 @@ exports[`Field Renderers #whoisRenderer it renders correctly against snapshot 1` - View at iana.org + iana.org - `; diff --git a/x-pack/plugins/siem/public/components/field_renderers/field_renderers.test.tsx b/x-pack/plugins/siem/public/components/field_renderers/field_renderers.test.tsx index ef9aa9a9be177..b96953af4a043 100644 --- a/x-pack/plugins/siem/public/components/field_renderers/field_renderers.test.tsx +++ b/x-pack/plugins/siem/public/components/field_renderers/field_renderers.test.tsx @@ -7,7 +7,7 @@ import { mount, shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers'; import { FlowTarget, GetIpOverviewQuery, HostEcsFields } from '../../graphql/types'; import { TestProviders } from '../../mock'; @@ -19,6 +19,8 @@ import { hostNameRenderer, locationRenderer, whoisRenderer, + reputationRenderer, + DefaultFieldRenderer, } from './field_renderers'; import { mockData } from '../page/network/ip_overview/mock'; @@ -112,9 +114,7 @@ describe('Field Renderers', () => { }; test('it renders correctly against snapshot', () => { const wrapper = shallow( - - {hostNameRenderer(mockData.complete.source!.host!, '10.10.10.10')} - + {hostNameRenderer(mockData.complete.host, '10.10.10.10')} ); expect(toJson(wrapper)).toMatchSnapshot(); @@ -122,9 +122,7 @@ describe('Field Renderers', () => { test('it renders emptyTagValue when non-matching IP is provided', () => { const wrapper = mount( - - {hostNameRenderer(mockData.complete.source!.host!, '10.10.10.11')} - + {hostNameRenderer(mockData.complete.host, '10.10.10.11')} ); expect(wrapper.text()).toEqual(getEmptyValue()); }); @@ -161,9 +159,7 @@ describe('Field Renderers', () => { }; test('it renders correctly against snapshot', () => { const wrapper = shallow( - - {hostNameRenderer(mockData.complete.source!.host!, '10.10.10.10')} - + {hostNameRenderer(mockData.complete.host, '10.10.10.10')} ); expect(toJson(wrapper)).toMatchSnapshot(); @@ -171,9 +167,7 @@ describe('Field Renderers', () => { test('it renders emptyTagValue when non-matching IP is provided', () => { const wrapper = mount( - - {hostNameRenderer(mockData.complete.source!.host!, '10.10.10.11')} - + {hostNameRenderer(mockData.complete.host, '10.10.10.11')} ); expect(wrapper.text()).toEqual(getEmptyValue()); }); @@ -211,10 +205,35 @@ describe('Field Renderers', () => { describe('#reputationRenderer', () => { test('it renders correctly against snapshot', () => { const wrapper = shallowWithIntl( - {whoisRenderer('10.10.10.10')} + {reputationRenderer('10.10.10.10')} ); expect(toJson(wrapper)).toMatchSnapshot(); }); }); + + describe('DefaultFieldRenderer', () => { + test('it should render a single item', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('item1 '); + }); + + test('it should render two items', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('item1,item2 '); + }); + }); }); diff --git a/x-pack/plugins/siem/public/components/field_renderers/field_renderers.tsx b/x-pack/plugins/siem/public/components/field_renderers/field_renderers.tsx index 45d523f9f337f..a0bb922022240 100644 --- a/x-pack/plugins/siem/public/components/field_renderers/field_renderers.tsx +++ b/x-pack/plugins/siem/public/components/field_renderers/field_renderers.tsx @@ -6,7 +6,7 @@ import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPopover, EuiText } from '@elastic/eui'; import { getOr } from 'lodash/fp'; -import React, { useState } from 'react'; +import React, { Fragment, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { pure } from 'recompose'; @@ -19,12 +19,12 @@ import { } from '../../graphql/types'; import { DefaultDraggable } from '../draggables'; import { getEmptyTagValue } from '../empty_value'; -import { ExternalLinkIcon } from '../external_link_icon'; import { FormattedDate } from '../formatted_date'; import { HostDetailsLink, ReputationLink, VirusTotalLink, WhoIsLink } from '../links'; import * as i18n from '../page/network/ip_overview/translations'; import { escapeDataProviderId } from '../drag_and_drop/helpers'; +import { Spacer } from '../page'; export const IpOverviewId = 'ip-overview'; @@ -34,7 +34,7 @@ export const locationRenderer = (fieldNames: string[], data: IpOverviewData): Re {fieldNames.map((fieldName, index) => { const locationValue = getOr('', fieldName, data); return ( - + {index ? ',\u00A0' : ''} - + ); })} @@ -66,8 +66,8 @@ export const autonomousSystemRenderer = ( id={`${IpOverviewId}-${flowTarget}.autonomous_system.as_org`} field={`${flowTarget}.autonomous_system.as_org`} value={as.as_org} - />{' '} - / + /> + {' /'} - host.id && host.ip && (!(ipFilter != null) || host.ip.includes(ipFilter)) ? ( - - - {host.name && host.name[0] != null ? ( - +interface HostIdRendererTypes { + host: HostEcsFields; + ipFilter?: string; + noLink?: boolean; +} + +export const hostIdRenderer = ({ + host, + ipFilter, + noLink, +}: HostIdRendererTypes): React.ReactElement => + host.id && host.ip && (ipFilter == null || host.ip.includes(ipFilter)) ? ( + <> + {host.name && host.name[0] != null ? ( + + {noLink ? ( + <>{host.id} + ) : ( {host.id} - - ) : ( - <>{host.id} - )} - - + )} + + ) : ( + <>{host.id} + )} + ) : ( getEmptyTagValue() ); export const hostNameRenderer = (host: HostEcsFields, ipFilter?: string): React.ReactElement => host.name && host.name[0] && host.ip && (!(ipFilter != null) || host.ip.includes(ipFilter)) ? ( - - - - - {host.name ? host.name : getEmptyTagValue()} - - - - + + + {host.name ? host.name : getEmptyTagValue()} + + ) : ( getEmptyTagValue() ); -export const whoisRenderer = (ip: string) => ( - <> - {i18n.VIEW_WHOIS} - - -); +export const whoisRenderer = (ip: string) => {i18n.VIEW_WHOIS}; export const reputationRenderer = (ip: string): React.ReactElement => ( <> {i18n.VIEW_VIRUS_TOTAL} - -
+ {', '} {i18n.VIEW_TALOS_INTELLIGENCE} - ); @@ -137,6 +138,8 @@ interface DefaultFieldRendererProps { maxOverflow?: number; } +// TODO: This causes breaks between elements until the ticket below is fixed +// https://github.com/elastic/ingest-dev/issues/474 export const DefaultFieldRenderer = pure( ({ rowItems, attrName, idPrefix, render, displayCount = 1, maxOverflow = 5 }) => { if (rowItems != null && rowItems.length > 0) { @@ -144,7 +147,12 @@ export const DefaultFieldRenderer = pure( const id = escapeDataProviderId(`${idPrefix}-${attrName}-${rowItem}`); return ( - {index !== 0 && <>, } + {index !== 0 && ( + <> + {','} + + + )} {render ? render(rowItem) : rowItem} diff --git a/x-pack/plugins/siem/public/components/fields_browser/categories_pane.tsx b/x-pack/plugins/siem/public/components/fields_browser/categories_pane.tsx new file mode 100644 index 0000000000000..d7ad1e335eda4 --- /dev/null +++ b/x-pack/plugins/siem/public/components/fields_browser/categories_pane.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiInMemoryTable, EuiTitle } from '@elastic/eui'; +import * as React from 'react'; +import { pure } from 'recompose'; +import styled from 'styled-components'; + +import { BrowserFields } from '../../containers/source'; + +import { FieldBrowserProps } from './types'; +import { getCategoryColumns } from './category_columns'; +import { TABLE_HEIGHT } from './helpers'; + +import * as i18n from './translations'; + +const CategoryNames = styled.div<{ height: number; width: number }>` + ${({ height }) => `height: ${height}px`}; + overflow: auto; + padding: 5px; + ${({ width }) => `width: ${width}px`}; + thead { + display: none; + } +`; + +const Title = styled(EuiTitle)` + padding-left: 5px; +`; + +type Props = Pick< + FieldBrowserProps, + 'browserFields' | 'isLoading' | 'timelineId' | 'onUpdateColumns' +> & { + /** + * A map of categoryId -> metadata about the fields in that category, + * filtered such that the name of every field in the category includes + * the filter input (as a substring). + */ + filteredBrowserFields: BrowserFields; + /** + * Invoked when the user clicks on the name of a category in the left-hand + * side of the field browser + */ + onCategorySelected: (categoryId: string) => void; + /** The category selected on the left-hand side of the field browser */ + selectedCategoryId: string; + /** The width of the categories pane */ + width: number; +}; +export const CategoriesPane = pure( + ({ + browserFields, + filteredBrowserFields, + isLoading, + onCategorySelected, + onUpdateColumns, + selectedCategoryId, + timelineId, + width, + }) => ( + <> + + <h5>{i18n.CATEGORIES}</h5> + + + + ({ categoryId }))} + message={i18n.NO_FIELDS_MATCH} + pagination={false} + sorting={false} + /> + + + ) +); diff --git a/x-pack/plugins/siem/public/components/fields_browser/category.tsx b/x-pack/plugins/siem/public/components/fields_browser/category.tsx new file mode 100644 index 0000000000000..c8010eb522062 --- /dev/null +++ b/x-pack/plugins/siem/public/components/fields_browser/category.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiInMemoryTable } from '@elastic/eui'; +import { pure } from 'recompose'; +import * as React from 'react'; +import styled from 'styled-components'; + +import { BrowserFields } from '../../containers/source'; + +import { CategoryTitle } from './category_title'; +import { FieldItem, getFieldColumns } from './field_items'; +import { TABLE_HEIGHT } from './helpers'; + +const TableContainer = styled.div<{ height: number; width: number }>` + ${({ height }) => `height: ${height}px`}; + overflow-x: hidden; + overflow-y: auto; + ${({ width }) => `width: ${width}px`}; +`; + +interface Props { + categoryId: string; + fieldItems: FieldItem[]; + filteredBrowserFields: BrowserFields; + onCategorySelected: (categoryId: string) => void; + timelineId: string; + width: number; +} + +export const Category = pure( + ({ categoryId, filteredBrowserFields, fieldItems, timelineId, width }) => ( + <> + + + + + + + ) +); diff --git a/x-pack/plugins/siem/public/components/fields_browser/category_buttons.tsx b/x-pack/plugins/siem/public/components/fields_browser/category_buttons.tsx new file mode 100644 index 0000000000000..6a199381013ba --- /dev/null +++ b/x-pack/plugins/siem/public/components/fields_browser/category_buttons.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButton, EuiToolTip } from '@elastic/eui'; +import * as React from 'react'; +import { pure } from 'recompose'; + +import { + DEFAULT_CATEGORY_NAME, + defaultHeaders, +} from '../timeline/body/column_headers/default_headers'; +import { OnUpdateColumns } from '../timeline/events'; + +import * as i18n from './translations'; + +/** + * The default category button allows the user to reset the fields shown in + * the timeline with a single click + */ +export const DefaultCategoryButton = pure<{ + isLoading: boolean; + onUpdateColumns: OnUpdateColumns; +}>(({ isLoading, onUpdateColumns }) => ( + + { + onUpdateColumns(defaultHeaders); + }} + size="s" + > + {DEFAULT_CATEGORY_NAME} + + +)); diff --git a/x-pack/plugins/siem/public/components/fields_browser/category_columns.tsx b/x-pack/plugins/siem/public/components/fields_browser/category_columns.tsx new file mode 100644 index 0000000000000..2a56d463c8080 --- /dev/null +++ b/x-pack/plugins/siem/public/components/fields_browser/category_columns.tsx @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiIcon, EuiFlexGroup, EuiFlexItem, EuiLink, EuiPanel, EuiToolTip } from '@elastic/eui'; +import * as React from 'react'; +import styled from 'styled-components'; + +import { BrowserFields } from '../../containers/source'; +import { getColumnsWithTimestamp } from '../event_details/helpers'; +import { OnUpdateColumns } from '../timeline/events'; +import { WithHoverActions } from '../with_hover_actions'; + +import * as i18n from './translations'; +import { CountBadge } from '../page'; +import { LoadingSpinner, getCategoryPaneCategoryClassName, getFieldCount } from './helpers'; + +const CategoryName = styled.span<{ bold: boolean }>` + font-weight: ${({ bold }) => (bold ? 'bold' : 'normal')}; +`; + +const HoverActionsContainer = styled(EuiPanel)` + cursor: default; + height: 25px; + left: 5px; + position: absolute; + top: -5px; + width: 30px; +`; + +const HoverActionsFlexGroup = styled(EuiFlexGroup)` + cursor: pointer; + left: -2px; + position: relative; + top: -6px; +`; + +const LinkContainer = styled.div` + width: 100%; + .euiLink { + width: 100%; + } +`; + +export interface CategoryItem { + categoryId: string; +} + +/** + * Returns the column definition for the (single) column that displays all the + * category names in the field browser */ +export const getCategoryColumns = ({ + browserFields, + filteredBrowserFields, + isLoading, + onCategorySelected, + onUpdateColumns, + selectedCategoryId, + timelineId, +}: { + browserFields: BrowserFields; + filteredBrowserFields: BrowserFields; + isLoading: boolean; + onCategorySelected: (categoryId: string) => void; + onUpdateColumns: OnUpdateColumns; + selectedCategoryId: string; + timelineId: string; +}) => [ + { + field: 'categoryId', + sortable: true, + truncateText: false, + render: (categoryId: string) => ( + + onCategorySelected(categoryId)}> + + + + + + + {!isLoading ? ( + { + onUpdateColumns( + getColumnsWithTimestamp({ + browserFields, + category: categoryId, + }) + ); + }} + type="visTable" + /> + ) : ( + + )} + + + + + } + render={() => ( + + {categoryId} + + )} + /> + + + + + {getFieldCount(filteredBrowserFields[categoryId])} + + + + + + ), + }, +]; diff --git a/x-pack/plugins/siem/public/components/fields_browser/category_title.tsx b/x-pack/plugins/siem/public/components/fields_browser/category_title.tsx new file mode 100644 index 0000000000000..d379ea80336d3 --- /dev/null +++ b/x-pack/plugins/siem/public/components/fields_browser/category_title.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; +import { pure } from 'recompose'; +import * as React from 'react'; +import styled from 'styled-components'; + +import { BrowserFields } from '../../containers/source'; + +import { getFieldBrowserCategoryTitleClassName, getFieldCount } from './helpers'; + +import { CountBadge } from '../page'; + +const CountBadgeContainer = styled.div` + position: relative; + top: -3px; +`; + +interface Props { + /** The title of the category */ + categoryId: string; + /** + * A map of categoryId -> metadata about the fields in that category, + * filtered such that the name of every field in the category includes + * the filter input (as a substring). + */ + filteredBrowserFields: BrowserFields; + /** The timeline associated with this field browser */ + timelineId: string; +} + +export const CategoryTitle = pure(({ filteredBrowserFields, categoryId, timelineId }) => ( + + + +
{categoryId}
+
+
+ + + + {getFieldCount(filteredBrowserFields[categoryId])} + + +
+)); diff --git a/x-pack/plugins/siem/public/components/fields_browser/field_browser.tsx b/x-pack/plugins/siem/public/components/fields_browser/field_browser.tsx new file mode 100644 index 0000000000000..03266df91c587 --- /dev/null +++ b/x-pack/plugins/siem/public/components/fields_browser/field_browser.tsx @@ -0,0 +1,247 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { noop } from 'lodash/fp'; +import { EuiFlexGroup, EuiFlexItem, EuiOutsideClickDetector } from '@elastic/eui'; +import * as React from 'react'; +import styled from 'styled-components'; + +import { BrowserFields } from '../../containers/source'; +import { ColumnHeader } from '../timeline/body/column_headers/column_header'; + +import { + CATEGORY_PANE_WIDTH, + FIELDS_PANE_WIDTH, + getCategoryPaneCategoryClassName, + getFieldBrowserCategoryTitleClassName, + getFieldBrowserSearchInputClassName, + PANES_FLEX_GROUP_WIDTH, +} from './helpers'; +import { FieldBrowserProps, OnFieldSelected, OnHideFieldBrowser } from './types'; +import { Header } from './header'; +import { CategoriesPane } from './categories_pane'; +import { FieldsPane } from './fields_pane'; + +const TOP_OFFSET = 207; + +const FieldsBrowserContainer = styled.div<{ + top: number; + width: number; +}>` + background-color: ${props => props.theme.eui.euiColorLightestShade}; + border: 1px solid ${({ theme }) => theme.eui.euiColorMediumShade}; + border-radius: 4px; + padding: 8px 8px 16px 8px; + position: absolute; + ${({ top }) => `top: ${top}px`}; + ${({ width }) => `width: ${width}px`}; + z-index: 9990; +`; + +const PanesFlexGroup = styled(EuiFlexGroup)` + width: ${PANES_FLEX_GROUP_WIDTH}px; +`; + +type Props = Pick< + FieldBrowserProps, + | 'browserFields' + | 'height' + | 'isLoading' + | 'onFieldSelected' + | 'onUpdateColumns' + | 'timelineId' + | 'width' +> & { + /** + * The current timeline column headers + */ + columnHeaders: ColumnHeader[]; + /** + * A map of categoryId -> metadata about the fields in that category, + * filtered such that the name of every field in the category includes + * the filter input (as a substring). + */ + filteredBrowserFields: BrowserFields; + /** + * When true, a busy spinner will be shown to indicate the field browser + * is searching for fields that match the specified `searchInput` + */ + isSearching: boolean; + /** The text displayed in the search input */ + searchInput: string; + /** + * The category selected on the left-hand side of the field browser + */ + selectedCategoryId: string; + /** + * Invoked when the user clicks on the name of a category in the left-hand + * side of the field browser + */ + onCategorySelected: (categoryId: string) => void; + /** + * Hides the field browser when invoked + */ + onHideFieldBrowser: OnHideFieldBrowser; + /** + * Invoked when the user clicks outside of the field browser + */ + onOutsideClick: () => void; + /** + * Invoked when the user types in the search input + */ + onSearchInputChange: (newSearchInput: string) => void; + /** + * Invoked to add or remove a column from the timeline + */ + toggleColumn: (column: ColumnHeader) => void; +}; + +/** + * This component has no internal state, but it uses lifecycle methods to + * set focus to the search input, scroll to the selected category, etc + */ +export class FieldsBrowser extends React.PureComponent { + public componentDidMount() { + this.scrollViews(); + this.focusInput(); + } + + public componentDidUpdate() { + this.scrollViews(); + this.focusInput(); // always re-focus the input to enable additional filtering + } + + public render() { + const { + columnHeaders, + browserFields, + filteredBrowserFields, + searchInput, + isLoading, + isSearching, + onCategorySelected, + onFieldSelected, + onOutsideClick, + onUpdateColumns, + selectedCategoryId, + timelineId, + toggleColumn, + width, + } = this.props; + + return ( + + +
+ + + + + + + + + + + + + ); + } + + /** Focuses the input that filters the field browser */ + private focusInput = () => { + const elements = document.getElementsByClassName( + getFieldBrowserSearchInputClassName(this.props.timelineId) + ); + + if (elements.length > 0) { + (elements[0] as HTMLElement).focus(); // this cast is required because focus() does not exist on every `Element` returned by `getElementsByClassName` + } + }; + + /** Invoked when the user types in the input to filter the field browser */ + private onInputChange = (event: React.ChangeEvent) => + this.props.onSearchInputChange(event.target.value); + + private selectFieldAndHide: OnFieldSelected = (fieldId: string) => { + const { onFieldSelected, onHideFieldBrowser } = this.props; + + if (onFieldSelected != null) { + onFieldSelected(fieldId); + } + + onHideFieldBrowser(); + }; + + private scrollViews = () => { + const { selectedCategoryId, timelineId } = this.props; + + if (this.props.selectedCategoryId !== '') { + const categoryPaneTitles = document.getElementsByClassName( + getCategoryPaneCategoryClassName({ + categoryId: selectedCategoryId, + timelineId, + }) + ); + + if (categoryPaneTitles.length > 0) { + categoryPaneTitles[0].scrollIntoView(); + } + + const fieldPaneTitles = document.getElementsByClassName( + getFieldBrowserCategoryTitleClassName({ + categoryId: selectedCategoryId, + timelineId, + }) + ); + + if (fieldPaneTitles.length > 0) { + fieldPaneTitles[0].scrollIntoView(); + } + } + + this.focusInput(); // always re-focus the input to enable additional filtering + }; +} diff --git a/x-pack/plugins/siem/public/components/fields_browser/field_items.tsx b/x-pack/plugins/siem/public/components/fields_browser/field_items.tsx new file mode 100644 index 0000000000000..09be8950b7fc5 --- /dev/null +++ b/x-pack/plugins/siem/public/components/fields_browser/field_items.tsx @@ -0,0 +1,180 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiCheckbox, EuiIcon, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { uniqBy } from 'lodash/fp'; +import * as React from 'react'; +import { Draggable } from 'react-beautiful-dnd'; +import styled from 'styled-components'; + +import { BrowserField, BrowserFields } from '../../containers/source'; +import { DraggableFieldBadge } from '../draggables/field_badge'; +import { DragEffects } from '../drag_and_drop/draggable_wrapper'; +import { DroppableWrapper } from '../drag_and_drop/droppable_wrapper'; +import { getColumnsWithTimestamp, getExampleText, getIconFromType } from '../event_details/helpers'; +import { getDraggableFieldId, getDroppableId, DRAG_TYPE_FIELD } from '../drag_and_drop/helpers'; +import { getEmptyValue } from '../empty_value'; +import { OnUpdateColumns } from '../timeline/events'; +import { SelectableText } from '../selectable_text'; +import { TruncatableText } from '../truncatable_text'; + +import { FieldName } from './field_name'; + +import * as i18n from './translations'; +import { ColumnHeader } from '../timeline/body/column_headers/column_header'; +import { DEFAULT_COLUMN_MIN_WIDTH } from '../timeline/body/helpers'; +import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers'; + +const TypeIcon = styled(EuiIcon)` + margin-left: 5px; + position: relative; + top: -1px; +`; + +/** + * An item rendered in the table + */ +export interface FieldItem { + description: React.ReactNode; + field: React.ReactNode; + fieldId: string; +} + +/** + * Returns the draggable fields, values, and descriptions shown when a user expands an event + */ +export const getFieldItems = ({ + browserFields, + category, + categoryId, + columnHeaders, + highlight = '', + isLoading, + onUpdateColumns, + timelineId, + toggleColumn, +}: { + browserFields: BrowserFields; + category: Partial; + categoryId: string; + columnHeaders: ColumnHeader[]; + highlight?: string; + isLoading: boolean; + timelineId: string; + toggleColumn: (column: ColumnHeader) => void; + onUpdateColumns: OnUpdateColumns; +}): FieldItem[] => + uniqBy('name', [ + ...Object.values(category != null && category.fields != null ? category.fields : {}), + ]).map(field => ({ + description: ( + + {`${field.description || getEmptyValue()} ${getExampleText(field.example)}`}{' '} + + ), + field: ( + + + {(provided, snapshot) => ( +
+ {!snapshot.isDragging ? ( + + + c.id === field.name) !== -1} + id={field.name || ''} + onChange={() => + toggleColumn({ + columnHeaderType: defaultColumnHeaderType, + id: field.name || '', + width: DEFAULT_COLUMN_MIN_WIDTH, + }) + } + /> + + + + + + + + + + + + + ) : ( + + + + )} +
+ )} +
+
+ ), + fieldId: field.name || '', + })); + +/** + * Returns a table column template provided to the `EuiInMemoryTable`'s + * `columns` prop + */ +export const getFieldColumns = () => [ + { + field: 'field', + name: i18n.FIELD, + sortable: true, + render: (field: React.ReactNode) => <>{field}, + width: '250px', + }, + { + field: 'description', + name: i18n.DESCRIPTION, + render: (description: string) => ( + + + {description} + + + ), + sortable: true, + truncateText: true, + width: '400px', + }, +]; diff --git a/x-pack/plugins/siem/public/components/fields_browser/field_name.tsx b/x-pack/plugins/siem/public/components/fields_browser/field_name.tsx new file mode 100644 index 0000000000000..ffc4076e5a22c --- /dev/null +++ b/x-pack/plugins/siem/public/components/fields_browser/field_name.tsx @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + // @ts-ignore + EuiHighlight, + EuiIcon, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiToolTip, +} from '@elastic/eui'; +import * as React from 'react'; +import { pure } from 'recompose'; +import styled from 'styled-components'; + +import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard'; +import { ColumnHeader } from '../timeline/body/column_headers/column_header'; +import { OnUpdateColumns } from '../timeline/events'; +import { WithHoverActions } from '../with_hover_actions'; + +import { LoadingSpinner } from './helpers'; +import * as i18n from './translations'; + +/** + * The name of a (draggable) field + */ +export const FieldNameContainer = styled.span` + padding: 5px; + &:hover { + transition: background-color 0.7s ease; + background-color: #000; + color: #fff; + } +`; + +const HoverActionsContainer = styled(EuiPanel)` + cursor: default; + height: 25px; + left: 5px; + position: absolute; + top: 3px; +`; + +const HoverActionsFlexGroup = styled(EuiFlexGroup)` + cursor: pointer; + position: relative; + top: -8px; +`; + +const ViewCategoryIcon = styled(EuiIcon)` + margin-left: 5px; +`; + +/** Renders a field name in it's non-dragging state */ +export const FieldName = pure<{ + categoryId: string; + categoryColumns: ColumnHeader[]; + fieldId: string; + highlight?: string; + isLoading: boolean; + onUpdateColumns: OnUpdateColumns; +}>(({ categoryId, categoryColumns, fieldId, highlight = '', isLoading, onUpdateColumns }) => ( + + + + + + + + + {categoryColumns.length > 0 && ( + + + {!isLoading ? ( + { + onUpdateColumns(categoryColumns); + }} + type="visTable" + /> + ) : ( + + )} + + + )} + + + } + render={() => ( + + {fieldId} + + )} + /> +)); diff --git a/x-pack/plugins/siem/public/components/fields_browser/fields_pane.tsx b/x-pack/plugins/siem/public/components/fields_browser/fields_pane.tsx new file mode 100644 index 0000000000000..a20bde63c6d5e --- /dev/null +++ b/x-pack/plugins/siem/public/components/fields_browser/fields_pane.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { pure } from 'recompose'; +import * as React from 'react'; +import styled from 'styled-components'; + +import { BrowserFields } from '../../containers/source'; +import { ColumnHeader } from '../timeline/body/column_headers/column_header'; + +import { Category } from './category'; +import { FieldBrowserProps } from './types'; +import { getFieldItems } from './field_items'; +import { FIELDS_PANE_WIDTH, TABLE_HEIGHT } from './helpers'; + +import * as i18n from './translations'; + +const NoFieldsPanel = styled.div` + background-color: ${props => props.theme.eui.euiColorLightestShade}; + width: ${FIELDS_PANE_WIDTH}px; + height: ${TABLE_HEIGHT}px; +`; + +const NoFieldsFlexGroup = styled(EuiFlexGroup)` + height: 100%; +`; + +type Props = Pick< + FieldBrowserProps, + 'isLoading' | 'onFieldSelected' | 'onUpdateColumns' | 'timelineId' +> & { + columnHeaders: ColumnHeader[]; + /** + * A map of categoryId -> metadata about the fields in that category, + * filtered such that the name of every field in the category includes + * the filter input (as a substring). + */ + filteredBrowserFields: BrowserFields; + /** + * Invoked when the user clicks on the name of a category in the left-hand + * side of the field browser + */ + onCategorySelected: (categoryId: string) => void; + /** The text displayed in the search input */ + searchInput: string; + /** + * The category selected on the left-hand side of the field browser + */ + selectedCategoryId: string; + /** The width field browser */ + width: number; + /** + * Invoked to add or remove a column from the timeline + */ + toggleColumn: (column: ColumnHeader) => void; +}; +export const FieldsPane = pure( + ({ + columnHeaders, + filteredBrowserFields, + isLoading, + onCategorySelected, + onUpdateColumns, + searchInput, + selectedCategoryId, + timelineId, + toggleColumn, + width, + }) => ( + <> + {Object.keys(filteredBrowserFields).length > 0 ? ( + + ) : ( + + + +

{i18n.NO_FIELDS_MATCH_INPUT(searchInput)}

+
+
+
+ )} + + ) +); diff --git a/x-pack/plugins/siem/public/components/fields_browser/header.tsx b/x-pack/plugins/siem/public/components/fields_browser/header.tsx new file mode 100644 index 0000000000000..4c9d063526e4b --- /dev/null +++ b/x-pack/plugins/siem/public/components/fields_browser/header.tsx @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiButtonEmpty, + EuiFieldSearch, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import * as React from 'react'; +import { pure } from 'recompose'; +import styled from 'styled-components'; + +import { BrowserFields } from '../../containers/source'; +import { defaultHeaders } from '../timeline/body/column_headers/default_headers'; +import { OnUpdateColumns } from '../timeline/events'; + +import { getFieldBrowserSearchInputClassName, getFieldCount, SEARCH_INPUT_WIDTH } from './helpers'; + +import * as i18n from './translations'; + +const CountsFlexGroup = styled(EuiFlexGroup)` + margin-top: 5px; +`; + +const CountFlexItem = styled(EuiFlexItem)` + margin-right: 5px; +`; + +// background-color: ${props => props.theme.eui.euiColorLightestShade}; +const HeaderContainer = styled.div` + padding: 16px; + margin-bottom: 8px; +`; + +const SearchContainer = styled.div` + input { + max-width: ${SEARCH_INPUT_WIDTH}px; + width: ${SEARCH_INPUT_WIDTH}px; + } +`; + +interface Props { + filteredBrowserFields: BrowserFields; + isSearching: boolean; + onOutsideClick: () => void; + onSearchInputChange: (event: React.ChangeEvent) => void; + onUpdateColumns: OnUpdateColumns; + searchInput: string; + timelineId: string; +} + +const CountRow = pure>(({ filteredBrowserFields }) => ( + + + + {i18n.CATEGORIES_COUNT(Object.keys(filteredBrowserFields).length)} + + + + + + {i18n.FIELDS_COUNT( + Object.keys(filteredBrowserFields).reduce( + (fieldsCount, category) => getFieldCount(filteredBrowserFields[category]) + fieldsCount, + 0 + ) + )} + + + +)); + +const TitleRow = pure<{ onOutsideClick: () => void; onUpdateColumns: OnUpdateColumns }>( + ({ onOutsideClick, onUpdateColumns }) => ( + + + +

{i18n.CUSTOMIZE_COLUMNS}

+
+
+ + + { + onUpdateColumns(defaultHeaders); + onOutsideClick(); + }} + > + {i18n.RESET_FIELDS} + + +
+ ) +); + +export const Header = pure( + ({ + isSearching, + filteredBrowserFields, + onOutsideClick, + onSearchInputChange, + onUpdateColumns, + searchInput, + timelineId, + }) => ( + + + + + + + + ) +); diff --git a/x-pack/plugins/siem/public/components/fields_browser/helpers.tsx b/x-pack/plugins/siem/public/components/fields_browser/helpers.tsx new file mode 100644 index 0000000000000..880e6210851f1 --- /dev/null +++ b/x-pack/plugins/siem/public/components/fields_browser/helpers.tsx @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiLoadingSpinner } from '@elastic/eui'; +import { filter, get, pickBy } from 'lodash/fp'; +import styled from 'styled-components'; + +import { BrowserField, BrowserFields } from '../../containers/source'; +import { + DEFAULT_CATEGORY_NAME, + defaultHeaders, +} from '../timeline/body/column_headers/default_headers'; + +export const LoadingSpinner = styled(EuiLoadingSpinner)` + cursor: pointer; + position: relative; + top: 3px; +`; + +export const CATEGORY_PANE_WIDTH = 200; +export const DESCRIPTION_COLUMN_WIDTH = 300; +export const FIELD_COLUMN_WIDTH = 200; +export const FIELD_BROWSER_WIDTH = 900; +export const FIELD_BROWSER_HEIGHT = 300; +export const FIELDS_PANE_WIDTH = 670; +export const HEADER_HEIGHT = 40; +export const PANES_FLEX_GROUP_WIDTH = CATEGORY_PANE_WIDTH + FIELDS_PANE_WIDTH + 10; +export const SEARCH_INPUT_WIDTH = 850; +export const TABLE_HEIGHT = 260; +export const TYPE_COLUMN_WIDTH = 50; + +/** + * Returns the CSS class name for the title of a category shown in the left + * side field browser + */ +export const getCategoryPaneCategoryClassName = ({ + categoryId, + timelineId, +}: { + categoryId: string; + timelineId: string; +}): string => `field-browser-category-pane-${categoryId}-${timelineId}`; + +/** + * Returns the CSS class name for the title of a category shown in the right + * side of field browser + */ +export const getFieldBrowserCategoryTitleClassName = ({ + categoryId, + timelineId, +}: { + categoryId: string; + timelineId: string; +}): string => `field-browser-category-title-${categoryId}-${timelineId}`; + +/** Returns the class name for a field browser search input */ +export const getFieldBrowserSearchInputClassName = (timelineId: string): string => + `field-browser-search-input-${timelineId}`; + +/** Returns true if the specified category has at least one field */ +export const categoryHasFields = (category: Partial): boolean => + category.fields != null && Object.keys(category.fields).length > 0; + +/** Returns the count of fields in the specified category */ +export const getFieldCount = (category: Partial | undefined): number => + category != null && category.fields != null ? Object.keys(category.fields).length : 0; + +/** + * Filters the specified `BrowserFields` to return a new collection where every + * category contains at least one field name that matches the specified substring. + */ +export const filterBrowserFieldsByFieldName = ({ + browserFields, + substring, +}: { + browserFields: BrowserFields; + substring: string; +}): BrowserFields => { + const trimmedSubstring = substring.trim(); + + // filter each category such that it only contains fields with field names + // that contain the specified substring: + const filteredBrowserFields: BrowserFields = Object.keys(browserFields).reduce( + (filteredCategories, categoryId) => ({ + ...filteredCategories, + [categoryId]: { + ...browserFields[categoryId], + fields: filter( + f => f.name != null && f.name.includes(trimmedSubstring), + browserFields[categoryId].fields + ), + }, + }), + {} + ); + + // only pick non-empty categories from the filtered browser fields + const nonEmptyCategories: BrowserFields = pickBy( + category => categoryHasFields(category), + filteredBrowserFields + ); + + return nonEmptyCategories; +}; + +/** + * Returns a "virtual" category (e.g. default ECS) from the specified fieldIds + */ +export const createVirtualCategory = ({ + browserFields, + fieldIds, +}: { + browserFields: BrowserFields; + fieldIds: string[]; +}): Partial => ({ + fields: fieldIds.reduce>>>((fields, fieldId) => { + const splitId = fieldId.split('.'); // source.geo.city_name -> [source, geo, city_name] + + return { + ...fields, + [fieldId]: { + ...get([splitId.length > 1 ? splitId[0] : 'base', 'fields', fieldId], browserFields), + name: fieldId, + }, + }; + }, {}), +}); + +/** Merges the specified browser fields with the default category (i.e. `default ECS`) */ +export const mergeBrowserFieldsWithDefaultCategory = ( + browserFields: BrowserFields +): BrowserFields => ({ + ...browserFields, + [DEFAULT_CATEGORY_NAME]: createVirtualCategory({ + browserFields, + fieldIds: defaultHeaders.map(header => header.id), + }), +}); diff --git a/x-pack/plugins/siem/public/components/fields_browser/index.tsx b/x-pack/plugins/siem/public/components/fields_browser/index.tsx new file mode 100644 index 0000000000000..94fd0e2fb1237 --- /dev/null +++ b/x-pack/plugins/siem/public/components/fields_browser/index.tsx @@ -0,0 +1,266 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ActionCreator } from 'typescript-fsa'; +import { connect } from 'react-redux'; +import { EuiButton, EuiToolTip } from '@elastic/eui'; +import { noop } from 'lodash/fp'; +import * as React from 'react'; +import styled from 'styled-components'; + +import { BrowserFields } from '../../containers/source'; +import { ColumnHeader } from '../timeline/body/column_headers/column_header'; +import { DEFAULT_CATEGORY_NAME } from '../timeline/body/column_headers/default_headers'; +import { OnUpdateColumns } from '../timeline/events'; + +import { FieldsBrowser } from './field_browser'; +import { FieldBrowserProps } from './types'; +import { filterBrowserFieldsByFieldName, mergeBrowserFieldsWithDefaultCategory } from './helpers'; + +import * as i18n from './translations'; +import { timelineActions } from '../../store/actions'; + +/** wait this many ms after the user completes typing before applying the filter input */ +const INPUT_TIMEOUT = 250; + +interface State { + /** all field names shown in the field browser must contain this string (when specified) */ + filterInput: string; + /** all fields in this collection have field names that match the filterInput */ + filteredBrowserFields: BrowserFields | null; + /** when true, show a spinner in the input to indicate the field browser is searching for matching field names */ + isSearching: boolean; + /** this category will be displayed in the right-hand pane of the field browser */ + selectedCategoryId: string; + /** show the field browser */ + show: boolean; +} + +const FieldsBrowserButtonContainer = styled.div` + button { + border-color: ${({ theme }) => theme.eui.euiColorLightShade}; + color: ${({ theme }) => theme.eui.euiColorDarkestShade}; + font-size: 14px; + margin: 1px 5px 2px 0; + } +`; + +interface DispatchProps { + removeColumn?: ActionCreator<{ + id: string; + columnId: string; + }>; + upsertColumn?: ActionCreator<{ + column: ColumnHeader; + id: string; + index: number; + }>; +} + +/** + * Manages the state of the field browser + */ +export class StatefulFieldsBrowserComponent extends React.PureComponent< + FieldBrowserProps & DispatchProps, + State +> { + /** tracks the latest timeout id from `setTimeout`*/ + private inputTimeoutId: number = 0; + + constructor(props: FieldBrowserProps) { + super(props); + + this.state = { + filterInput: '', + filteredBrowserFields: null, + isSearching: false, + selectedCategoryId: DEFAULT_CATEGORY_NAME, + show: false, + }; + } + + public componentWillUnmount() { + if (this.inputTimeoutId !== 0) { + // ⚠️ mutation: cancel any remaining timers and zero-out the timer id: + clearTimeout(this.inputTimeoutId); + this.inputTimeoutId = 0; + } + } + + public render() { + const { + columnHeaders, + browserFields, + height, + isLoading, + onFieldSelected, + timelineId, + width, + } = this.props; + const { + filterInput, + filteredBrowserFields, + isSearching, + selectedCategoryId, + show, + } = this.state; + + // only merge in the default category if the field browser is visible + const browserFieldsWithDefaultCategory = show + ? mergeBrowserFieldsWithDefaultCategory(browserFields) + : {}; + + return ( + <> + + + + {i18n.FIELDS} + + + + + {show && ( + + )} + + ); + } + + /** Shows / hides the field browser */ + private toggleShow = () => { + this.setState(({ show }) => ({ + show: !show, + })); + }; + + private toggleColumn = (column: ColumnHeader) => { + const { columnHeaders, removeColumn, timelineId, upsertColumn } = this.props; + const exists = columnHeaders.findIndex(c => c.id === column.id) !== -1; + + if (!exists && upsertColumn != null) { + upsertColumn({ + column, + id: timelineId, + index: 1, + }); + } + + if (exists && removeColumn != null) { + removeColumn({ + columnId: column.id, + id: timelineId, + }); + } + }; + + /** Invoked when the user types in the filter input */ + private updateFilter = (filterInput: string): void => { + this.setState({ + filterInput, + isSearching: true, + }); + + if (this.inputTimeoutId !== 0) { + clearTimeout(this.inputTimeoutId); // ⚠️ mutation: cancel any previous timers + } + + // ⚠️ mutation: schedule a new timer that will apply the filter when it fires: + this.inputTimeoutId = window.setTimeout(() => { + const filteredBrowserFields = filterBrowserFieldsByFieldName({ + browserFields: mergeBrowserFieldsWithDefaultCategory(this.props.browserFields), + substring: this.state.filterInput, + }); + + this.setState(currentState => ({ + filteredBrowserFields, + isSearching: false, + selectedCategoryId: + currentState.filterInput === '' || Object.keys(filteredBrowserFields).length === 0 + ? DEFAULT_CATEGORY_NAME + : Object.keys(filteredBrowserFields) + .sort() + .reduce( + (selected, category) => + filteredBrowserFields[category].fields != null && + filteredBrowserFields[selected].fields != null && + filteredBrowserFields[category].fields!.length > + filteredBrowserFields[selected].fields!.length + ? category + : selected, + Object.keys(filteredBrowserFields)[0] + ), + })); + }, INPUT_TIMEOUT); + }; + + /** + * Invoked when the user clicks a category name in the left-hand side of + * the field browser + */ + private updateSelectedCategoryId = (categoryId: string): void => { + this.setState({ + selectedCategoryId: categoryId, + }); + }; + + /** + * Invoked when the user clicks on the context menu to view a category's + * columns in the timeline, this function dispatches the action that + * causes the timeline display those columns. + */ + private updateColumnsAndSelectCategoryId: OnUpdateColumns = (columns: ColumnHeader[]): void => { + this.props.onUpdateColumns(columns); // show the category columns in the timeline + }; + + /** Invoked when the field browser should be hidden */ + private hideFieldBrowser = () => { + this.setState({ + filterInput: '', + filteredBrowserFields: null, + isSearching: false, + selectedCategoryId: DEFAULT_CATEGORY_NAME, + show: false, + }); + }; +} + +export const StatefulFieldsBrowser = connect( + null, + { + removeColumn: timelineActions.removeColumn, + upsertColumn: timelineActions.upsertColumn, + } +)(StatefulFieldsBrowserComponent); diff --git a/x-pack/plugins/siem/public/components/fields_browser/translations.ts b/x-pack/plugins/siem/public/components/fields_browser/translations.ts new file mode 100644 index 0000000000000..23ada2b7ff561 --- /dev/null +++ b/x-pack/plugins/siem/public/components/fields_browser/translations.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const CATEGORY = i18n.translate('xpack.siem.fieldBrowser.categoryLabel', { + defaultMessage: 'Category', +}); + +export const CATEGORIES = i18n.translate('xpack.siem.fieldBrowser.categoriesTitle', { + defaultMessage: 'Categories', +}); + +export const CATEGORIES_COUNT = (totalCount: number) => + i18n.translate('xpack.siem.fieldBrowser.categoriesCountTitle', { + values: { totalCount }, + defaultMessage: '{totalCount} {totalCount, plural, =1 {Category} other {Categories}}', + }); + +export const COPY_TO_CLIPBOARD = i18n.translate('xpack.siem.fieldBrowser.copyToClipboard', { + defaultMessage: 'Copy to Clipboard', +}); + +export const CUSTOMIZE_COLUMNS = i18n.translate('xpack.siem.fieldBrowser.customizeColumnsTitle', { + defaultMessage: 'Customize Columns', +}); + +export const DESCRIPTION = i18n.translate('xpack.siem.fieldBrowser.descriptionLabel', { + defaultMessage: 'Description', +}); + +export const FIELD = i18n.translate('xpack.siem.fieldBrowser.fieldLabel', { + defaultMessage: 'Field', +}); + +export const FIELDS = i18n.translate('xpack.siem.fieldBrowser.fieldsTitle', { + defaultMessage: 'Fields', +}); + +export const FIELDS_COUNT = (totalCount: number) => + i18n.translate('xpack.siem.fieldBrowser.fieldsCountTitle', { + values: { totalCount }, + defaultMessage: '{totalCount} {totalCount, plural, =1 {Field} other {Fields}}', + }); + +export const FILTER_PLACEHOLDER = i18n.translate('xpack.siem.fieldBrowser.filterPlaceholder', { + defaultMessage: 'Field name', +}); + +export const NO_FIELDS_MATCH = i18n.translate('xpack.siem.fieldBrowser.noFieldsMatchLabel', { + defaultMessage: 'No fields match', +}); + +export const NO_FIELDS_MATCH_INPUT = (searchInput: string) => + i18n.translate('xpack.siem.fieldBrowser.noFieldsMatchInputLabel', { + defaultMessage: 'No fields match {searchInput}', + values: { + searchInput, + }, + }); + +export const RESET_FIELDS = i18n.translate('xpack.siem.fieldBrowser.resetFieldsLink', { + defaultMessage: 'Reset Fields', +}); + +export const VIEW_CATEGORY = (categoryId: string) => + i18n.translate('xpack.siem.fieldBrowser.viewCategoryTooltip', { + defaultMessage: 'View all {categoryId} fields', + values: { + categoryId, + }, + }); diff --git a/x-pack/plugins/siem/public/components/fields_browser/types.ts b/x-pack/plugins/siem/public/components/fields_browser/types.ts new file mode 100644 index 0000000000000..e10502bf57f46 --- /dev/null +++ b/x-pack/plugins/siem/public/components/fields_browser/types.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { BrowserFields } from '../../containers/source'; +import { OnUpdateColumns } from '../timeline/events'; +import { ColumnHeader } from '../timeline/body/column_headers/column_header'; + +export type OnFieldSelected = (fieldId: string) => void; +export type OnHideFieldBrowser = () => void; + +export interface FieldBrowserProps { + /** The timeline's current column headers */ + columnHeaders: ColumnHeader[]; + /** A map of categoryId -> metadata about the fields in that category */ + browserFields: BrowserFields; + /** The height of the field browser */ + height: number; + /** When true, the timeline is loading data */ + isLoading: boolean; + /** + * Overrides the default behavior of the `FieldBrowser` to enable + * "selection" mode, where a field is selected by clicking a button + * instead of dragging it to the timeline + */ + onFieldSelected?: OnFieldSelected; + /** Invoked when a user chooses to view a new set of columns in the timeline */ + onUpdateColumns: OnUpdateColumns; + /** The timeline associated with this field browser */ + timelineId: string; + /** The width of the field browser */ + width: number; +} diff --git a/x-pack/plugins/siem/public/components/filters_global/filters_global.test.tsx b/x-pack/plugins/siem/public/components/filters_global/filters_global.test.tsx index 154da34022fd2..adbd904c5c325 100644 --- a/x-pack/plugins/siem/public/components/filters_global/filters_global.test.tsx +++ b/x-pack/plugins/siem/public/components/filters_global/filters_global.test.tsx @@ -15,7 +15,7 @@ describe('rendering', () => { test('renders correctly', () => { const wrapper = shallow( -

Additional filters here.

+

{'Additional filters here.'}

); expect(toJson(wrapper)).toMatchSnapshot(); diff --git a/x-pack/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap index 4f46d2f32351d..898a5f5c4f3c2 100644 --- a/x-pack/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap @@ -4,7 +4,7 @@ exports[`Flyout rendering it renders correctly against snapshot 1`] = ` void; createTimeline: ActionCreator<{ id: string; show?: boolean }>; - toggleLock: ActionCreator<{ linkToId: inputsModel.InputsModelId }>; + toggleLock: ActionCreator<{ linkToId: InputsModelId }>; updateDescription: ActionCreator<{ id: string; description: string }>; updateIsFavorite: ActionCreator<{ id: string; isFavorite: boolean }>; updateNote: UpdateNote; @@ -171,7 +172,13 @@ const mapDispatchToProps = (dispatch: Dispatch, { timelineId }: OwnProps) => ({ ); }, createTimeline: ({ id, show }: { id: string; show?: boolean }) => { - dispatch(timelineActions.createTimeline({ id, columns: defaultHeaders, show })); + dispatch( + timelineActions.createTimeline({ + id, + columns: defaultHeaders, + show, + }) + ); }, updateDescription: ({ id, description }: { id: string; description: string }) => { dispatch(timelineActions.updateDescription({ id, description })); @@ -188,7 +195,7 @@ const mapDispatchToProps = (dispatch: Dispatch, { timelineId }: OwnProps) => ({ updateTitle: ({ id, title }: { id: string; title: string }) => { dispatch(timelineActions.updateTitle({ id, title })); }, - toggleLock: ({ linkToId }: { linkToId: inputsModel.InputsModelId }) => { + toggleLock: ({ linkToId }: { linkToId: InputsModelId }) => { dispatch(inputsActions.toggleTimelineLinkTo({ linkToId })); }, }); diff --git a/x-pack/plugins/siem/public/components/flyout/index.test.tsx b/x-pack/plugins/siem/public/components/flyout/index.test.tsx index bb587a824b6fd..7d9f176c88d15 100644 --- a/x-pack/plugins/siem/public/components/flyout/index.test.tsx +++ b/x-pack/plugins/siem/public/components/flyout/index.test.tsx @@ -10,7 +10,7 @@ import { set } from 'lodash/fp'; import * as React from 'react'; import { ActionCreator } from 'typescript-fsa'; -import { mockGlobalState, TestProviders } from '../../mock'; +import { apolloClientObservable, mockGlobalState, TestProviders } from '../../mock'; import { createStore, State } from '../../store'; import { mockDataProviders } from '../timeline/data_providers/mock/mock_data_providers'; @@ -60,7 +60,7 @@ describe('Flyout', () => { test('it renders the title field when its state is set to flyout is true', () => { const stateShowIsTrue = set('timeline.timelineById.test.show', true, state); - const storeShowIsTrue = createStore(stateShowIsTrue); + const storeShowIsTrue = createStore(stateShowIsTrue, apolloClientObservable); const wrapper = mount( @@ -83,7 +83,7 @@ describe('Flyout', () => { test('it does NOT render the fly out button when its state is set to flyout is true', () => { const stateShowIsTrue = set('timeline.timelineById.test.show', true, state); - const storeShowIsTrue = createStore(stateShowIsTrue); + const storeShowIsTrue = createStore(stateShowIsTrue, apolloClientObservable); const wrapper = mount( @@ -101,7 +101,7 @@ describe('Flyout', () => { test('it renders the flyout body', () => { const stateShowIsTrue = set('timeline.timelineById.test.show', true, state); - const storeShowIsTrue = createStore(stateShowIsTrue); + const storeShowIsTrue = createStore(stateShowIsTrue, apolloClientObservable); const wrapper = mount( @@ -111,7 +111,7 @@ describe('Flyout', () => { timelineId="test" usersViewing={usersViewing} > -

Fake flyout body

+

{'Fake flyout body'}

); @@ -130,7 +130,7 @@ describe('Flyout', () => { mockDataProviders, state ); - const storeWithDataProviders = createStore(stateWithDataProviders); + const storeWithDataProviders = createStore(stateWithDataProviders, apolloClientObservable); const wrapper = mount( @@ -152,7 +152,7 @@ describe('Flyout', () => { mockDataProviders, state ); - const storeWithDataProviders = createStore(stateWithDataProviders); + const storeWithDataProviders = createStore(stateWithDataProviders, apolloClientObservable); const wrapper = mount( @@ -214,7 +214,7 @@ describe('Flyout', () => { test('should call the onClose when the close button is clicked', () => { const stateShowIsTrue = set('timeline.timelineById.test.show', true, state); - const storeShowIsTrue = createStore(stateShowIsTrue); + const storeShowIsTrue = createStore(stateShowIsTrue, apolloClientObservable); const showTimeline = (jest.fn() as unknown) as ActionCreator<{ id: string; show: boolean }>; const wrapper = mount( diff --git a/x-pack/plugins/siem/public/components/flyout/index.tsx b/x-pack/plugins/siem/public/components/flyout/index.tsx index 9d8ad341d3b13..90e675f067548 100644 --- a/x-pack/plugins/siem/public/components/flyout/index.tsx +++ b/x-pack/plugins/siem/public/components/flyout/index.tsx @@ -19,9 +19,10 @@ import { FlyoutButton } from './button'; import { Pane } from './pane'; import { timelineActions } from '../../store/actions'; import { DEFAULT_TIMELINE_WIDTH } from '../timeline/body/helpers'; +import { trackUiAction as track } from '../../lib/track_usage'; /** The height in pixels of the flyout header, exported for use in height calculations */ -export const flyoutHeaderHeight: number = 48; +export const flyoutHeaderHeight: number = 60; export const Badge = styled(EuiBadge)` position: absolute; @@ -100,7 +101,10 @@ export const FlyoutComponent = pure( dataProviders={dataProviders!} show={!show} timelineId={timelineId} - onOpen={() => showTimeline!({ id: timelineId, show: true })} + onOpen={() => { + track('open_timeline'); + showTimeline!({ id: timelineId, show: true }); + }} /> ) diff --git a/x-pack/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap index 6ab67aae04e18..31eaf4f56d7bc 100644 --- a/x-pack/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap @@ -4,7 +4,7 @@ exports[`Pane renders correctly against snapshot 1`] = ` I am a child of flyout - , `; diff --git a/x-pack/plugins/siem/public/components/flyout/pane/index.test.tsx b/x-pack/plugins/siem/public/components/flyout/pane/index.test.tsx index 8a8af8ad810a0..6f0f7a45e5c4e 100644 --- a/x-pack/plugins/siem/public/components/flyout/pane/index.test.tsx +++ b/x-pack/plugins/siem/public/components/flyout/pane/index.test.tsx @@ -30,7 +30,7 @@ describe('Pane', () => { usersViewing={usersViewing} width={testWidth} > - I am a child of flyout, + {'I am a child of flyout'} ); @@ -48,7 +48,7 @@ describe('Pane', () => { usersViewing={usersViewing} width={testWidth} > - I am a child of flyout, + {'I am a child of flyout'} ); @@ -67,7 +67,7 @@ describe('Pane', () => { usersViewing={usersViewing} width={testWidth} > - I am a child of flyout, + {'I am a child of flyout'}
); @@ -91,7 +91,7 @@ describe('Pane', () => { usersViewing={usersViewing} width={testWidth} > - I am a child of flyout, + {'I am a child of flyout'}
); @@ -115,7 +115,7 @@ describe('Pane', () => { usersViewing={usersViewing} width={testWidth} > - I am a child of flyout, + {'I am a child of flyout'} ); @@ -139,7 +139,7 @@ describe('Pane', () => { usersViewing={usersViewing} width={testWidth} > - I am a child of flyout, + {'I am a child of flyout'} ); @@ -163,7 +163,7 @@ describe('Pane', () => { usersViewing={usersViewing} width={testWidth} > - I am a child of flyout, + {'I am a child of flyout'} ); @@ -187,7 +187,7 @@ describe('Pane', () => { usersViewing={usersViewing} width={testWidth} > - I am a mock body, + {'I am a mock body'} ); @@ -211,7 +211,7 @@ describe('Pane', () => { usersViewing={usersViewing} width={testWidth} > - I am a mock child, + {'I am a mock child'} ); diff --git a/x-pack/plugins/siem/public/components/formatted_bytes/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/formatted_bytes/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..2ee75c47f14c1 --- /dev/null +++ b/x-pack/plugins/siem/public/components/formatted_bytes/__snapshots__/index.test.tsx.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`formatted_bytes PreferenceFormattedBytes rendering renders correctly against snapshot 1`] = ` + + 2.676MB + +`; diff --git a/x-pack/plugins/siem/public/components/formatted_bytes/index.test.tsx b/x-pack/plugins/siem/public/components/formatted_bytes/index.test.tsx new file mode 100644 index 0000000000000..a4fbb408a9e7b --- /dev/null +++ b/x-pack/plugins/siem/public/components/formatted_bytes/index.test.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mount, shallow } from 'enzyme'; +import toJson from 'enzyme-to-json'; +import * as React from 'react'; + +import { mockFrameworks, TestProviders } from '../../mock'; + +import { PreferenceFormattedBytes } from '.'; + +describe('formatted_bytes', () => { + describe('PreferenceFormattedBytes', () => { + describe('rendering', () => { + const bytes = '2806422'; + + test('renders correctly against snapshot', () => { + const wrapper = shallow(); + expect(toJson(wrapper)).toMatchSnapshot(); + }); + + test('it renders bytes to hardcoded format when no configuration exists', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual('2.676MB'); + }); + + test('it renders bytes according to the default format', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual('2.676MB'); + }); + + test('it renders bytes supplied as a number according to the default format', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual('2.676MB'); + }); + + test('it renders bytes according to new format', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual('3MB'); + }); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/components/formatted_bytes/index.tsx b/x-pack/plugins/siem/public/components/formatted_bytes/index.tsx new file mode 100644 index 0000000000000..b965471f65f91 --- /dev/null +++ b/x-pack/plugins/siem/public/components/formatted_bytes/index.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; +import { useContext } from 'react'; +import numeral from '@elastic/numeral'; + +import { + AppKibanaFrameworkAdapter, + KibanaConfigContext, +} from '../../lib/adapters/framework/kibana_framework_adapter'; + +export const PreferenceFormattedBytes = React.memo<{ value: string | number }>(({ value }) => { + const config: Partial = useContext(KibanaConfigContext); + return ( + <> + {config.bytesFormat + ? numeral(value).format(config.bytesFormat) + : numeral(value).format('0,0.[000]b')} + + ); +}); diff --git a/x-pack/plugins/siem/public/components/formatted_date/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/formatted_date/__snapshots__/index.test.tsx.snap index b4d6579d20228..0f9cf1ba89f9c 100644 --- a/x-pack/plugins/siem/public/components/formatted_date/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/siem/public/components/formatted_date/__snapshots__/index.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`PreferenceFormattedDate rendering renders correctly against snapshot 1`] = ` +exports[`formatted_date PreferenceFormattedDate rendering renders correctly against snapshot 1`] = ` diff --git a/x-pack/plugins/siem/public/components/formatted_date/index.test.tsx b/x-pack/plugins/siem/public/components/formatted_date/index.test.tsx index dec7025489816..10bd3b8613c0f 100644 --- a/x-pack/plugins/siem/public/components/formatted_date/index.test.tsx +++ b/x-pack/plugins/siem/public/components/formatted_date/index.test.tsx @@ -10,70 +10,201 @@ import moment from 'moment-timezone'; import * as React from 'react'; import { AppTestingFrameworkAdapter } from '../../lib/adapters/framework/testing_framework_adapter'; -import { mockFrameworks } from '../../mock'; - -import { KibanaConfigContext, PreferenceFormattedDate } from '.'; - -describe('PreferenceFormattedDate', () => { - describe('rendering', () => { - const isoDateString = '2019-02-25T22:27:05.000Z'; - const isoDate = new Date(isoDateString); - const configFormattedDateString = ( - dateString: string, - config: Partial - ): string => - moment - .tz( - dateString, - config.dateFormatTz! === 'Browser' ? config.timezone! : config.dateFormatTz! - ) - .format(config.dateFormat); - - test('renders correctly against snapshot', () => { - const wrapper = shallow(); - expect(toJson(wrapper)).toMatchSnapshot(); +import { mockFrameworks, TestProviders } from '../../mock'; + +import { PreferenceFormattedDate, FormattedDate, getMaybeDate } from '.'; +import { getEmptyValue } from '../empty_value'; +import { KibanaConfigContext } from '../../lib/adapters/framework/kibana_framework_adapter'; + +describe('formatted_date', () => { + describe('PreferenceFormattedDate', () => { + describe('rendering', () => { + const isoDateString = '2019-02-25T22:27:05.000Z'; + const isoDate = new Date(isoDateString); + const configFormattedDateString = ( + dateString: string, + config: Partial + ): string => + moment + .tz( + dateString, + config.dateFormatTz! === 'Browser' ? config.timezone! : config.dateFormatTz! + ) + .format(config.dateFormat); + + test('renders correctly against snapshot', () => { + const wrapper = shallow(); + expect(toJson(wrapper)).toMatchSnapshot(); + }); + + test('it renders the UTC ISO8601 date string supplied when no configuration exists', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual(isoDateString); + }); + + test('it renders the UTC ISO8601 date supplied when the default configuration exists', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual( + configFormattedDateString(isoDateString, mockFrameworks.default_UTC) + ); + }); + + test('it renders the correct tz when the default browser configuration exists', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual( + configFormattedDateString(isoDateString, mockFrameworks.default_browser) + ); + }); + + test('it renders the correct tz when a non-UTC configuration exists', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual( + configFormattedDateString(isoDateString, mockFrameworks.default_MT) + ); + }); }); + }); - test('it renders the UTC ISO8601 date string supplied when no configuration exists', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual(isoDateString); + describe('FormattedDate', () => { + describe('rendering', () => { + test('it renders against a numeric epoch', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual('May 28, 2019 @ 21:35:39.000'); + }); + + test('it renders against a string epoch', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual('May 28, 2019 @ 21:35:39.000'); + }); + + test('it renders against a ISO string', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual('May 28, 2019 @ 22:04:49.957'); + }); + + test('it renders against an empty string as an empty string placeholder', () => { + const wrapper = mount( + + + + + + ); + expect(wrapper.text()).toEqual('(Empty String)'); + }); + + test('it renders against an null as a EMPTY_VALUE', () => { + const wrapper = mount( + + + + + + ); + expect(wrapper.text()).toEqual(getEmptyValue()); + }); + + test('it renders against an undefined as a EMPTY_VALUE', () => { + const wrapper = mount( + + + + + + ); + expect(wrapper.text()).toEqual(getEmptyValue()); + }); + + test('it renders against an invalid date time as just the string its self', () => { + const wrapper = mount( + + + + + + ); + expect(wrapper.text()).toEqual('Rebecca Evan Braden'); + }); }); + }); - test('it renders the UTC ISO8601 date supplied when the default configuration exists', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual( - configFormattedDateString(isoDateString, mockFrameworks.default_UTC) - ); + describe('getMaybeDate', () => { + test('returns empty string as invalid date', () => { + expect(getMaybeDate('').isValid()).toBe(false); }); - test('it renders the correct tz when the default browser configuration exists', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual( - configFormattedDateString(isoDateString, mockFrameworks.default_browser) - ); + test('returns string with empty spaces as invalid date', () => { + expect(getMaybeDate(' ').isValid()).toBe(false); }); - test('it renders the correct tz when a non-UTC configuration exists', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual( - configFormattedDateString(isoDateString, mockFrameworks.default_MT) + test('returns string date time as valid date', () => { + expect(getMaybeDate('2019-05-28T23:05:28.405Z').isValid()).toBe(true); + }); + + test('returns string date time as the date we expect', () => { + expect(getMaybeDate('2019-05-28T23:05:28.405Z').toISOString()).toBe( + '2019-05-28T23:05:28.405Z' ); }); + + test('returns plain string number as epoch as valid date', () => { + expect(getMaybeDate('1559084770612').isValid()).toBe(true); + }); + + test('returns plain string number as the date we expect', () => { + expect( + getMaybeDate('1559084770612') + .toDate() + .toISOString() + ).toBe('2019-05-28T23:06:10.612Z'); + }); + + test('returns plain number as epoch as valid date', () => { + expect(getMaybeDate(1559084770612).isValid()).toBe(true); + }); + + test('returns plain number as epoch as the date we expect', () => { + expect( + getMaybeDate(1559084770612) + .toDate() + .toISOString() + ).toBe('2019-05-28T23:06:10.612Z'); + }); + + test('returns a short date time string as an epoch (sadly) so this is ambiguous', () => { + expect( + getMaybeDate('20190101') + .toDate() + .toISOString() + ).toBe('1970-01-01T05:36:30.101Z'); + }); }); }); diff --git a/x-pack/plugins/siem/public/components/formatted_date/index.tsx b/x-pack/plugins/siem/public/components/formatted_date/index.tsx index 00fc8e06d1277..d6206358c0d04 100644 --- a/x-pack/plugins/siem/public/components/formatted_date/index.tsx +++ b/x-pack/plugins/siem/public/components/formatted_date/index.tsx @@ -6,25 +6,42 @@ import moment from 'moment-timezone'; import * as React from 'react'; +import { useContext } from 'react'; import { pure } from 'recompose'; -import { AppKibanaFrameworkAdapter } from '../../lib/adapters/framework/kibana_framework_adapter'; +import { isString } from 'lodash/fp'; +import { + AppKibanaFrameworkAdapter, + KibanaConfigContext, +} from '../../lib/adapters/framework/kibana_framework_adapter'; import { getOrEmptyTagFromValue } from '../empty_value'; import { LocalizedDateTooltip } from '../localized_date_tooltip'; -export const KibanaConfigContext = React.createContext>({}); - -export const PreferenceFormattedDate = pure<{ value: Date }>(({ value }) => ( - - {(config: Partial) => { - return config && config.dateFormat && config.dateFormatTz && config.timezone +export const PreferenceFormattedDate = pure<{ value: Date }>(({ value }) => { + const config: Partial = useContext(KibanaConfigContext); + return ( + <> + {config.dateFormat && config.dateFormatTz && config.timezone ? moment .tz(value, config.dateFormatTz === 'Browser' ? config.timezone : config.dateFormatTz) .format(config.dateFormat) - : moment.utc(value).toISOString(); - }} - -)); + : moment.utc(value).toISOString()} + + ); +}); + +export const getMaybeDate = (value: string | number): moment.Moment => { + if (isString(value) && value.trim() !== '') { + const maybeDate = moment(new Date(value)); + if (maybeDate.isValid() || isNaN(+value)) { + return maybeDate; + } else { + return moment(new Date(+value)); + } + } else { + return moment(new Date(value)); + } +}; /** * Renders the specified date value in a format determined by the user's preferences, @@ -37,18 +54,18 @@ export const PreferenceFormattedDate = pure<{ value: Date }>(({ value }) => ( export const FormattedDate = pure<{ fieldName: string; value?: string | number | null; -}>(({ value, fieldName }) => { - if (value == null) { - return getOrEmptyTagFromValue(value); +}>( + ({ value, fieldName }): JSX.Element => { + if (value == null) { + return getOrEmptyTagFromValue(value); + } + const maybeDate = getMaybeDate(value); + return maybeDate.isValid() ? ( + + + + ) : ( + getOrEmptyTagFromValue(value) + ); } - - const maybeDate = moment(new Date(value)); - - return maybeDate.isValid() ? ( - - - - ) : ( - getOrEmptyTagFromValue(value) - ); -}); +); diff --git a/x-pack/plugins/siem/public/components/formatted_duration/tooltip/index.tsx b/x-pack/plugins/siem/public/components/formatted_duration/tooltip/index.tsx index b3f09ef90dcac..512740f29bcc4 100644 --- a/x-pack/plugins/siem/public/components/formatted_duration/tooltip/index.tsx +++ b/x-pack/plugins/siem/public/components/formatted_duration/tooltip/index.tsx @@ -7,6 +7,7 @@ import { EuiToolTip } from '@elastic/eui'; import * as React from 'react'; import { pure } from 'recompose'; +import { FormattedMessage } from '@kbn/i18n/react'; import styled from 'styled-components'; import { getHumanizedDuration } from '../helpers'; @@ -22,7 +23,11 @@ export const FormattedDurationTooltipContent = pure<{ <> {tooltipTitle != null ?

{tooltipTitle}

: null}

{getHumanizedDuration(maybeDurationNanoseconds)}

-

raw: {maybeDurationNanoseconds}

+

+ + {': '} + {maybeDurationNanoseconds} +

)); diff --git a/x-pack/plugins/siem/public/components/formatted_ip/index.tsx b/x-pack/plugins/siem/public/components/formatted_ip/index.tsx index 9e65f1eaac090..bde2b394c2f76 100644 --- a/x-pack/plugins/siem/public/components/formatted_ip/index.tsx +++ b/x-pack/plugins/siem/public/components/formatted_ip/index.tsx @@ -8,12 +8,11 @@ import { isArray, isEmpty, isString, uniq } from 'lodash/fp'; import * as React from 'react'; import { pure } from 'recompose'; -import { escapeQueryValue } from '../../lib/keury'; import { DragEffects, DraggableWrapper } from '../drag_and_drop/draggable_wrapper'; import { escapeDataProviderId } from '../drag_and_drop/helpers'; import { getOrEmptyTagFromValue } from '../empty_value'; import { IPDetailsLink } from '../links'; -import { DataProvider } from '../timeline/data_providers/data_provider'; +import { DataProvider, IS_OPERATOR } from '../timeline/data_providers/data_provider'; import { Provider } from '../timeline/data_providers/provider'; import { TruncatableText } from '../truncatable_text'; import { parseQueryValue } from '../timeline/body/renderers/parse_query_value'; @@ -54,7 +53,8 @@ const getDataProvider = ({ name: `${fieldName}: ${parseQueryValue(address)}`, queryMatch: { field: fieldName, - value: escapeQueryValue(parseQueryValue(address)), + value: parseQueryValue(address), + operator: IS_OPERATOR, }, excluded: false, kqlQuery: '', diff --git a/x-pack/plugins/siem/public/components/header_page/__snapshots__/header_page.test.tsx.snap b/x-pack/plugins/siem/public/components/header_page/__snapshots__/header_page.test.tsx.snap index 5b57944504b91..280acc0c63334 100644 --- a/x-pack/plugins/siem/public/components/header_page/__snapshots__/header_page.test.tsx.snap +++ b/x-pack/plugins/siem/public/components/header_page/__snapshots__/header_page.test.tsx.snap @@ -2,6 +2,8 @@ exports[`rendering renders correctly 1`] = ` diff --git a/x-pack/plugins/siem/public/components/header_page/header_page.test.tsx b/x-pack/plugins/siem/public/components/header_page/header_page.test.tsx index 31a849071d7b5..9d9efa0b51d6b 100644 --- a/x-pack/plugins/siem/public/components/header_page/header_page.test.tsx +++ b/x-pack/plugins/siem/public/components/header_page/header_page.test.tsx @@ -13,8 +13,13 @@ import { HeaderPage } from './index'; describe('rendering', () => { test('renders correctly', () => { const wrapper = shallow( - -

My test supplement.

+ +

{'My test supplement.'}

); expect(toJson(wrapper)).toMatchSnapshot(); diff --git a/x-pack/plugins/siem/public/components/header_page/header_page.tsx b/x-pack/plugins/siem/public/components/header_page/header_page.tsx index 921e915df510a..65e9be37baffd 100644 --- a/x-pack/plugins/siem/public/components/header_page/header_page.tsx +++ b/x-pack/plugins/siem/public/components/header_page/header_page.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui'; +import { EuiBetaBadge, EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui'; import React from 'react'; import { pure } from 'recompose'; import styled from 'styled-components'; @@ -18,25 +18,42 @@ const Header = styled.header` `; export interface HeaderPageProps { + badgeLabel?: string; + badgeTooltip?: string; children?: React.ReactNode; subtitle?: string | React.ReactNode; title: string | React.ReactNode; + 'data-test-subj'?: string; } -export const HeaderPage = pure(({ children, subtitle, title }) => ( -
- - - -

{title}

-
+export const HeaderPage = pure( + ({ badgeLabel, badgeTooltip, children, subtitle, title, ...rest }) => ( +
+ + + +

+ {title} + {badgeLabel && ( + <> + {' '} + + + )} +

+
- - {subtitle} - -
+ + {subtitle} + + - {children && {children}} -
-
-)); + {children && {children}} +
+
+ ) +); diff --git a/x-pack/plugins/siem/public/components/header_panel/header_panel.test.tsx b/x-pack/plugins/siem/public/components/header_panel/header_panel.test.tsx index 4712a3cb996a3..2f0dbd735a96b 100644 --- a/x-pack/plugins/siem/public/components/header_panel/header_panel.test.tsx +++ b/x-pack/plugins/siem/public/components/header_panel/header_panel.test.tsx @@ -14,7 +14,7 @@ describe('rendering', () => { test('renders correctly', () => { const wrapper = shallow( -

My test supplement.

+

{'My test supplement.'}

); expect(toJson(wrapper)).toMatchSnapshot(); diff --git a/x-pack/plugins/siem/public/components/help_menu/help_menu.tsx b/x-pack/plugins/siem/public/components/help_menu/help_menu.tsx index 44f9ed6b3ae9b..eb97e81a78e61 100644 --- a/x-pack/plugins/siem/public/components/help_menu/help_menu.tsx +++ b/x-pack/plugins/siem/public/components/help_menu/help_menu.tsx @@ -34,7 +34,7 @@ export class HelpMenuComponent extends React.PureComponent { - + diff --git a/x-pack/plugins/siem/public/components/lazy_accordion/index.tsx b/x-pack/plugins/siem/public/components/lazy_accordion/index.tsx index de9d8cf3dcf96..5ed9a3b623c1c 100644 --- a/x-pack/plugins/siem/public/components/lazy_accordion/index.tsx +++ b/x-pack/plugins/siem/public/components/lazy_accordion/index.tsx @@ -9,6 +9,9 @@ import * as React from 'react'; type Props = Pick> & { forceExpand?: boolean; + onCollapse?: () => void; + onExpand?: () => void; + renderExpandedContent: (expanded: boolean) => React.ReactNode; }; interface State { @@ -31,7 +34,7 @@ interface State { * the real `EuiAccordion`. */ export class LazyAccordion extends React.PureComponent { - constructor(props: EuiAccordionProps) { + constructor(props: Props) { super(props); this.state = { @@ -39,14 +42,6 @@ export class LazyAccordion extends React.PureComponent { }; } - public onCollapsedClick = () => { - this.setState({ expanded: true }); - }; - - public onExpandedClick = () => { - this.setState({ expanded: false }); - }; - public render() { const { id, @@ -54,8 +49,8 @@ export class LazyAccordion extends React.PureComponent { buttonContent, forceExpand, extraAction, + renderExpandedContent, paddingSize, - children, } = this.props; return ( @@ -74,7 +69,7 @@ export class LazyAccordion extends React.PureComponent { > <> - {children} + {renderExpandedContent(this.state.expanded)} ) : ( { id={id} onClick={this.onCollapsedClick} paddingSize={paddingSize} - > - <> - + /> )} ); } + + private onCollapsedClick = () => { + const { onExpand } = this.props; + + this.setState({ expanded: true }); + + if (onExpand != null) { + onExpand(); + } + }; + + private onExpandedClick = () => { + const { onCollapse } = this.props; + + this.setState({ expanded: false }); + + if (onCollapse != null) { + onCollapse(); + } + }; } diff --git a/x-pack/plugins/siem/public/components/link_to/redirect_to_hosts.tsx b/x-pack/plugins/siem/public/components/link_to/redirect_to_hosts.tsx index b9de328ca47bc..17cb4b8955958 100644 --- a/x-pack/plugins/siem/public/components/link_to/redirect_to_hosts.tsx +++ b/x-pack/plugins/siem/public/components/link_to/redirect_to_hosts.tsx @@ -5,7 +5,9 @@ */ import React from 'react'; -import { Redirect, RouteComponentProps } from 'react-router-dom'; +import { RouteComponentProps } from 'react-router-dom'; + +import { RedirectWrapper } from './redirect_wrapper'; export type HostComponentProps = RouteComponentProps<{ hostName: string; @@ -15,6 +17,6 @@ export const RedirectToHostsPage = ({ match: { params: { hostName }, }, -}: HostComponentProps) => ; +}: HostComponentProps) => ; export const getHostsUrl = () => '#/link-to/hosts'; diff --git a/x-pack/plugins/siem/public/components/link_to/redirect_to_network.tsx b/x-pack/plugins/siem/public/components/link_to/redirect_to_network.tsx index bbcd661d64716..1dae48618d730 100644 --- a/x-pack/plugins/siem/public/components/link_to/redirect_to_network.tsx +++ b/x-pack/plugins/siem/public/components/link_to/redirect_to_network.tsx @@ -5,7 +5,9 @@ */ import React from 'react'; -import { Redirect, RouteComponentProps } from 'react-router-dom'; +import { RouteComponentProps } from 'react-router-dom'; + +import { RedirectWrapper } from './redirect_wrapper'; export type NetworkComponentProps = RouteComponentProps<{ ip: string; @@ -15,6 +17,6 @@ export const RedirectToNetworkPage = ({ match: { params: { ip }, }, -}: NetworkComponentProps) => ; +}: NetworkComponentProps) => ; export const getNetworkUrl = () => '#/link-to/network'; diff --git a/x-pack/plugins/siem/public/components/link_to/redirect_to_overview.tsx b/x-pack/plugins/siem/public/components/link_to/redirect_to_overview.tsx index 1a33b5d370c62..d52ce6acdae8e 100644 --- a/x-pack/plugins/siem/public/components/link_to/redirect_to_overview.tsx +++ b/x-pack/plugins/siem/public/components/link_to/redirect_to_overview.tsx @@ -5,8 +5,8 @@ */ import React from 'react'; -import { Redirect } from 'react-router-dom'; +import { RedirectWrapper } from './redirect_wrapper'; -export const RedirectToOverviewPage = () => ; +export const RedirectToOverviewPage = () => ; export const getOverviewUrl = () => '#/link-to/overview'; diff --git a/x-pack/plugins/siem/public/components/link_to/redirect_to_timelines.tsx b/x-pack/plugins/siem/public/components/link_to/redirect_to_timelines.tsx index 78520eb0534b9..04912dff30f91 100644 --- a/x-pack/plugins/siem/public/components/link_to/redirect_to_timelines.tsx +++ b/x-pack/plugins/siem/public/components/link_to/redirect_to_timelines.tsx @@ -5,10 +5,10 @@ */ import React from 'react'; -import { Redirect } from 'react-router-dom'; +import { RedirectWrapper } from './redirect_wrapper'; export const TIMELINES_PAGE_NAME = 'timelines'; -export const RedirectToTimelinesPage = () => ; +export const RedirectToTimelinesPage = () => ; export const getTimelinesUrl = () => `#/link-to/${TIMELINES_PAGE_NAME}`; diff --git a/x-pack/plugins/siem/public/components/link_to/redirect_wrapper.tsx b/x-pack/plugins/siem/public/components/link_to/redirect_wrapper.tsx new file mode 100644 index 0000000000000..2b56dd44c970a --- /dev/null +++ b/x-pack/plugins/siem/public/components/link_to/redirect_wrapper.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { Redirect } from 'react-router-dom'; +import { scrollToTop } from '../scroll_to_top'; + +export interface RedirectWrapperProps { + to: string; +} + +export const RedirectWrapper = ({ to }: RedirectWrapperProps) => { + scrollToTop(); + return ; +}; diff --git a/x-pack/plugins/siem/public/components/load_more_table/index.test.tsx b/x-pack/plugins/siem/public/components/load_more_table/index.test.tsx index 1e17a0676def2..2d490dd4b751e 100644 --- a/x-pack/plugins/siem/public/components/load_more_table/index.test.tsx +++ b/x-pack/plugins/siem/public/components/load_more_table/index.test.tsx @@ -27,7 +27,7 @@ describe('Load More Table Component', () => { columns={getHostsColumns()} hasNextPage={mockData.Hosts.pageInfo.hasNextPage!} headerCount={1} - headerSupplement={

My test supplement.

} + headerSupplement={

{'My test supplement.'}

} headerTitle="Hosts" headerTooltip="My test tooltip" headerUnit="Test Unit" @@ -52,7 +52,7 @@ describe('Load More Table Component', () => { columns={getHostsColumns()} hasNextPage={mockData.Hosts.pageInfo.hasNextPage!} headerCount={1} - headerSupplement={

My test supplement.

} + headerSupplement={

{'My test supplement.'}

} headerTitle="Hosts" headerTooltip="My test tooltip" headerUnit="Test Unit" @@ -79,7 +79,7 @@ describe('Load More Table Component', () => { columns={getHostsColumns()} hasNextPage={mockData.Hosts.pageInfo.hasNextPage!} headerCount={1} - headerSupplement={

My test supplement.

} + headerSupplement={

{'My test supplement.'}

} headerTitle="Hosts" headerTooltip="My test tooltip" headerUnit="Test Unit" @@ -104,7 +104,7 @@ describe('Load More Table Component', () => { columns={getHostsColumns()} hasNextPage={mockData.Hosts.pageInfo.hasNextPage!} headerCount={1} - headerSupplement={

My test supplement.

} + headerSupplement={

{'My test supplement.'}

} headerTitle="Hosts" headerTooltip="My test tooltip" headerUnit="Test Unit" @@ -134,7 +134,7 @@ describe('Load More Table Component', () => { columns={getHostsColumns()} hasNextPage={mockData.Hosts.pageInfo.hasNextPage!} headerCount={1} - headerSupplement={

My test supplement.

} + headerSupplement={

{'My test supplement.'}

} headerTitle="Hosts" headerTooltip="My test tooltip" headerUnit="Test Unit" @@ -167,7 +167,7 @@ describe('Load More Table Component', () => { columns={getHostsColumns()} hasNextPage={false} headerCount={1} - headerSupplement={

My test supplement.

} + headerSupplement={

{'My test supplement.'}

} headerTitle="Hosts" headerTooltip="My test tooltip" headerUnit="Test Unit" @@ -192,7 +192,7 @@ describe('Load More Table Component', () => { columns={getHostsColumns()} hasNextPage={true} headerCount={1} - headerSupplement={

My test supplement.

} + headerSupplement={

{'My test supplement.'}

} headerTitle="Hosts" headerTooltip="My test tooltip" headerUnit="Test Unit" @@ -221,7 +221,7 @@ describe('Load More Table Component', () => { columns={getHostsColumns()} hasNextPage={true} headerCount={1} - headerSupplement={

My test supplement.

} + headerSupplement={

{'My test supplement.'}

} headerTitle="Hosts" headerTooltip="My test tooltip" headerUnit="Test Unit" @@ -247,7 +247,7 @@ describe('Load More Table Component', () => { columns={sortedHosts} hasNextPage={true} headerCount={1} - headerSupplement={

My test supplement.

} + headerSupplement={

{'My test supplement.'}

} headerTitle="Hosts" headerTooltip="My test tooltip" headerUnit="Test Unit" @@ -276,7 +276,7 @@ describe('Load More Table Component', () => { columns={getHostsColumns()} hasNextPage={mockData.Hosts.pageInfo.hasNextPage!} headerCount={1} - headerSupplement={

My test supplement.

} + headerSupplement={

{'My test supplement.'}

} headerTitle="Hosts" headerTooltip="My test tooltip" headerUnit="Test Unit" @@ -306,7 +306,7 @@ describe('Load More Table Component', () => { columns={getHostsColumns()} hasNextPage={true} headerCount={1} - headerSupplement={

My test supplement.

} + headerSupplement={

{'My test supplement.'}

} headerTitle="Hosts" headerTooltip="My test tooltip" headerUnit="Test Unit" @@ -341,7 +341,7 @@ describe('Load More Table Component', () => { columns={sortedHosts} hasNextPage={true} headerCount={1} - headerSupplement={

My test supplement.

} + headerSupplement={

{'My test supplement.'}

} headerTitle="Hosts" headerTooltip="My test tooltip" headerUnit="Test Unit" diff --git a/x-pack/plugins/siem/public/components/load_more_table/index.tsx b/x-pack/plugins/siem/public/components/load_more_table/index.tsx index 521fbe93a97ab..49ecc14177626 100644 --- a/x-pack/plugins/siem/public/components/load_more_table/index.tsx +++ b/x-pack/plugins/siem/public/components/load_more_table/index.tsx @@ -104,6 +104,7 @@ export interface Columns { truncateText?: boolean; hideForMobile?: boolean; render?: (item: T) => void; + width?: string; } export class LoadMoreTable extends React.PureComponent< @@ -170,7 +171,7 @@ export class LoadMoreTable extends React.PureCompon iconSide="right" onClick={this.onButtonClick} > - Rows: {limit} + {`${i18n.ROWS}: ${limit}`} ); @@ -281,17 +282,17 @@ export class LoadMoreTable extends React.PureCompon } private onButtonClick = () => { - this.setState({ - ...this.state, - isPopoverOpen: !this.state.isPopoverOpen, - }); + this.setState(prevState => ({ + ...prevState, + isPopoverOpen: !prevState.isPopoverOpen, + })); }; private closePopover = () => { - this.setState({ - ...this.state, + this.setState(prevState => ({ + ...prevState, isPopoverOpen: false, - }); + })); }; } diff --git a/x-pack/plugins/siem/public/components/load_more_table/translations.ts b/x-pack/plugins/siem/public/components/load_more_table/translations.ts index 48b4d77ea8afb..3d2d24362ce1f 100644 --- a/x-pack/plugins/siem/public/components/load_more_table/translations.ts +++ b/x-pack/plugins/siem/public/components/load_more_table/translations.ts @@ -17,3 +17,7 @@ export const LOAD_MORE = i18n.translate('xpack.siem.loadingMoreTable.loadMoreDes export const SHOWING = i18n.translate('xpack.siem.loadingMoreTable.showing', { defaultMessage: 'Showing', }); + +export const ROWS = i18n.translate('xpack.siem.loadingMoreTable.rows', { + defaultMessage: 'Rows', +}); diff --git a/x-pack/plugins/siem/public/components/markdown/index.tsx b/x-pack/plugins/siem/public/components/markdown/index.tsx index 8cb1a2bfa5ed2..71fcb566dc109 100644 --- a/x-pack/plugins/siem/public/components/markdown/index.tsx +++ b/x-pack/plugins/siem/public/components/markdown/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiLink, EuiTableRow, EuiTableRowCell, EuiText } from '@elastic/eui'; +import { EuiLink, EuiTableRow, EuiTableRowCell, EuiText, EuiToolTip } from '@elastic/eui'; import * as React from 'react'; import ReactMarkdown from 'react-markdown'; import { pure } from 'recompose'; @@ -45,14 +45,16 @@ export const Markdown = pure<{ raw?: string; size?: 'xs' | 's' | 'm' }>(({ raw, {children} ), link: ({ children, href }: { children: React.ReactNode[]; href?: string }) => ( - - {children} - + + + {children} + + ), }; diff --git a/x-pack/plugins/siem/public/components/markdown/markdown_hint.tsx b/x-pack/plugins/siem/public/components/markdown/markdown_hint.tsx index 559488e8f468c..d05c1598cfce2 100644 --- a/x-pack/plugins/siem/public/components/markdown/markdown_hint.tsx +++ b/x-pack/plugins/siem/public/components/markdown/markdown_hint.tsx @@ -61,11 +61,11 @@ export const MarkdownHint = pure<{ show: boolean }>(({ show }) => ( {i18n.MARKDOWN_HINT_BULLET} {i18n.MARKDOWN_HINT_PREFORMATTED} {i18n.MARKDOWN_HINT_QUOTE} - ~~ + {'~~'} {i18n.MARKDOWN_HINT_STRIKETHROUGH} - ~~ + {'~~'} {i18n.MARKDOWN_HINT_IMAGE_URL} )); diff --git a/x-pack/plugins/siem/public/components/navigation/breadcrumbs/index.test.ts b/x-pack/plugins/siem/public/components/navigation/breadcrumbs/index.test.ts index 42a50590655bf..f7ebba74e0f27 100644 --- a/x-pack/plugins/siem/public/components/navigation/breadcrumbs/index.test.ts +++ b/x-pack/plugins/siem/public/components/navigation/breadcrumbs/index.test.ts @@ -20,6 +20,9 @@ jest.mock('ui/chrome', () => ({ breadcrumbs: { set: jest.fn(), }, + getUiSettingsClient: () => ({ + get: jest.fn(), + }), })); describe('Navigation Breadcrumbs', () => { diff --git a/x-pack/plugins/siem/public/components/navigation/breadcrumbs/index.ts b/x-pack/plugins/siem/public/components/navigation/breadcrumbs/index.ts index fa7246be40cac..b4360755bd060 100644 --- a/x-pack/plugins/siem/public/components/navigation/breadcrumbs/index.ts +++ b/x-pack/plugins/siem/public/components/navigation/breadcrumbs/index.ts @@ -6,7 +6,7 @@ import chrome, { Breadcrumb } from 'ui/chrome'; -import { APP_NAME } from '../../../..'; +import { APP_NAME } from '../../../../common/constants'; import { getBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../pages/hosts/host_details'; import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../pages/network/ip_details'; import { getHostsUrl, getNetworkUrl, getOverviewUrl, getTimelinesUrl } from '../../link_to'; diff --git a/x-pack/plugins/siem/public/components/navigation/tab_navigation/index.tsx b/x-pack/plugins/siem/public/components/navigation/tab_navigation/index.tsx index 1a36a208d4e21..e2663cb2f84a6 100644 --- a/x-pack/plugins/siem/public/components/navigation/tab_navigation/index.tsx +++ b/x-pack/plugins/siem/public/components/navigation/tab_navigation/index.tsx @@ -7,6 +7,7 @@ import { EuiTab, EuiTabs } from '@elastic/eui'; import * as React from 'react'; import { getHostsUrl, getNetworkUrl, getOverviewUrl, getTimelinesUrl } from '../../link_to'; +import { trackUiAction as track } from '../../../lib/track_usage'; import * as i18n from '../translations'; @@ -66,10 +67,10 @@ export class TabNavigation extends React.PureComponent ({ + ...prevState, selectedTabId, - }); + })); } } public render() { @@ -85,10 +86,11 @@ export class TabNavigation extends React.PureComponent { - this.setState({ - ...this.state, + this.setState(prevState => ({ + ...prevState, selectedTabId: id, - }); + })); + track(`tab_${id}`); window.location.assign(href); }; diff --git a/x-pack/plugins/siem/public/components/netflow/index.test.tsx b/x-pack/plugins/siem/public/components/netflow/index.test.tsx index 17fbb238b4824..b53f473e0b51a 100644 --- a/x-pack/plugins/siem/public/components/netflow/index.test.tsx +++ b/x-pack/plugins/siem/public/components/netflow/index.test.tsx @@ -143,7 +143,7 @@ describe('Netflow', () => { .find('[data-test-subj="destination-bytes"]') .first() .text() - ).toEqual('40.000 B'); + ).toEqual('40B'); }); test('it renders destination.geo.continent_name', () => { @@ -278,7 +278,7 @@ describe('Netflow', () => { .find('[data-test-subj="network-bytes"]') .first() .text() - ).toEqual('100.000 B'); + ).toEqual('100B'); }); test('it renders network.community_id', () => { @@ -355,7 +355,7 @@ describe('Netflow', () => { .find('[data-test-subj="source-bytes"]') .first() .text() - ).toEqual('60.000 B'); + ).toEqual('60B'); }); test('it renders source.geo.continent_name', () => { diff --git a/x-pack/plugins/siem/public/components/notes/add_note/new_note.test.tsx b/x-pack/plugins/siem/public/components/notes/add_note/new_note.test.tsx index 3047a5e8a8a1b..3ab556a4e5dc4 100644 --- a/x-pack/plugins/siem/public/components/notes/add_note/new_note.test.tsx +++ b/x-pack/plugins/siem/public/components/notes/add_note/new_note.test.tsx @@ -30,7 +30,7 @@ describe('NewNote', () => { .find('button[role="tab"]') .first() .text() - ).toEqual(i18n.NOTE(1)); + ).toEqual(i18n.NOTE); }); test('it renders a tab labeled "Preview (Markdown)"', () => { diff --git a/x-pack/plugins/siem/public/components/notes/add_note/new_note.tsx b/x-pack/plugins/siem/public/components/notes/add_note/new_note.tsx index ad0945d5ac176..ae71601f05c31 100644 --- a/x-pack/plugins/siem/public/components/notes/add_note/new_note.tsx +++ b/x-pack/plugins/siem/public/components/notes/add_note/new_note.tsx @@ -36,11 +36,11 @@ export const NewNote = pure<{ const tabs = [ { id: 'note', - name: i18n.NOTE(1), + name: i18n.NOTE, content: (