Merge remote-tracking branch 'upstream/master' into feature-secops

This commit is contained in:
FrankHassanabad 2019-04-24 09:20:43 -06:00
commit 6e579b47e4
No known key found for this signature in database
GPG key ID: 0BC9DC0E0023FA5D
954 changed files with 57622 additions and 6200 deletions

View file

@ -9,6 +9,9 @@ charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[package.json]
insert_final_newline = false
[*.{md,asciidoc}]
trim_trailing_whitespace = false
insert_final_newline = false

View file

@ -60,7 +60,12 @@ module.exports = {
plugins: ['prettier'],
rules: Object.assign(
{
'prettier/prettier': ['error'],
'prettier/prettier': [
'error',
{
endOfLine: 'auto',
},
],
},
require('eslint-config-prettier').rules,
require('eslint-config-prettier/react').rules

View file

@ -327,19 +327,36 @@ macOS users on a machine with a discrete graphics card may see significant speed
`yarn debug` will start the server with Node's inspect flag. Kibana's development mode will start three processes on ports `9229`, `9230`, and `9231`. Chrome's developer tools need to be configured to connect to all three connections. Add `localhost:<port>` for each Kibana process in Chrome's developer tools connection tab.
### Unit testing frameworks
Kibana is migrating unit testing from Mocha to Jest. Legacy unit tests still exist in Mocha but all new unit tests should be written in Jest.
Kibana is migrating unit testing from Mocha to Jest. Legacy unit tests still
exist in Mocha but all new unit tests should be written in Jest. Mocha tests
are contained in `__tests__` directories. Whereas Jest tests are stored in
the same directory as source code files with the `.test.js` suffix.
#### Mocha (legacy)
Mocha tests are contained in `__tests__` directories.
### Running specific Kibana tests
#### Jest
Jest tests are stored in the same directory as source code files with the `.test.js` suffix.
The following table outlines possible test file locations and how to invoke them:
### Running Jest Unit Tests
| Test runner | Test location | Runner command (working directory is kibana root) |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| Jest | `src/**/*.test.js`<br>`src/**/*.test.ts` | `node scripts/jest -t regexp [test path]` |
| Jest (integration) | `**/integration_tests/**/*.test.js` | `node scripts/jest_integration -t regexp [test path]` |
| Mocha | `src/**/__tests__/**/*.js`<br>`packages/kbn-datemath/test/**/*.js`<br>`packages/kbn-dev-utils/src/**/__tests__/**/*.js`<br>`tasks/**/__tests__/**/*.js` | `node scripts/mocha --grep=regexp [test path]` |
| Functional | `test/*integration/**/config.js`<br>`test/*functional/**/config.js` | `node scripts/functional_tests_server --config test/[directory]/config.js`<br>`node scripts/functional_test_runner --config test/[directory]/config.js --grep=regexp` |
```bash
node scripts/jest
```
For X-Pack tests located in `x-pack/` see [X-Pack Testing](x-pack/README.md#testing)
Test runner arguments:
- Where applicable, the optional arguments `-t=regexp` or `--grep=regexp` will only run tests or test suites whose descriptions matches the regular expression.
- `[test path]` is the relative path to the test file.
Examples:
- Run the entire elasticsearch_service test suite with yarn:
`node scripts/jest src/core/server/elasticsearch/elasticsearch_service.test.ts`
- Run the jest test case whose description matches 'stops both admin and data clients':
`node scripts/jest -t 'stops both admin and data clients' src/core/server/elasticsearch/elasticsearch_service.test.ts`
- Run the api integration test case whose description matches the given string:
`node scripts/functional_tests_server --config test/api_integration/config.js`
`node scripts/functional_tests_runner --config test/api_integration/config.js --grep='should return 404 if id does not match any sample data sets'`
### Debugging Unit Tests

View file

@ -133,6 +133,32 @@ 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 includes code that is based on facebookincubator/idx, which was
available under a "MIT" license.
MIT License
Copyright (c) 2013-present, Facebook, Inc.
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 includes code that is based on flot-charts, which was available
under a "MIT" license.

View file

@ -0,0 +1,17 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) &gt; [getUpdateErrors$](./kibana-plugin-public.uisettingsclient.getupdateerrors$.md)
## UiSettingsClient.getUpdateErrors$() method
Returns an Observable that notifies subscribers of each error while trying to update the settings, containing the actual Error class.
<b>Signature:</b>
```typescript
getUpdateErrors$(): Rx.Observable<Error>;
```
<b>Returns:</b>
`Rx.Observable<Error>`

View file

@ -26,6 +26,7 @@ export declare class UiSettingsClient
| [getAll()](./kibana-plugin-public.uisettingsclient.getall.md) | | Gets the metadata about all uiSettings, including the type, default value, and user value for each key. |
| [getSaved$()](./kibana-plugin-public.uisettingsclient.getsaved$.md) | | Returns an Observable that notifies subscribers of each update to the uiSettings, including the key, newValue, and oldValue of the setting that changed. |
| [getUpdate$()](./kibana-plugin-public.uisettingsclient.getupdate$.md) | | Returns an Observable that notifies subscribers of each update to the uiSettings, including the key, newValue, and oldValue of the setting that changed. |
| [getUpdateErrors$()](./kibana-plugin-public.uisettingsclient.getupdateerrors$.md) | | Returns an Observable that notifies subscribers of each error while trying to update the settings, containing the actual Error class. |
| [isCustom(key)](./kibana-plugin-public.uisettingsclient.iscustom.md) | | Returns true if the setting is not a part of the uiSettingDefaults, but was either added directly via <code>set()</code>, or is an unknown setting found in the uiSettings saved object |
| [isDeclared(key)](./kibana-plugin-public.uisettingsclient.isdeclared.md) | | Returns true if the key is a "known" uiSetting, meaning it is either defined in the uiSettingDefaults or was previously added as a custom setting via the <code>set()</code> method. |
| [isDefault(key)](./kibana-plugin-public.uisettingsclient.isdefault.md) | | Returns true if the setting has no user-defined value or is unknown |

View file

@ -100,75 +100,3 @@ Authorization: Basic foo_read_only_user password
{es} checks if the user is granted a specific action. If the user is assigned a role that grants a privilege, {es} uses the <<development-rbac-privileges, {kib} privileges>> definition to associate this with the actions, which makes authorizing users more intuitive and flexible programatically.
Once we have authorized the user to perform a specific action, we can execute the request using `callWithInternalUser`.
[[development-rbac-legacy-fallback]]
==== Legacy Fallback
Users have existing roles that rely on index privileges to the `.kibana` index. The legacy fallback uses the `callWithRequest` method when the user doesn't have any application privileges. This relies on the user having index privileges on `.kibana`. The legacy fallback will be available until 7.0.
Within the secured instance of the `SavedObjectsClient` the `_has_privileges` check determines if the user has any index privileges on the `.kibana` index:
[source,js]
----------------------------------
POST /_security/user/_has_privileges
Content-Type: application/json
Authorization: Basic foo_legacy_user password
{
"applications":[
{
"application":"kibana-.kibana",
"resources":["*"],
"privileges":[
"saved_object:dashboard/save"
]
}
],
"index": [
{
"names": ".kibana",
"privileges": ["create", "delete", "read", "view_index_metadata"]
}
]
}
----------------------------------
Here is an example response if the user does not have application privileges, but does have privileges on the `.kibana` index:
[source,js]
----------------------------------
{
"username": "foo_legacy_user",
"has_all_requested": false,
"cluster": {},
"index": {
".kibana": {
"read": true,
"view_index_metadata": true,
"create": true,
"delete": true
}
},
"application": {
"kibana-.kibana": {
"*": {
"saved_object:dashboard/save": false
}
}
}
}
----------------------------------
{kib} automatically detects that the request could be executed against `.kibana` using `callWithRequest` and does so.
When the user first logs into {kib}, if they have no application privileges and will have to rely on the legacy fallback, {kib} logs a deprecation warning similar to the following:
[source,js]
----------------------------------
${username} relies on index privileges on the {kib} index. This is deprecated and will be removed in {kib} 7.0
----------------------------------
[[development-rbac-reserved-roles]]
==== Reserved roles
Ideally, the `kibana_user` and `kibana_dashboard_only_user` roles should only use application privileges, and no longer have index privileges on the `.kibana` index. However, making this switch forces the user to incur downtime if Elasticsearch is upgraded to >= 6.4, and {kib} is running < 6.4. To mitigate this downtime, for the 6.x releases the `kibana_user` and `kibana_dashbord_only_user` roles have both application privileges and index privileges. When {kib} is running >= 6.4 it uses the application privileges to authorize the user, but when {kib} is running < 6.4 {kib} relies on the direct index privileges.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 3.5 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 3.5 MiB

Before After
Before After

View file

@ -21,7 +21,19 @@ You can create a layer that requests data from {es} from the following:
image::maps/images/global_search_bar.png[]
[float]
[role="xpack"]
[[maps-layer-based-filtering]]
=== Filtering a single layer
You can apply a search request to individual layers by setting `Filters` in the layer details panel.
Click the *Add filter* button to add a filter to a layer.
[role="screenshot"]
image::maps/images/layer_search.png[]
[role="xpack"]
[[maps-search-across-multiple-indices]]
=== Searching across multiple indices
Your map might contain multiple {es} indices.

View file

@ -1,21 +1,76 @@
[role="xpack"]
[[vector-style]]
=== Vector style
=== Vector styling
*Border color*:: Defines the border color of the vector features.
When styling a vector layer, you can customize your data by property, such as size and color.
For each property, you can specify whether to use a constant or dynamic value for the style.
*Border width*:: Defines the border width of the vector features.
[float]
[[maps-vector-style-properties]]
==== Style properties
*Fill color*:: Defines the fill color of the vector features.
You can configure the following properties.
*Symbol size*:: Defines the symbol size of point features.
*Fill color*:: The fill color of the vector features.
+
NOTE: *LineString* and *MultiLineString* geometries do not have fill and do not use the fill color property.
Set border color to style line geometries.
Click the *link* button to toggle between static styling and data-driven styling.
*Border color*:: The border color of the vector features.
*Border width*:: The border width of the vector features.
*Symbol size*:: The symbol size of point features.
[float]
[[maps-vector-style-static]]
==== Static styling
Use static styling to specificy a constant value for a style property.
The image below shows an example of static styling using the <<add-sample-data, Kibana sample web logs>> data set.
The *kibana_sample_data_logs* layer uses static styling for all properties.
[role="screenshot"]
image::maps/images/vector_style_static.png[]
[float]
[[maps-vector-style-data-driven]]
==== Data driven styling
Use data driven styling to symbolize features from a range of numeric property values.
To enable data driven styling, click image:maps/images/gs_link_icon.png[] next to the property.
NOTE: The image:maps/images/gs_link_icon.png[] button is only available for vector features that contain numeric properties.
The image below shows an example of data driven styling using the <<add-sample-data, Kibana sample web logs>> data set.
The *kibana_sample_data_logs* layer uses data driven styling for fill color and symbol size style properties.
* The `hour_of_day` property determines the fill color for each feature based on where the value fits on a linear scale.
Light green circles symbolize documents that occur earlier in the day, and dark green circles symbolize documents that occur later in the day.
* The `bytes` property determines the size of each symbol based on where the value fits on a linear scale.
Smaller circles symbolize documents with smaller payloads, and larger circles symbolize documents with larger payloads.
[role="screenshot"]
image::maps/images/vector_style_dynamic.png[]
NOTE: The *link* button is only available when your vector features contain numeric properties.
[float]
[[maps-vector-style-class]]
==== Class styling
Class styling symbolizes features by class and requires multiple layers.
Use <<maps-layer-based-filtering, layer filtering>> to define the class for each layer, and <<maps-vector-style-static, static styling>> to symbolize each class.
The image below shows an example of class styling using the <<add-sample-data, Kibana sample web logs>> data set.
* The *Mac OS requests* layer applies the filter `machine.os : osx` so the layer only contains Mac OS requests.
The fill color is a static value of green.
* The *Window OS requests* layer applies the filter `machine.os : win*` so the layer only contains Window OS requests.
The fill color is a static value of red.
[role="screenshot"]
image::maps/images/vector_style_class.png[]

View file

@ -11,11 +11,26 @@ coming[8.0.0]
See also <<release-highlights>> and <<release-notes>>.
* <<breaking_80_index_pattern_changes>>
* <<breaking_80_setting_changes>>
//NOTE: The notable-breaking-changes tagged regions are re-used in the
//Installation and Upgrade Guide
[float]
[[breaking_80_index_pattern_changes]]
=== Index pattern changes
[float]
==== Removed support for time-based internal index patterns
*Details:* Time-based interval index patterns were deprecated in 5.x. In 6.x,
you could no longer create time-based interval index patterns, but they continued
to function as expected. Support for these index patterns has been removed in 8.0.
*Impact:* You must migrate your time_based index patterns to a wildcard pattern,
for example, `logstash-*`.
[float]
[[breaking_80_setting_changes]]
=== Settings changes

View file

@ -7,13 +7,43 @@
{kib} supports the following authentication mechanisms:
- Basic Authentication
- SAML Single Sign-On
- <<basic-authentication>>
- <<token-authentication>>
- <<saml>>
[[basic-authentication]]
==== Basic Authentication
Basic Authentication requires a username and password to successfully log in to {kib}. It is enabled by default and based on the Native security realm provided by {es}. For more information about Basic Authentication and built-in users, see {xpack-ref}/setting-up-authentication.html[Setting Up User Authentication].
Basic authentication requires a username and password to successfully log in to {kib}. It is enabled by default and based on the Native security realm provided by {es}. The basic authentication provider uses a Kibana provided login form, and supports authentication using the `Authorization` request header's `Basic` scheme.
The session cookies that are issued by the basic authentication provider are stateless. Therefore, logging out of Kibana when using the basic authentication provider clears the session cookies from the browser but does not invalidate the session cookie for reuse.
For more information about basic authentication and built-in users, see {xpack-ref}/setting-up-authentication.html[Setting Up User Authentication].
[[token-authentication]]
==== Token Authentication
Token authentication allows users to login using the same Kibana provided login form as basic authentication. The token authentication provider is built on {es}'s token APIs. The bearer tokens returned by {es}'s {ref}/security-api-get-token.html[get token API] can be used directly with Kibana using the `Authorization` request header with the `Bearer` scheme.
The session cookies that are issued by the token authentication provider are stateful, and logging out of Kibana invalidates the session cookies for reuse.
Prior to configuring Kibana, ensure token support is enabled in Elasticsearch. See the {ref}/security-api-get-token.html[Elasticsearch token API] documentation for more information.
To enable the token authentication provider in Kibana, set the following value in your `kibana.yml`:
[source,yaml]
--------------------------------------------------------------------------------
xpack.security.authProviders: [token]
--------------------------------------------------------------------------------
The token authentication provider can be used in conjuction with the basic authentication provider. The login form will continue to use the token authentication provider, while enabling applications like `curl` to use the `Authorization` request header with the `Basic` scheme. Set the following in your `kibana.yml`, maintaining the order of the auth providers:
[source,yaml]
--------------------------------------------------------------------------------
xpack.security.authProviders: [token, basic]
--------------------------------------------------------------------------------
[[saml]]
==== SAML Single Sign-On
SAML authentication allows users to log in to {kib} with an external Identity Provider, such as Okta or Auth0. Make sure that SAML is enabled and configured in {es} before setting it up in {kib}. See {xpack-ref}/saml-guide.html[Configuring SAML Single-Sign-On on the Elastic Stack].
@ -49,8 +79,6 @@ 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_.
NOTE: Some Identity Providers support a portal or dashboard from which users can open an application that is integrated with the Identity Provider. This login scenario is known as _Identity Provider initiated login_, and {kib} has limitations with this scenario. Users cannot open {kib} in multiple tabs from the Identity Provider portal in the same browser context if an active {kib} session exists. No issues exist if users open {kib} in multiple tabs using _direct_ {kib} URL.
[float]
===== Access and Refresh Tokens

View file

@ -19,18 +19,3 @@ to assign a specific <<kibana-privileges, Kibana privilege>> at that tenant. Aft
custom role, you should assign this role to the user(s) that you wish to have access.
While multi-tenant installations are supported, the recommended approach to securing access to segments of {kib} is to grant users access to specific spaces.
==== Legacy roles
Prior to {kib} 6.4, {kib} users required index privileges to the `kibana.index`
in {es}. This approach is deprecated starting in 6.4, and you will need to switch to using
<<kibana-privileges>> before 7.0. When a user logs into {kib} and they're using
a legacy role, the following is logged to your {kib} logs:
[source,js]
----------------------------------
<username> relies on index privileges on the Kibana index. This is deprecated and will be removed in Kibana 7.0
----------------------------------
To disable legacy roles from being authorized in {kib}, set `xpack.security.authorization.legacyFallback` to `false`
in your `kibana.yml`.

View file

@ -25,10 +25,6 @@ authorization using <<kibana-privileges>> are disabled. +
Set to `true` to enable audit logging for security events. This is set to `false` by default.
For more details see <<xpack-security-audit-logging>>.
`xpack.security.authorization.legacyFallback`::
Set to `true` (default) to enable the legacy fallback. See <<xpack-security-authorization>>
for more details.
[float]
[[security-ui-settings]]
==== User interface security settings

View file

@ -131,6 +131,12 @@ be lowercase, and conform to {es} {ref}/indices-create-index.html[index name lim
`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.
If `logging.dest.stdout` is set and there is no interactive terminal ("TTY"), this setting
will default to `true`.
`logging.quiet:`:: *Default: false* Set the value of this setting to `true` to
suppress all logging output other than error messages.
@ -300,6 +306,7 @@ include::{docdir}/settings/apm-settings.asciidoc[]
include::{docdir}/settings/dev-settings.asciidoc[]
include::{docdir}/settings/graph-settings.asciidoc[]
include::{docdir}/settings/infrastructure-ui-settings.asciidoc[]
include::{docdir}/settings/i18n-settings.asciidoc[]
include::{docdir}/settings/logs-ui-settings.asciidoc[]
include::{docdir}/settings/ml-settings.asciidoc[]
include::{docdir}/settings/monitoring-settings.asciidoc[]

View file

@ -69,7 +69,8 @@
"kbn:watch": "node scripts/kibana --dev --logging.json=false",
"build:types": "tsc --p tsconfig.types.json",
"core:acceptApiChanges": "node scripts/check_core_api_changes.js --accept",
"kbn:bootstrap": "yarn build:types && node scripts/register_git_hook"
"kbn:bootstrap": "yarn build:types && node scripts/register_git_hook",
"spec_to_console": "node scripts/spec_to_console"
},
"repository": {
"type": "git",
@ -101,10 +102,10 @@
"@babel/polyfill": "^7.2.5",
"@babel/register": "^7.0.0",
"@elastic/datemath": "5.0.2",
"@elastic/eui": "10.0.1",
"@elastic/eui": "10.1.0",
"@elastic/filesaver": "1.1.2",
"@elastic/good": "8.1.1-kibana2",
"@elastic/numeral": "2.3.2",
"@elastic/numeral": "2.3.3",
"@elastic/ui-ace": "0.2.3",
"@kbn/babel-code-parser": "1.0.0",
"@kbn/babel-preset": "1.0.0",
@ -160,6 +161,7 @@
"h2o2": "^8.1.2",
"handlebars": "4.0.13",
"hapi": "^17.5.3",
"hapi-auth-cookie": "^9.0.0",
"hjson": "3.1.0",
"hoek": "^5.0.4",
"http-proxy-agent": "^2.1.0",
@ -206,7 +208,8 @@
"react-color": "^2.13.8",
"react-dom": "^16.8.0",
"react-grid-layout": "^0.16.2",
"react-markdown": "^3.1.4",
"react-input-range": "^1.3.0",
"react-markdown": "^3.4.1",
"react-redux": "^5.0.7",
"react-router-dom": "^4.3.1",
"react-sizeme": "^2.3.6",
@ -271,6 +274,7 @@
"@types/bluebird": "^3.1.1",
"@types/boom": "^7.2.0",
"@types/chance": "^1.0.0",
"@types/cheerio": "^0.22.10",
"@types/chromedriver": "^2.38.0",
"@types/classnames": "^2.2.3",
"@types/d3": "^3.5.41",
@ -283,7 +287,7 @@
"@types/execa": "^0.9.0",
"@types/fetch-mock": "7.2.1",
"@types/getopts": "^2.0.1",
"@types/glob": "^5.0.35",
"@types/glob": "^7.1.1",
"@types/globby": "^8.0.0",
"@types/graphql": "^0.13.1",
"@types/hapi": "^17.0.18",
@ -317,8 +321,9 @@
"@types/redux-actions": "^2.2.1",
"@types/request": "^2.48.1",
"@types/rimraf": "^2.0.2",
"@types/selenium-webdriver": "^3.0.15",
"@types/semver": "^5.5.0",
"@types/sinon": "^5.0.1",
"@types/sinon": "^7.0.0",
"@types/strip-ansi": "^3.0.0",
"@types/styled-components": "^3.0.1",
"@types/supertest": "^2.0.5",
@ -336,7 +341,7 @@
"chance": "1.0.10",
"cheerio": "0.22.0",
"chokidar": "1.6.0",
"chromedriver": "2.46.0",
"chromedriver": "73.0.0",
"classnames": "2.2.5",
"dedent": "^0.7.0",
"delete-empty": "^2.0.0",
@ -377,6 +382,7 @@
"istanbul-instrumenter-loader": "3.0.1",
"jest": "^24.1.0",
"jest-cli": "^24.1.0",
"jest-dom": "^3.1.3",
"jest-raw-loader": "^1.0.1",
"jimp": "0.6.0",
"json5": "^1.0.1",
@ -395,7 +401,7 @@
"multistream": "^2.1.1",
"murmurhash3js": "3.0.1",
"mutation-observer": "^1.0.3",
"nock": "8.0.0",
"nock": "10.0.4",
"node-sass": "^4.9.4",
"normalize-path": "^3.0.0",
"pixelmatch": "4.0.2",
@ -409,7 +415,7 @@
"sass-lint": "^1.12.1",
"selenium-webdriver": "^4.0.0-alpha.1",
"simple-git": "1.37.0",
"sinon": "^5.0.7",
"sinon": "^7.2.2",
"strip-ansi": "^3.0.1",
"supertest": "^3.1.0",
"supertest-as-promised": "^4.0.2",

View file

@ -18,11 +18,9 @@
*/
module.exports = {
presets: [
require.resolve('@babel/preset-typescript'),
require.resolve('@babel/preset-react')
],
presets: [require.resolve('@babel/preset-typescript'), require.resolve('@babel/preset-react')],
plugins: [
require.resolve('@kbn/elastic-idx/babel'),
require.resolve('babel-plugin-add-module-exports'),
// The class properties proposal was merged with the private fields proposal
@ -44,11 +42,7 @@ module.exports = {
/x-pack[\/\\]plugins[\/\\]infra[\/\\].*[\/\\]graphql/,
/x-pack[\/\\]plugins[\/\\]siem[\/\\].*[\/\\]graphql/,
],
plugins: [
[
require.resolve('babel-plugin-typescript-strip-namespaces'),
],
]
}
]
plugins: [[require.resolve('babel-plugin-typescript-strip-namespaces')]],
},
],
};

View file

@ -8,6 +8,7 @@
"@babel/preset-react":"^7.0.0",
"@babel/preset-env": "^7.3.4",
"@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"

View file

@ -38,7 +38,7 @@ export class ToolingLog {
public warning(...args: any[]): void;
public error(errOrMsg: string | Error): void;
public write(...args: any[]): void;
public indent(spaces: number): void;
public indent(spaces?: number): void;
public getWriters(): ToolingLogWriter[];
public setWriters(reporters: ToolingLogWriter[]): void;
public getWritten$(): Rx.Observable<LogMessage>;

View file

@ -0,0 +1,3 @@
/tsconfig.json
/src
/babel/index.test.js

View file

@ -0,0 +1,76 @@
Kibana elastic-idx Library
==========================
The `@kbn/elastic-idx` package provides the `idx` function used for optional
chaining. Currently, the optional chaining draft is in stage 1, making it too
uncertain to add syntax support within Kibana. Other optional chaining
libraries require the Proxy object to be polyfilled for browser support,
however, this polyfill is not fully supported across all browsers that Kibana
requires. The facebookincubator `idx` project
(https://github.com/facebookincubator/idx) provides an answer to this with a
specific implementation that is understood by TypeScript so that type
information does not get lost (unlike lodash get) The `@kbn/elastic-idx`
library makes use the `idx` idiom but differs in the way null values within the
property chain are handled.
Similar to the facebookincubator `idx` project, `@kbn/elastic-idx` also
provides the Babel plugin to transform `idx()` function calls into the expanded
form. This Babel plugin was based off the facebookincubator `idx` Babel
plugin, since the invocation syntax is almost identical, but the transformed
code differs to match how the `@kbn/elastic-idx` library treats null values.
App Usage
----------
Within Kibana, `@kbn/elastic-idx` can be imported and used in any JavaScript or
TypeScript project:
```
import { idx } from '@kbn/elastic-idx';
const obj0 = { a: { b: { c: { d: 'iamdefined' } } } };
const obj1 = { a: { b: null } };
idx(obj0, _ => _.a.b.c.d); // returns 'iamdefined'
idx(obj1, _ => _.a.b.c.e); // returns undefined
idx(obj1, _ => _.a.b); // returns null
```
Build Optimization
-------------------
Similar to the facebookincubator `idx` project, it is NOT RECOMMENDED to use
idx in shipped app code. The implementation details which make
`@kbn/elastic-idx` possible comes at a non-negligible performance cost. This
usually isn't noticable during development, but for production builds, it is
recommended to transform idx calls into native, expanded form JS. Use the
plugin `@kbn/elastic-idx/babel` within your Babel configuration:
```
{ "plugins": [ "@kbn/elastic-idx/babel" ] }
```
The resulting Babel transforms the following:
```
import { idx } from '@kbn/elastic-idx';
const obj = { a: { b: { c: { d: 'iamdefined' } } } };
idx(obj, _ => _.a.b.c.d);
```
into this:
```
obj != null &&
obj.a != null &&
obj.a.b != null &&
obj.a.b.c != null ?
obj.a.b.c.d : undefined
```
Note that this also removes the import statement from the source code, since it
no longer needs to be bundled.
Testing
--------
Tests can be run with `npm test`. This includes "functional" tests that
transform and evaluate idx calls.

View file

@ -0,0 +1,321 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/* @notice
* This product includes code that is based on facebookincubator/idx, which was
* available under a "MIT" license.
*
* MIT License
*
* Copyright (c) 2013-present, Facebook, Inc.
*
* 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.
*/
/* eslint strict: 0, new-cap: 0 */
'use strict';
module.exports = context => {
const t = context.types;
const idxRe = /\bidx\b/;
function checkIdxArguments(file, node) {
const args = node.arguments;
if (args.length !== 2) {
throw file.buildCodeFrameError(node, 'The `idx` function takes exactly two arguments.');
}
const arrowFunction = args[1];
if (!t.isArrowFunctionExpression(arrowFunction)) {
throw file.buildCodeFrameError(
arrowFunction,
'The second argument supplied to `idx` must be an arrow function.'
);
}
if (!t.isExpression(arrowFunction.body)) {
throw file.buildCodeFrameError(
arrowFunction.body,
'The body of the arrow function supplied to `idx` must be a single ' +
'expression (without curly braces).'
);
}
if (arrowFunction.params.length !== 1) {
throw file.buildCodeFrameError(
arrowFunction.params[2] || arrowFunction,
'The arrow function supplied to `idx` must take exactly one parameter.'
);
}
const input = arrowFunction.params[0];
if (!t.isIdentifier(input)) {
throw file.buildCodeFrameError(
arrowFunction.params[0],
'The parameter supplied to `idx` must be an identifier.'
);
}
}
function checkIdxBindingNode(file, node) {
if (t.isImportDeclaration(node)) {
// E.g. `import '...'`
if (node.specifiers.length === 0) {
throw file.buildCodeFrameError(node, 'The idx import must have a value.');
}
// E.g. `import A, {B} from '...'`
// `import A, * as B from '...'`
// `import {A, B} from '...'`
if (node.specifiers.length > 1) {
throw file.buildCodeFrameError(
node.specifiers[1],
'The idx import must be a single specifier.'
);
}
// `importKind` is not a property unless flow syntax is enabled.
// On specifiers, `importKind` is not "value" when it's not a type, it's
// `null`.
// E.g. `import type {...} from '...'`
// `import typeof {...} from '...'`
// `import {type ...} from '...'`.
// `import {typeof ...} from '...'`
if (
node.importKind === 'type' ||
node.importKind === 'typeof' ||
node.specifiers[0].importKind === 'type' ||
node.specifiers[0].importKind === 'typeof'
) {
throw file.buildCodeFrameError(node, 'The idx import must be a value import.');
}
} else if (t.isVariableDeclarator(node)) {
// E.g. var {idx} or var [idx]
if (!t.isIdentifier(node.id)) {
throw file.buildCodeFrameError(
node.specifiers[0],
'The idx declaration must be an identifier.'
);
}
}
}
class UnsupportedNodeTypeError extends Error {
constructor(node, ...params) {
super(`Node type is not supported: ${node.type}`, ...params);
if (Error.captureStackTrace) {
Error.captureStackTrace(this, UnsupportedNodeTypeError);
}
this.name = 'UnsupportedNodeTypeError';
}
}
function getDeepProperties(node, properties = [], computedProperties = new Set()) {
if (t.isMemberExpression(node)) {
if (node.computed) {
computedProperties.add(node.property);
}
return getDeepProperties(node.object, [node.property, ...properties], computedProperties);
} else if (t.isIdentifier(node)) {
return [[node, ...properties], computedProperties];
}
throw new UnsupportedNodeTypeError(node);
}
function buildMemberChain(properties, computedProperties) {
if (properties.length > 1) {
const lead = properties.slice(0, properties.length - 1);
const last = properties[properties.length - 1];
return t.MemberExpression(
buildMemberChain(lead, computedProperties),
last,
computedProperties.has(last)
);
} else if (properties.length === 1) {
return properties[0];
}
return t.identifier('undefined');
}
function buildExpandedMemberNullChecks(
leadingProperties = [],
trailingProperties = [],
computedProperties
) {
const propertyChainNullCheck = t.BinaryExpression(
'!=',
buildMemberChain(leadingProperties, computedProperties),
t.NullLiteral()
);
if (trailingProperties.length <= 1) {
return propertyChainNullCheck;
}
const [headTrailingProperty, ...tailProperties] = trailingProperties;
return t.LogicalExpression(
'&&',
propertyChainNullCheck,
buildExpandedMemberNullChecks(
[...leadingProperties, headTrailingProperty],
tailProperties,
computedProperties
)
);
}
function buildExpandedMemberAccess(node, state) {
let baseNode;
let properties;
let computedProperties;
try {
[[baseNode, ...properties], computedProperties] = getDeepProperties(node);
} catch (error) {
if (error instanceof UnsupportedNodeTypeError) {
throw state.file.buildCodeFrameError(
node,
'idx callbacks may only access properties on the callback parameter.'
);
}
throw error;
}
if (baseNode.name !== state.base.name) {
throw state.file.buildCodeFrameError(
node,
'The parameter of the arrow function supplied to `idx` must match ' +
'the base of the body expression.'
);
}
return t.ConditionalExpression(
buildExpandedMemberNullChecks([state.input], properties, computedProperties),
buildMemberChain([state.input, ...properties], computedProperties),
t.identifier('undefined')
);
}
function visitIdxCallExpression(path, state) {
const node = path.node;
checkIdxArguments(state.file, node);
const replacement = buildExpandedMemberAccess(node.arguments[1].body, {
file: state.file,
input: node.arguments[0],
base: node.arguments[1].params[0],
});
path.replaceWith(replacement);
}
function isIdxImportOrRequire(node, name) {
if (t.isImportDeclaration(node)) {
if (name instanceof RegExp) {
return name.test(node.source.value);
} else {
return t.isStringLiteral(node.source, { value: name });
}
} else if (t.isVariableDeclarator(node)) {
return (
t.isCallExpression(node.init) &&
t.isIdentifier(node.init.callee, { name: 'require' }) &&
(name instanceof RegExp
? name.test(node.init.arguments[0].value)
: t.isLiteral(node.init.arguments[0], { value: name }))
);
} else {
return false;
}
}
const declareVisitor = {
'ImportDeclaration|VariableDeclarator'(path, state) {
if (!isIdxImportOrRequire(path.node, state.importName)) {
return;
}
checkIdxBindingNode(state.file, path.node);
const bindingName = t.isImportDeclaration(path.node)
? path.node.specifiers[0].local.name
: path.node.id.name;
const idxBinding = path.scope.getOwnBinding(bindingName);
idxBinding.constantViolations.forEach(refPath => {
throw state.file.buildCodeFrameError(refPath.node, '`idx` cannot be redefined.');
});
let didTransform = false;
let didSkip = false;
// Traverse the references backwards to process inner calls before
// outer calls.
idxBinding.referencePaths
.slice()
.reverse()
.forEach(refPath => {
if (refPath.node === idxBinding.node) {
// Do nothing...
} else if (refPath.parentPath.isMemberExpression()) {
visitIdxCallExpression(refPath.parentPath.parentPath, state);
didTransform = true;
} else if (refPath.parentPath.isCallExpression()) {
visitIdxCallExpression(refPath.parentPath, state);
didTransform = true;
} else {
// Should this throw?
didSkip = true;
}
});
if (didTransform && !didSkip) {
path.remove();
}
},
};
return {
visitor: {
Program(path, state) {
const importName = state.opts.importName || '@kbn/elastic-idx';
// If there can't reasonably be an idx call, exit fast.
if (importName !== '@kbn/elastic-idx' || idxRe.test(state.file.code)) {
// We're very strict about the shape of idx. Some transforms, like
// "babel-plugin-transform-async-to-generator", will convert arrow
// functions inside async functions into regular functions. So we do
// our transformation before any one else interferes.
const newState = { file: state.file, importName };
path.traverse(declareVisitor, newState);
}
},
},
};
};

View file

@ -0,0 +1,711 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/* @notice
* This product includes code that is based on facebookincubator/idx, which was
* available under a "MIT" license.
*
* MIT License
*
* Copyright (c) 2013-present, Facebook, Inc.
*
* 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.
*/
'use strict'; // eslint-disable-line strict
jest.autoMockOff();
const { transformSync: babelTransform } = require('@babel/core');
const babelPluginIdx = require('./index');
const transformAsyncToGenerator = require('@babel/plugin-transform-async-to-generator');
const vm = require('vm');
function transform(source, plugins, options) {
return babelTransform(source, {
plugins: plugins || [[babelPluginIdx, options]],
babelrc: false,
highlightCode: false,
}).code;
}
const asyncToGeneratorHelperCode = `
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
try {
var info = gen[key](arg);
var value = info.value;
} catch (error) {
reject(error);
return;
}
if (info.done) {
resolve(value);
} else {
Promise.resolve(value).then(_next, _throw);
}
}
function _asyncToGenerator(fn) {
return function() {
var self = this,
args = arguments;
return new Promise(function(resolve, reject) {
var gen = fn.apply(self, args);
function _next(value) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
}
function _throw(err) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
}
_next(undefined);
});
};
}
`;
function stringByTrimmingSpaces(string) {
return string.replace(/\s+/g, '');
}
expect.extend({
toTransformInto: (input, expected) => {
const plugins = typeof input === 'string' ? null : input.plugins;
const options = typeof input === 'string' ? undefined : input.options;
const code = typeof input === 'string' ? input : input.code;
const actual = transform(code, plugins, options);
const pass = stringByTrimmingSpaces(actual) === stringByTrimmingSpaces(expected);
return {
pass,
message: () =>
'Expected input to transform into:\n' + expected + '\n' + 'Instead, got:\n' + actual,
};
},
toThrowTransformError: (input, expected) => {
try {
const plugins = typeof input === 'string' ? null : input.plugins;
const options = typeof input === 'string' ? undefined : input.options;
const code = typeof input === 'string' ? input : input.code;
transform(code, plugins, options);
} catch (error) {
const actual = /^.+:\s*(.*)/.exec(error.message)[1]; // Strip "undefined: " and code snippet
return {
pass: actual === expected,
message: () =>
'Expected transform to throw "' + expected + '", but instead ' + 'got "' + actual + '".',
};
}
return {
pass: false,
message: () => 'Expected transform to throw "' + expected + '".',
};
},
toReturn: (input, expected) => {
const code = transform(input, undefined);
const actual = vm.runInNewContext(code);
return {
pass: actual === expected,
message: () => 'Expected "' + expected + '" but got "' + actual + '".',
};
},
});
describe('kbn-babel-plugin-apm-idx', () => {
it('transforms member expressions', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
idx(base, _ => _.b.c.d.e);
`).toTransformInto(`
base != null && base.b != null && base.b.c != null && base.b.c.d != null
? base.b.c.d.e
: undefined;
`);
});
it('throws on call expressions', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
idx(base, _ => _.b.c(...foo)().d(bar, null, [...baz]));
`).toThrowTransformError('idx callbacks may only access properties on the callback parameter.');
});
it('transforms bracket notation', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
idx(base, _ => _["b"][0][c + d]);
`).toTransformInto(`
base != null && base["b"] != null && base["b"][0] != null ? base["b"][0][c + d] : undefined;
`);
});
it('throws on bracket notation call expressions', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
idx(base, _ => _["b"](...foo)()[0][c + d](bar, null, [...baz]));
`).toThrowTransformError('idx callbacks may only access properties on the callback parameter.');
});
it('transforms combination of both member access notations', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
idx(base, _ => _.a["b"].c[d[e[f]]].g);
`).toTransformInto(`
base != null && base.a != null && base.a["b"] != null && base.a["b"].c != null && base.a["b"].c[d[e[f]]] != null
? base.a["b"].c[d[e[f]]].g
: undefined;
`);
});
it('transforms if the base is an expression', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
idx(this.props.base[5], _ => _.property);
`).toTransformInto(`
this.props.base[5] != null ? this.props.base[5].property : undefined;
`);
});
it('throws if the arrow function has more than one param', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
idx(base, (a, b) => _.property);
`).toThrowTransformError(
'The arrow function supplied to `idx` must take exactly one parameter.'
);
});
it('throws if the arrow function has an invalid base', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
idx(base, a => b.property)
`).toThrowTransformError(
'The parameter of the arrow function supplied to `idx` must match the ' +
'base of the body expression.'
);
});
it('throws if the arrow function expression has non-properties/methods', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
idx(base, _ => (_.a++).b.c);
`).toThrowTransformError('idx callbacks may only access properties on the callback parameter.');
});
it('throws if the body of the arrow function is not an expression', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
idx(base, _ => {})
`).toThrowTransformError(
'The body of the arrow function supplied to `idx` must be a single ' +
'expression (without curly braces).'
);
});
it('ignores non-function call idx', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
result = idx;
`).toTransformInto(`
import { idx } from '@kbn/elastic-idx';
result = idx;
`);
});
it('throws if idx is called with zero arguments', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
idx();
`).toThrowTransformError('The `idx` function takes exactly two arguments.');
});
it('throws if idx is called with one argument', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
idx(1);
`).toThrowTransformError('The `idx` function takes exactly two arguments.');
});
it('throws if idx is called with three arguments', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
idx(1, 2, 3);
`).toThrowTransformError('The `idx` function takes exactly two arguments.');
});
it('transforms idx calls as part of another expressions', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
paddingStatement();
a = idx(base, _ => _.b[c]);
`).toTransformInto(`
paddingStatement();
a = base != null && base.b != null ? base.b[c] : undefined;
`);
});
it('transforms nested idx calls', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
idx(
idx(
idx(base, _ => _.a.b),
_ => _.c.d
),
_ => _.e.f
);
`).toTransformInto(`
(
(base != null && base.a != null ? base.a.b : undefined) != null &&
(base != null && base.a != null ? base.a.b : undefined).c != null ?
(base != null && base.a != null ? base.a.b : undefined).c.d :
undefined
) != null
&&
(
(base != null && base.a != null ? base.a.b : undefined) != null &&
(base != null && base.a != null ? base.a.b : undefined).c != null ?
(base != null && base.a != null ? base.a.b : undefined).c.d :
undefined
).e != null ?
(
(base != null && base.a != null ? base.a.b : undefined) != null &&
(base != null && base.a != null ? base.a.b : undefined).c != null ?
(base != null && base.a != null ? base.a.b : undefined).c.d :
undefined
).e.f :
undefined;
`);
});
it('transforms idx calls inside async functions (plugin order #1)', () => {
expect({
plugins: [babelPluginIdx, transformAsyncToGenerator],
code: `
import { idx } from '@kbn/elastic-idx';
async function f() {
idx(base, _ => _.b.c.d.e);
}
`,
}).toTransformInto(`
${asyncToGeneratorHelperCode}
function f() {
return _f.apply(this, arguments);
}
function _f() {
_f = _asyncToGenerator(function* () {
base != null && base.b != null && base.b.c != null && base.b.c.d != null ? base.b.c.d.e : undefined;
});
return _f.apply(this, arguments);
}
`);
});
it('transforms idx calls inside async functions (plugin order #2)', () => {
expect({
plugins: [transformAsyncToGenerator, babelPluginIdx],
code: `
import { idx } from '@kbn/elastic-idx';
async function f() {
idx(base, _ => _.b.c.d.e);
}
`,
}).toTransformInto(`
${asyncToGeneratorHelperCode}
function f() {
return _f.apply(this, arguments);
}
function _f() {
_f = _asyncToGenerator(function* () {
base != null && base.b != null && base.b.c != null && base.b.c.d != null ? base.b.c.d.e : undefined;
});
return _f.apply(this, arguments);
}
`);
});
it('transforms idx calls in async methods', () => {
expect({
plugins: [transformAsyncToGenerator, babelPluginIdx],
code: `
import { idx } from '@kbn/elastic-idx';
class Foo {
async bar() {
idx(base, _ => _.b);
return this;
}
}
`,
}).toTransformInto(`
${asyncToGeneratorHelperCode}
class Foo {
bar() {
var _this = this;
return _asyncToGenerator(function* () {
base != null ? base.b : undefined;
return _this;
})();
}
}
`);
});
it('transforms idx calls when an idx import binding is in scope', () => {
expect(`
import idx from '@kbn/elastic-idx';
idx(base, _ => _.b);
`).toTransformInto(`
base != null ? base.b : undefined;
`);
});
it('transforms idx calls when an idx const binding is in scope', () => {
expect(`
const idx = require('@kbn/elastic-idx');
idx(base, _ => _.b);
`).toTransformInto(`
base != null ? base.b : undefined;
`);
});
it('transforms deep idx calls when an idx import binding is in scope', () => {
expect(`
import idx from '@kbn/elastic-idx';
function f() {
idx(base, _ => _.b);
}
`).toTransformInto(`
function f() {
base != null ? base.b : undefined;
}
`);
});
it('transforms deep idx calls when an idx const binding is in scope', () => {
expect(`
const idx = require('@kbn/elastic-idx');
function f() {
idx(base, _ => _.b);
}
`).toTransformInto(`
function f() {
base != null ? base.b : undefined;
}
`);
});
it('transforms idx calls when an idx is called as a member function on the binding in scope', () => {
expect(`
const elastic_idx = require("@kbn/elastic-idx");
const result = elastic_idx.idx(base, _ => _.a.b.c.d);
`).toTransformInto(`
const result = base != null &&
base.a != null &&
base.a.b != null &&
base.a.b.c != null ?
base.a.b.c.d :
undefined;
`);
});
it('throws on base call expressions', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
idx(base, _ => _().b.c);
`).toThrowTransformError('idx callbacks may only access properties on the callback parameter.');
});
it('transforms when the idx parent is a scope creating expression', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
(() => idx(base, _ => _.b));
`).toTransformInto(`
() => base != null ? base.b : undefined;
`);
});
it('throws if redefined before use', () => {
expect(`
let idx = require('@kbn/elastic-idx');
idx = null;
idx(base, _ => _.b);
`).toThrowTransformError('`idx` cannot be redefined.');
});
it('throws if redefined after use', () => {
expect(`
let idx = require('@kbn/elastic-idx');
idx(base, _ => _.b);
idx = null;
`).toThrowTransformError('`idx` cannot be redefined.');
});
it('throws if there is a duplicate declaration', () => {
expect(() =>
transform(`
let idx = require('@kbn/elastic-idx');
idx(base, _ => _.b);
function idx() {}
`)
).toThrow();
});
it('handles sibling scopes with unique idx', () => {
expect(`
function aaa() {
const idx = require('@kbn/elastic-idx');
idx(base, _ => _.b);
}
function bbb() {
const idx = require('@kbn/elastic-idx');
idx(base, _ => _.b);
}
`).toTransformInto(`
function aaa() {
base != null ? base.b : undefined;
}
function bbb() {
base != null ? base.b : undefined;
}
`);
});
it('handles sibling scopes with and without idx', () => {
expect(`
function aaa() {
const idx = require('@kbn/elastic-idx');
idx(base, _ => _.b);
}
function bbb() {
idx(base, _ => _.b);
}
`).toTransformInto(`
function aaa() {
base != null ? base.b : undefined;
}
function bbb() {
idx(base, _ => _.b);
}
`);
});
it('handles nested scopes with shadowing', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
idx(base, _ => _.b);
function aaa() {
idx(base, _ => _.b);
function bbb(idx) {
idx(base, _ => _.b);
}
}
`).toTransformInto(`
base != null ? base.b : undefined;
function aaa() {
base != null ? base.b : undefined;
function bbb(idx) {
idx(base, _ => _.b);
}
}
`);
});
it('handles named idx import', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
idx(base, _ => _.b);
`).toTransformInto(`
base != null ? base.b : undefined;
`);
});
it('throws on default plus named import', () => {
expect(`
import idx, {foo} from '@kbn/elastic-idx';
idx(base, _ => _.b);
`).toThrowTransformError('The idx import must be a single specifier.');
});
it('throws on default plus namespace import', () => {
expect(`
import idx, * as foo from '@kbn/elastic-idx';
idx(base, _ => _.b);
`).toThrowTransformError('The idx import must be a single specifier.');
});
it('throws on named default plus other import', () => {
expect(`
import {default as idx, foo} from '@kbn/elastic-idx';
idx(base, _ => _.b);
`).toThrowTransformError('The idx import must be a single specifier.');
});
it('unused idx import should be left alone', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
`).toTransformInto(`
import { idx } from '@kbn/elastic-idx';
`);
});
it('allows configuration of the import name', () => {
expect({
code: `
import { idx } from 'i_d_x';
idx(base, _ => _.b);
`,
options: { importName: 'i_d_x' },
}).toTransformInto(`
base != null ? base.b : undefined;
`);
});
it('follows configuration of the import name', () => {
expect({
code: `
import { idx } from '@kbn/elastic-idx';
import { idx as i_d_x } from 'i_d_x';
i_d_x(base, _ => _.b);
idx(base, _ => _.c);
`,
options: { importName: 'i_d_x' },
}).toTransformInto(`
import { idx } from '@kbn/elastic-idx';
base != null ? base.b : undefined;
idx(base, _ => _.c);
`);
});
it('allows configuration of the require name as a string', () => {
expect({
code: `
import { idx } from 'i_d_x';
idx(base, _ => _.b);
`,
options: { importName: 'i_d_x' },
}).toTransformInto(`
base != null ? base.b : undefined;
`);
});
it('allows configuration of the require name as a RegExp', () => {
expect({
code: `
import { idx } from '../../common/idx';
idx(base, _ => _.b);
`,
options: { importName: /.*idx$/ },
}).toTransformInto(`
base != null ? base.b : undefined;
`);
});
it('follows configuration of the require name', () => {
expect({
code: `
const idx = require('@kbn/elastic-idx');
const i_d_x = require('i_d_x');
i_d_x(base, _ => _.b);
idx(base, _ => _.c);
`,
options: { importName: 'i_d_x' },
}).toTransformInto(`
const idx = require('@kbn/elastic-idx');
base != null ? base.b : undefined;
idx(base, _ => _.c);
`);
});
describe('functional', () => {
it('works with only properties', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
const base = {a: {b: {c: 2}}};
idx(base, _ => _.a.b.c);
`).toReturn(2);
});
it('works with missing properties', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
const base = {a: {b: {}}};
idx(base, _ => _.a.b.c);
`).toReturn(undefined);
});
it('works with null properties', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
const base = {a: {b: null}};
idx(base, _ => _.a.b.c);
`).toReturn(undefined);
});
it('works with nested idx calls', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
const base = {a: {b: {c: {d: {e: {f: 2}}}}}};
idx(
idx(
idx(base, _ => _.a.b),
_ => _.c.d
),
_ => _.e.f
);
`).toReturn(2);
});
it('works with nested idx calls with missing properties', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
const base = {a: {b: {c: null}}};
idx(
idx(
idx(base, _ => _.a.b),
_ => _.c.d
),
_ => _.e.f
);
`).toReturn(undefined);
});
});
});

View file

@ -0,0 +1,28 @@
{
"name": "@kbn/elastic-idx",
"version": "1.0.0",
"private": true,
"license": "Apache-2.0",
"description": "Library for optional chaining & the Babel plugin to transpile idx calls to plain, optimized JS",
"main": "target/index.js",
"types": "target/index.d.js",
"repository": {
"type": "git",
"url": "https://github.com/elastic/kibana/tree/master/packages/kbn-elastic-idx"
},
"scripts": {
"build": "tsc",
"kbn:bootstrap": "yarn build",
"kbn:watch": "yarn build --watch",
"test": "jest"
},
"devDependencies": {
"@babel/core": "^7.3.4",
"@babel/plugin-transform-async-to-generator": "^7.4.0",
"jest": "^24.1.0",
"typescript": "^3.3.3333"
},
"jest": {
"testEnvironment": "node"
}
}

View file

@ -1,7 +1,20 @@
/*
* 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.
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
@ -20,9 +33,9 @@ type DeepRequiredObject<T> = { [P in keyof T]-?: DeepRequired<T[P]> };
/**
* Function that has deeply required return type
*/
type FunctionWithRequiredReturnType<
T extends (...args: any[]) => any
> = T extends (...args: infer A) => infer R
type FunctionWithRequiredReturnType<T extends (...args: any[]) => any> = T extends (
...args: infer A
) => infer R
? (...args: A) => DeepRequired<R>
: never;

View file

@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": true,
"outDir": "./target"
},
"include": ["src/**/*"]
}

View file

@ -5,6 +5,7 @@
{
"name": "bytes",
"type": "number",
"esTypes": ["long"],
"count": 10,
"scripted": false,
"searchable": true,
@ -14,6 +15,7 @@
{
"name": "ssl",
"type": "boolean",
"esTypes": ["boolean"],
"count": 20,
"scripted": false,
"searchable": true,
@ -23,6 +25,7 @@
{
"name": "@timestamp",
"type": "date",
"esTypes": ["date"],
"count": 30,
"scripted": false,
"searchable": true,
@ -32,6 +35,7 @@
{
"name": "time",
"type": "date",
"esTypes": ["date"],
"count": 30,
"scripted": false,
"searchable": true,
@ -41,6 +45,7 @@
{
"name": "@tags",
"type": "string",
"esTypes": ["keyword"],
"count": 0,
"scripted": false,
"searchable": true,
@ -50,6 +55,7 @@
{
"name": "utc_time",
"type": "date",
"esTypes": ["date"],
"count": 0,
"scripted": false,
"searchable": true,
@ -59,6 +65,7 @@
{
"name": "phpmemory",
"type": "number",
"esTypes": ["integer"],
"count": 0,
"scripted": false,
"searchable": true,
@ -68,6 +75,7 @@
{
"name": "ip",
"type": "ip",
"esTypes": ["ip"],
"count": 0,
"scripted": false,
"searchable": true,
@ -77,6 +85,7 @@
{
"name": "request_body",
"type": "attachment",
"esTypes": ["attachment"],
"count": 0,
"scripted": false,
"searchable": true,
@ -86,6 +95,7 @@
{
"name": "point",
"type": "geo_point",
"esTypes": ["geo_point"],
"count": 0,
"scripted": false,
"searchable": true,
@ -95,6 +105,7 @@
{
"name": "area",
"type": "geo_shape",
"esTypes": ["geo_shape"],
"count": 0,
"scripted": false,
"searchable": true,
@ -104,6 +115,7 @@
{
"name": "hashed",
"type": "murmur3",
"esTypes": ["murmur3"],
"count": 0,
"scripted": false,
"searchable": true,
@ -113,6 +125,7 @@
{
"name": "geo.coordinates",
"type": "geo_point",
"esTypes": ["geo_point"],
"count": 0,
"scripted": false,
"searchable": true,
@ -122,6 +135,7 @@
{
"name": "extension",
"type": "string",
"esTypes": ["keyword"],
"count": 0,
"scripted": false,
"searchable": true,
@ -131,6 +145,7 @@
{
"name": "machine.os",
"type": "string",
"esTypes": ["text"],
"count": 0,
"scripted": false,
"searchable": true,
@ -140,6 +155,7 @@
{
"name": "machine.os.raw",
"type": "string",
"esTypes": ["keyword"],
"count": 0,
"scripted": false,
"searchable": true,
@ -151,6 +167,7 @@
{
"name": "geo.src",
"type": "string",
"esTypes": ["keyword"],
"count": 0,
"scripted": false,
"searchable": true,
@ -160,6 +177,7 @@
{
"name": "_id",
"type": "string",
"esTypes": ["_id"],
"count": 0,
"scripted": false,
"searchable": true,
@ -169,6 +187,7 @@
{
"name": "_type",
"type": "string",
"esTypes": ["_type"],
"count": 0,
"scripted": false,
"searchable": true,
@ -178,6 +197,7 @@
{
"name": "_source",
"type": "_source",
"esTypes": ["_source"],
"count": 0,
"scripted": false,
"searchable": true,
@ -187,6 +207,7 @@
{
"name": "non-filterable",
"type": "string",
"esTypes": ["text"],
"count": 0,
"scripted": false,
"searchable": false,
@ -196,6 +217,7 @@
{
"name": "non-sortable",
"type": "string",
"esTypes": ["text"],
"count": 0,
"scripted": false,
"searchable": false,
@ -205,6 +227,7 @@
{
"name": "custom_user_field",
"type": "conflict",
"esTypes": ["long", "text"],
"count": 0,
"scripted": false,
"searchable": true,

View file

@ -73,7 +73,7 @@ exports.Artifact = class Artifact {
* @param {string} version
* @param {ToolingLog} log
*/
static async get(license, version, log) {
static async getSnapshot(license, version, log) {
const urlVersion = `${encodeURIComponent(version)}-SNAPSHOT`;
const urlBuild = encodeURIComponent(TEST_ES_SNAPSHOT_VERSION);
const url = `${V1_VERSIONS_API}/${urlVersion}/builds/${urlBuild}/projects/elasticsearch`;
@ -138,6 +138,24 @@ exports.Artifact = class Artifact {
return new Artifact(artifactSpec, log);
}
/**
* Fetch an Artifact from the Elasticsearch past releases url
* @param {string} url
* @param {ToolingLog} log
*/
static async getArchive(url, log) {
const shaUrl = `${url}.sha512`;
const artifactSpec = {
url: url,
filename: path.basename(url),
checksumUrl: shaUrl,
checksumType: getChecksumType(shaUrl),
};
return new Artifact(artifactSpec, log);
}
constructor(spec, log) {
this._spec = spec;
this._log = log;

View file

@ -22,8 +22,10 @@ const path = require('path');
const chalk = require('chalk');
const execa = require('execa');
const del = require('del');
const url = require('url');
const { log: defaultLog, decompress } = require('../utils');
const { BASE_PATH, ES_CONFIG, ES_KEYSTORE_BIN } = require('../paths');
const { Artifact } = require('../artifact');
/**
* Extracts an ES archive and optionally installs plugins
@ -44,13 +46,20 @@ exports.installArchive = async function installArchive(archive, options = {}) {
log = defaultLog,
} = options;
let dest = archive;
if (['http:', 'https:'].includes(url.parse(archive).protocol)) {
const artifact = await Artifact.getArchive(archive, log);
dest = path.resolve(basePath, 'cache', artifact.getFilename());
await artifact.download(dest);
}
if (fs.existsSync(installPath)) {
log.info('install directory already exists, removing');
await del(installPath, { force: true });
}
log.info('extracting %s', chalk.bold(archive));
await decompress(archive, installPath);
log.info('extracting %s', chalk.bold(dest));
await decompress(dest, installPath);
log.info('extracted to %s', chalk.bold(installPath));
if (license === 'trial') {

View file

@ -45,7 +45,7 @@ exports.downloadSnapshot = async function installSnapshot({
log.info('install path: %s', chalk.bold(installPath));
log.info('license: %s', chalk.bold(license));
const artifact = await Artifact.get(license, version, log);
const artifact = await Artifact.getSnapshot(license, version, log);
const dest = path.resolve(basePath, 'cache', artifact.getFilename());
await artifact.download(dest);

View file

@ -0,0 +1,28 @@
A mini utility to convert [Elasticsearch's REST spec](https://github.com/elastic/elasticsearch/blob/master/rest-api-spec) to Console's (Kibana) autocomplete format.
It is used to semi-manually update Console's autocompletion rules.
### Retrieving the spec
```
cd es-spec
git init
git remote add origin https://github.com/elastic/elasticsearch
git config core.sparsecheckout true
echo "rest-api-spec/src/main/resources/rest-api-spec/api/*" > .git/info/sparse-checkout
git pull --depth=1 origin master
```
### Usage
```
yarn spec_to_console \
-g "es-spec/rest-api-spec/src/main/resources/rest-api-spec/api/*.json" \
-d "../kibana/src/core_plugins/console/api_server/spec/generated"
```
### Information used in Console that is not available in the REST spec
* Request bodies
* Data fetched at runtime: indices, fields, snapshots, etc
* Ad hoc additions

View file

@ -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.
*/
const fs = require('fs');
const path = require('path');
const program = require('commander');
const glob = require('glob');
const packageJSON = require('../package.json');
const convert = require('../lib/convert');
program
.version(packageJSON.version)
.option('-g --glob []', 'Files to convert')
.option('-d --directory []', 'Output directory')
.parse(process.argv);
if (!program.glob) {
console.error('Expected input');
process.exit(1);
}
const files = glob.sync(program.glob);
console.log(files.length, files);
files.forEach(file => {
const spec = JSON.parse(fs.readFileSync(file));
const output = JSON.stringify(convert(spec), null, 2);
if (program.directory) {
const outputName = path.basename(file);
const outputPath = path.resolve(program.directory, outputName);
fs.writeFileSync(outputPath, output + '\n');
} else {
console.log(output);
}
});

View file

@ -17,5 +17,6 @@
* under the License.
*/
export { Config } from './config/config';
export { Lifecycle } from './lifecycle';
const convert = require('./lib/convert');
module.exports = convert;

View file

@ -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.
*/
const convertParams = require('./convert/params');
const convertMethods = require('./convert/methods');
const convertPaths = require('./convert/paths');
const convertParts = require('./convert/parts');
module.exports = spec => {
const result = {};
Object.keys(spec).forEach(api => {
const source = spec[api];
if (!source.url) {
return result;
}
const convertedSpec = result[api] = {};
if (source.url && source.url.params) {
const urlParams = convertParams(source.url.params);
if (Object.keys(urlParams).length > 0) {
convertedSpec.url_params = urlParams;
}
}
if (source.methods) {
convertedSpec.methods = convertMethods(source.methods);
}
if (source.url.paths) {
convertedSpec.patterns = convertPaths(source.url.paths);
}
if (source.url.parts) {
const components = convertParts(source.url.parts);
const hasComponents = Object.keys(components).filter(c => {
return Boolean(components[c]);
}).length > 0;
if (hasComponents) {
convertedSpec.url_components = convertParts(source.url.parts);
}
}
if (source.documentation) {
convertedSpec.documentation = source.documentation;
}
});
return result;
};

View file

@ -17,13 +17,12 @@
* under the License.
*/
import indexPattern from './index_pattern.json';
function getIndexPatternWithTitle(indexPatternTitle) {
indexPattern.attributes.title = indexPatternTitle;
return indexPattern;
}
const convert = require('./convert');
export function getSavedObjects(indexPatternTitle) {
return [getIndexPatternWithTitle(indexPatternTitle)];
}
const clusterHealthSpec = require('../test/fixtures/cluster_health_spec');
const clusterHealthAutocomplete = require('../test/fixtures/cluster_health_autocomplete');
test('convert', () => {
expect(convert(clusterHealthSpec)).toEqual(clusterHealthAutocomplete);
});

View file

@ -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.
*/
module.exports = methods => {
return methods;
};

View file

@ -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.
*/
module.exports = params => {
const result = {};
Object.keys(params).forEach(param => {
const { type, description = '', options = [] } = params[param];
const [, defaultValue] = description.match(/\(default: (.*)\)/) || [];
switch (type) {
case undefined:
// { description: 'TODO: ?' }
break;
case 'int':
result[param] = 0;
break;
case 'double':
result[param] = 0.0;
break;
case 'enum':
result[param] = options;
break;
case 'boolean':
result[param] = '__flag__';
break;
case 'time':
case 'date':
case 'string':
case 'number':
result[param] = defaultValue || '';
break;
case 'list':
result[param] = [];
break;
default:
throw new Error(`Unexpected type ${type}`);
}
});
return result;
};

View file

@ -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.
*/
const replacePattern = require('../replace_pattern');
module.exports = parts => {
const result = {};
Object.keys(parts).forEach(part => {
const key = replacePattern(part, { exact: true });
const options = parts[part].options;
if (options && options.length) {
result[key] = options.sort();
} else {
result[key] = null;
}
});
return result;
};

View file

@ -17,12 +17,11 @@
* under the License.
*/
export function readProviderSpec(type, providers) {
return Object.keys(providers).map(name => {
return {
type,
name,
fn: providers[name],
};
const replacePattern = require('../replace_pattern');
module.exports = patterns => {
return patterns.map(pattern => {
return replacePattern(pattern, { brackets: true });
});
}
};

View file

@ -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.
*/
const map = require('./static/map_interpolation');
module.exports = (pattern, { brackets, exact } = {}) => {
let newPattern = pattern;
Object.keys(map).forEach(key => {
const replaceFrom = brackets ? `{${key}}` : key;
const replaceTo = brackets ? `{${map[key]}}` : map[key];
if (exact) {
const exactMatch = replaceFrom === newPattern;
newPattern = exactMatch ? replaceTo : newPattern;
} else {
newPattern = newPattern.replace(replaceFrom, replaceTo);
}
});
return newPattern.replace(/^\//, '');
};

View file

@ -0,0 +1,5 @@
{
"index": "indices",
"node_id": "nodes",
"metric": "metrics"
}

View file

@ -0,0 +1,27 @@
{
"name": "spec-to-console",
"version": "0.0.0",
"description": "ES REST spec -> Console autocomplete",
"main": "index.js",
"directories": {
"lib": "lib"
},
"scripts": {
"test": "jest",
"format": "prettier **/*.js --write"
},
"author": "",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/jbudz/spec-to-console/issues"
},
"homepage": "https://github.com/jbudz/spec-to-console#readme",
"devDependencies": {
"jest": "^24.1.0",
"prettier": "^1.14.3"
},
"dependencies": {
"commander": "^2.11.0",
"glob": "^7.1.2"
}
}

View file

@ -0,0 +1,39 @@
{
"cluster.health": {
"url_params": {
"level": [
"cluster",
"indices",
"shards"
],
"local": "__flag__",
"master_timeout": "",
"timeout": "",
"wait_for_active_shards": "",
"wait_for_nodes": "",
"wait_for_events": [
"immediate",
"urgent",
"high",
"normal",
"low",
"languid"
],
"wait_for_no_relocating_shards": "__flag__",
"wait_for_no_initializing_shards": "__flag__",
"wait_for_status": [
"green",
"yellow",
"red"
]
},
"methods": [
"GET"
],
"patterns": [
"_cluster/health",
"_cluster/health/{indices}"
],
"documentation": "http://www.elastic.co/guide/en/elasticsearch/reference/master/cluster-health.html"
}
}

View file

@ -0,0 +1,64 @@
{
"cluster.health": {
"documentation": "http://www.elastic.co/guide/en/elasticsearch/reference/master/cluster-health.html",
"methods": ["GET"],
"url": {
"path": "/_cluster/health",
"paths": ["/_cluster/health", "/_cluster/health/{index}"],
"parts": {
"index": {
"type" : "list",
"description" : "Limit the information returned to a specific index"
}
},
"params": {
"level": {
"type" : "enum",
"options" : ["cluster","indices","shards"],
"default" : "cluster",
"description" : "Specify the level of detail for returned information"
},
"local": {
"type" : "boolean",
"description" : "Return local information, do not retrieve the state from master node (default: false)"
},
"master_timeout": {
"type" : "time",
"description" : "Explicit operation timeout for connection to master node"
},
"timeout": {
"type" : "time",
"description" : "Explicit operation timeout"
},
"wait_for_active_shards": {
"type" : "string",
"description" : "Wait until the specified number of shards is active"
},
"wait_for_nodes": {
"type" : "string",
"description" : "Wait until the specified number of nodes is available"
},
"wait_for_events": {
"type" : "enum",
"options" : ["immediate", "urgent", "high", "normal", "low", "languid"],
"description" : "Wait until all currently queued events with the given priority are processed"
},
"wait_for_no_relocating_shards": {
"type" : "boolean",
"description" : "Whether to wait until there are no relocating shards in the cluster"
},
"wait_for_no_initializing_shards": {
"type" : "boolean",
"description" : "Whether to wait until there are no initializing shards in the cluster"
},
"wait_for_status": {
"type" : "enum",
"options" : ["green","yellow","red"],
"default" : null,
"description" : "Wait until cluster is in a specific state"
}
}
},
"body": null
}
}

File diff suppressed because it is too large Load diff

View file

@ -17,21 +17,17 @@
* under the License.
*/
import * as FunctionalTestRunner from '../../../../../src/functional_test_runner';
import { FunctionalTestRunner } from '../../../../../src/functional_test_runner';
import { CliError } from './run_cli';
function createFtr({ configPath, options: { log, bail, grep, updateBaselines, suiteTags } }) {
return FunctionalTestRunner.createFunctionalTestRunner({
log,
configFile: configPath,
configOverrides: {
mochaOpts: {
bail: !!bail,
grep,
},
updateBaselines,
suiteTags,
return new FunctionalTestRunner(log, configPath, {
mochaOpts: {
bail: !!bail,
grep,
},
updateBaselines,
suiteTags,
});
}

View file

@ -65,7 +65,7 @@
"redux": "3.7.2",
"redux-thunk": "2.2.0",
"sass-loader": "^7.1.0",
"sinon": "^5.0.7",
"sinon": "^7.2.2",
"style-loader": "^0.23.1",
"webpack": "^4.23.1",
"webpack-dev-server": "^3.1.10",

View file

@ -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('../packages/kbn-spec-to-console/bin/spec_to_console');

View file

@ -181,6 +181,7 @@ export default class ClusterManager {
/[\\\/](\..*|node_modules|bower_components|public|__[a-z0-9_]+__|coverage)[\\\/]/,
/\.test\.js$/,
...extraIgnores,
'plugins/java_languageserver'
],
});

View file

@ -134,8 +134,7 @@ export default function (program) {
.option('-e, --elasticsearch <uri1,uri2>', 'Elasticsearch instances')
.option(
'-c, --config <path>',
'Path to the config file, can be changed with the CONFIG_PATH environment variable as well. ' +
'Use multiple --config args to include multiple config files.',
'Path to the config file, use multiple --config args to include multiple config files',
configPathCollector,
[ getConfig() ]
)

View file

@ -132,7 +132,7 @@ export function extractArchive(archive, targetDir, extractPath) {
return reject(err);
}
readStream.pipe(createWriteStream(fileName));
readStream.pipe(createWriteStream(fileName, { mode: entry.externalFileAttributes >>> 16 }));
readStream.on('end', function () {
zipfile.readEntry();
});

View file

@ -21,6 +21,7 @@ import rimraf from 'rimraf';
import path from 'path';
import os from 'os';
import glob from 'glob';
import fs from 'fs';
import { analyzeArchive, extractArchive, _isDirectory } from './zip';
describe('kibana cli', function () {
@ -72,6 +73,28 @@ describe('kibana cli', function () {
});
});
describe('checkFilePermission', () => {
it('verify consistency of modes of files', async () => {
const archivePath = path.resolve(repliesPath, 'test_plugin.zip');
await extractArchive(archivePath, tempPath, 'kibana/libs');
const files = await glob.sync('**/*', { cwd: tempPath });
const expected = [
'executable',
'unexecutable'
];
expect(files.sort()).toEqual(expected.sort());
const executableMode = '0' + (fs.statSync(path.resolve(tempPath, 'executable')).mode & parseInt('777', 8)).toString(8);
const unExecutableMode = '0' + (fs.statSync(path.resolve(tempPath, 'unexecutable')).mode & parseInt('777', 8)).toString(8);
expect(executableMode).toEqual('0755');
expect(unExecutableMode).toEqual('0644');
});
});
it('handles a corrupt zip archive', async () => {
try {
await extractArchive(path.resolve(repliesPath, 'corrupt.zip'));

View file

@ -16,30 +16,30 @@
* specific language governing permissions and limitations
* under the License.
*/
import { Capabilities, CapabilitiesService, CapabilitiesSetup } from './capabilities_service';
import { Capabilities, CapabilitiesService, CapabilitiesStart } from './capabilities_service';
const createSetupContractMock = () => {
const setupContract: jest.Mocked<CapabilitiesSetup> = {
const createStartContractMock = () => {
const startContract: jest.Mocked<CapabilitiesStart> = {
getCapabilities: jest.fn(),
};
setupContract.getCapabilities.mockReturnValue({
startContract.getCapabilities.mockReturnValue({
catalogue: {},
management: {},
navLinks: {},
} as Capabilities);
return setupContract;
return startContract;
};
type CapabilitiesServiceContract = PublicMethodsOf<CapabilitiesService>;
const createMock = () => {
const mocked: jest.Mocked<CapabilitiesServiceContract> = {
setup: jest.fn(),
start: jest.fn(),
};
mocked.setup.mockReturnValue(createSetupContractMock());
mocked.start.mockReturnValue(createStartContractMock());
return mocked;
};
export const capabilitiesServiceMock = {
create: createMock,
createSetupContract: createSetupContractMock,
createStartContract: createStartContractMock,
};

View file

@ -32,7 +32,7 @@ describe('#start', () => {
} as any,
});
const service = new CapabilitiesService();
const startContract = service.setup({ injectedMetadata: injectedMetadata.setup() });
const startContract = service.start({ injectedMetadata: injectedMetadata.start() });
expect(startContract.getCapabilities()).toEqual({
foo: 'bar',
bar: 'baz',
@ -51,7 +51,7 @@ describe('#start', () => {
} as any,
});
const service = new CapabilitiesService();
const startContract = service.setup({ injectedMetadata: injectedMetadata.setup() });
const startContract = service.start({ injectedMetadata: injectedMetadata.start() });
const capabilities = startContract.getCapabilities();
// @ts-ignore TypeScript knows this shouldn't be possible

View file

@ -16,11 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
import { InjectedMetadataSetup } from '../injected_metadata';
import { InjectedMetadataStart } from '../injected_metadata';
import { deepFreeze } from '../utils/deep_freeze';
interface StartDeps {
injectedMetadata: InjectedMetadataSetup;
injectedMetadata: InjectedMetadataStart;
}
/**
@ -50,7 +50,7 @@ export interface Capabilities {
* Capabilities Setup.
* @public
*/
export interface CapabilitiesSetup {
export interface CapabilitiesStart {
/**
* Gets the read-only capabilities.
*/
@ -63,10 +63,10 @@ export interface CapabilitiesSetup {
* Service that is responsible for UI Capabilities.
*/
export class CapabilitiesService {
public setup({ injectedMetadata }: StartDeps): CapabilitiesSetup {
public start({ injectedMetadata }: StartDeps): CapabilitiesStart {
return {
getCapabilities: () =>
deepFreeze<Capabilities>(injectedMetadata.getInjectedVar('uiCapabilities') as Capabilities),
deepFreeze(injectedMetadata.getInjectedVar('uiCapabilities') as Capabilities),
};
}
}

View file

@ -17,4 +17,4 @@
* under the License.
*/
export { Capabilities, CapabilitiesService, CapabilitiesSetup } from './capabilities_service';
export { Capabilities, CapabilitiesService, CapabilitiesStart } from './capabilities_service';

View file

@ -18,6 +18,7 @@
*/
import { basePathServiceMock } from './base_path/base_path_service.mock';
import { capabilitiesServiceMock } from './capabilities/capabilities_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';
@ -104,3 +105,11 @@ export const PluginsServiceConstructor = jest.fn().mockImplementation(() => Mock
jest.doMock('./plugins', () => ({
PluginsService: PluginsServiceConstructor,
}));
export const MockCapabilitiesService = capabilitiesServiceMock.create();
export const CapabilitiesServiceConstructor = jest
.fn()
.mockImplementation(() => MockCapabilitiesService);
jest.doMock('./capabilities', () => ({
CapabilitiesService: CapabilitiesServiceConstructor,
}));

View file

@ -17,9 +17,6 @@
* under the License.
*/
import { Observable } from 'rxjs';
import { take } from 'rxjs/operators';
import {
BasePathServiceConstructor,
ChromeServiceConstructor,
@ -42,6 +39,7 @@ import {
NotificationServiceConstructor,
OverlayServiceConstructor,
UiSettingsServiceConstructor,
MockCapabilitiesService,
} from './core_system.test.mocks';
import { CoreSystem } from './core_system';
@ -110,21 +108,11 @@ describe('constructor', () => {
expect(LegacyPlatformServiceConstructor).toHaveBeenCalledTimes(1);
expect(LegacyPlatformServiceConstructor).toHaveBeenCalledWith({
targetDomElement: expect.any(HTMLElement),
requireLegacyFiles,
useLegacyTestHarness,
});
});
it('passes a dom element to NotificationsService', () => {
createCoreSystem();
expect(NotificationServiceConstructor).toHaveBeenCalledTimes(1);
expect(NotificationServiceConstructor).toHaveBeenCalledWith({
targetDomElement$: expect.any(Observable),
});
});
it('passes browserSupportsCsp to ChromeService', () => {
createCoreSystem();
@ -157,7 +145,121 @@ describe('constructor', () => {
});
});
describe('#stop', () => {
describe('#setup()', () => {
function setupCore(rootDomElement = defaultCoreSystemParams.rootDomElement) {
const core = createCoreSystem({
...defaultCoreSystemParams,
rootDomElement,
});
return core.setup();
}
it('calls injectedMetadata#setup()', async () => {
await setupCore();
expect(MockInjectedMetadataService.setup).toHaveBeenCalledTimes(1);
});
it('calls http#setup()', async () => {
await setupCore();
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);
});
it('calls i18n#setup()', async () => {
await setupCore();
expect(MockI18nService.setup).toHaveBeenCalledTimes(1);
});
it('calls fatalErrors#setup()', async () => {
await setupCore();
expect(MockFatalErrorsService.setup).toHaveBeenCalledTimes(1);
});
it('calls notifications#setup()', async () => {
await setupCore();
expect(MockNotificationsService.setup).toHaveBeenCalledTimes(1);
});
it('calls chrome#setup()', async () => {
await setupCore();
expect(MockChromeService.setup).toHaveBeenCalledTimes(1);
});
it('calls plugin#setup()', async () => {
await setupCore();
expect(MockPluginsService.setup).toHaveBeenCalledTimes(1);
});
});
describe('#start()', () => {
async function startCore(rootDomElement = defaultCoreSystemParams.rootDomElement) {
const core = createCoreSystem({
...defaultCoreSystemParams,
rootDomElement,
});
await core.setup();
await core.start();
}
it('clears the children of the rootDomElement and appends container for legacyPlatform and notifications', async () => {
const root = document.createElement('div');
root.innerHTML = '<p>foo bar</p>';
await startCore(root);
expect(root.innerHTML).toBe('<div></div><div></div><div></div>');
});
it('calls capabilities#start()', async () => {
await startCore();
expect(MockCapabilitiesService.start).toHaveBeenCalledTimes(1);
});
it('calls i18n#start()', async () => {
await startCore();
expect(MockI18nService.start).toHaveBeenCalledTimes(1);
});
it('calls injectedMetadata#start()', async () => {
await startCore();
expect(MockInjectedMetadataService.start).toHaveBeenCalledTimes(1);
});
it('calls notifications#start() with a dom element', async () => {
await startCore();
expect(MockNotificationsService.start).toHaveBeenCalledTimes(1);
expect(MockNotificationsService.start).toHaveBeenCalledWith({
i18n: expect.any(Object),
targetDomElement: expect.any(HTMLElement),
});
});
it('calls plugins#start()', async () => {
await startCore();
expect(MockPluginsService.start).toHaveBeenCalledTimes(1);
});
it('calls legacyPlatform#start()', async () => {
await startCore();
expect(MockLegacyPlatformService.start).toHaveBeenCalledTimes(1);
});
it('calls overlays#start()', async () => {
await startCore();
expect(MockOverlayService.start).toHaveBeenCalledTimes(1);
});
});
describe('#stop()', () => {
it('calls legacyPlatform.stop()', () => {
const coreSystem = createCoreSystem();
@ -212,126 +314,50 @@ describe('#stop', () => {
});
await coreSystem.setup();
await coreSystem.start();
expect(rootDomElement.innerHTML).not.toBe('');
await coreSystem.stop();
expect(rootDomElement.innerHTML).toBe('');
});
});
describe('#setup()', () => {
function setupCore(rootDomElement = defaultCoreSystemParams.rootDomElement) {
const core = createCoreSystem({
...defaultCoreSystemParams,
rootDomElement,
});
return core.setup();
}
it('clears the children of the rootDomElement and appends container for legacyPlatform and notifications', async () => {
const root = document.createElement('div');
root.innerHTML = '<p>foo bar</p>';
await setupCore(root);
expect(root.innerHTML).toBe('<div></div><div></div><div></div>');
});
it('calls injectedMetadata#setup()', async () => {
await setupCore();
expect(MockInjectedMetadataService.setup).toHaveBeenCalledTimes(1);
});
it('calls http#setup()', async () => {
await setupCore();
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);
});
it('calls i18n#setup()', async () => {
await setupCore();
expect(MockI18nService.setup).toHaveBeenCalledTimes(1);
});
it('calls fatalErrors#setup()', async () => {
await setupCore();
expect(MockFatalErrorsService.setup).toHaveBeenCalledTimes(1);
});
it('calls notifications#setup()', async () => {
await setupCore();
expect(MockNotificationsService.setup).toHaveBeenCalledTimes(1);
});
it('calls chrome#setup()', async () => {
await setupCore();
expect(MockChromeService.setup).toHaveBeenCalledTimes(1);
});
it('calls overlays#setup()', () => {
setupCore();
expect(MockOverlayService.setup).toHaveBeenCalledTimes(1);
});
it('calls plugin#setup()', async () => {
await setupCore();
expect(MockPluginsService.setup).toHaveBeenCalledTimes(1);
});
});
describe('LegacyPlatform targetDomElement', () => {
it('only mounts the element when set up, before setting up the legacyPlatformService', async () => {
it('only mounts the element when start, after setting up the legacyPlatformService', async () => {
const rootDomElement = document.createElement('div');
const core = createCoreSystem({
rootDomElement,
});
let targetDomElementParentInSetup: HTMLElement;
MockLegacyPlatformService.setup.mockImplementation(() => {
targetDomElementParentInSetup = targetDomElement.parentElement;
let targetDomElementParentInStart: HTMLElement | null;
MockLegacyPlatformService.start.mockImplementation(async ({ targetDomElement }) => {
targetDomElementParentInStart = targetDomElement.parentElement;
});
// targetDomElement should not have a parent element when the LegacyPlatformService is constructed
const [[{ targetDomElement }]] = LegacyPlatformServiceConstructor.mock.calls;
expect(targetDomElement).toHaveProperty('parentElement', null);
// setting up the core system should mount the targetDomElement as a child of the rootDomElement
await core.setup();
expect(targetDomElementParentInSetup!).toBe(rootDomElement);
await core.start();
expect(targetDomElementParentInStart!).toBe(rootDomElement);
});
});
describe('Notifications targetDomElement', () => {
it('only mounts the element when set up, before setting up the notificationsService', async () => {
it('only mounts the element when started, after setting up the notificationsService', async () => {
const rootDomElement = document.createElement('div');
const core = createCoreSystem({
rootDomElement,
});
const [[{ targetDomElement$ }]] = NotificationServiceConstructor.mock.calls;
let targetDomElementParentInSetup: HTMLElement | null;
MockNotificationsService.setup.mockImplementation(
(): any => {
(targetDomElement$ as Observable<HTMLElement>).pipe(take(1)).subscribe({
next: targetDomElement => {
// The targetDomElement should already be a child once it's received by the NotificationsService
expect(targetDomElement.parentElement).not.toBeNull();
targetDomElementParentInSetup = targetDomElement.parentElement;
},
});
let targetDomElementParentInStart: HTMLElement | null;
MockNotificationsService.start.mockImplementation(
({ targetDomElement }): any => {
expect(targetDomElement.parentElement).not.toBeNull();
targetDomElementParentInStart = targetDomElement.parentElement;
}
);
// setting up the core system should mount the targetDomElement as a child of the rootDomElement
// setting up and starting the core system should mount the targetDomElement as a child of the rootDomElement
await core.setup();
expect(targetDomElementParentInSetup!).toBe(rootDomElement);
await core.start();
expect(targetDomElementParentInStart!).toBe(rootDomElement);
});
});

View file

@ -19,9 +19,7 @@
import './core.css';
import { Subject } from 'rxjs';
import { CoreSetup } from '.';
import { CoreSetup, CoreStart } from '.';
import { BasePathService } from './base_path';
import { CapabilitiesService } from './capabilities';
import { ChromeService } from './chrome';
@ -70,8 +68,6 @@ export class CoreSystem {
private readonly plugins: PluginsService;
private readonly rootDomElement: HTMLElement;
private readonly notificationsTargetDomElement$: Subject<HTMLDivElement>;
private readonly legacyPlatformTargetDomElement: HTMLDivElement;
private readonly overlayTargetDomElement: HTMLDivElement;
constructor(params: Params) {
@ -101,10 +97,7 @@ export class CoreSystem {
},
});
this.notificationsTargetDomElement$ = new Subject();
this.notifications = new NotificationsService({
targetDomElement$: this.notificationsTargetDomElement$.asObservable(),
});
this.notifications = new NotificationsService();
this.http = new HttpService();
this.basePath = new BasePathService();
this.uiSettings = new UiSettingsService();
@ -115,9 +108,7 @@ export class CoreSystem {
const core: CoreContext = {};
this.plugins = new PluginsService(core);
this.legacyPlatformTargetDomElement = document.createElement('div');
this.legacyPlatform = new LegacyPlatformService({
targetDomElement: this.legacyPlatformTargetDomElement,
requireLegacyFiles,
useLegacyTestHarness,
});
@ -126,19 +117,16 @@ export class CoreSystem {
public async setup() {
try {
const i18n = this.i18n.setup();
const notifications = this.notifications.setup({ i18n });
const injectedMetadata = this.injectedMetadata.setup();
const fatalErrors = this.fatalErrors.setup({ i18n });
const http = this.http.setup({ fatalErrors });
const overlays = this.overlay.setup({ i18n });
const basePath = this.basePath.setup({ injectedMetadata });
const capabilities = this.capabilities.setup({ injectedMetadata });
const uiSettings = this.uiSettings.setup({
notifications,
http,
injectedMetadata,
basePath,
});
const notifications = this.notifications.setup({ uiSettings });
const chrome = this.chrome.setup({
injectedMetadata,
notifications,
@ -150,31 +138,52 @@ export class CoreSystem {
fatalErrors,
http,
i18n,
capabilities,
injectedMetadata,
notifications,
uiSettings,
overlays,
};
// Services that do not expose contracts at setup
await this.plugins.setup(core);
await this.legacyPlatform.setup({ core });
return { fatalErrors };
} catch (error) {
this.fatalErrors.add(error);
}
}
public async start() {
try {
// ensure the rootDomElement is empty
this.rootDomElement.textContent = '';
this.rootDomElement.classList.add('coreSystemRootDomElement');
const notificationsTargetDomElement = document.createElement('div');
const legacyPlatformTargetDomElement = document.createElement('div');
this.rootDomElement.appendChild(notificationsTargetDomElement);
this.rootDomElement.appendChild(this.legacyPlatformTargetDomElement);
this.rootDomElement.appendChild(legacyPlatformTargetDomElement);
this.rootDomElement.appendChild(this.overlayTargetDomElement);
// Only provide the DOM element to notifications once it's attached to the page.
// This prevents notifications from timing out before being displayed.
this.notificationsTargetDomElement$.next(notificationsTargetDomElement);
const injectedMetadata = this.injectedMetadata.start();
const i18n = this.i18n.start();
const capabilities = this.capabilities.start({ injectedMetadata });
const notifications = this.notifications.start({
i18n,
targetDomElement: notificationsTargetDomElement,
});
const overlays = this.overlay.start({ i18n });
this.legacyPlatform.setup(core);
const core: CoreStart = {
capabilities,
i18n,
injectedMetadata,
notifications,
overlays,
};
return { fatalErrors };
await this.plugins.start(core);
await this.legacyPlatform.start({ core, targetDomElement: legacyPlatformTargetDomElement });
} catch (error) {
this.fatalErrors.add(error);
}

View file

@ -6,7 +6,109 @@ exports[`#setup() returns \`Context\` component 1`] = `
i18n={
Object {
"mapping": Object {
"euiBasicTable.selectAllRows": "Select all rows",
"euiBasicTable.selectThisRow": "Select this row",
"euiBasicTable.tableDescription": [Function],
"euiBottomBar.screenReaderAnnouncement": "There is a new menu opening with page level controls at the end of the document.",
"euiCodeBlock.copyButton": "Copy",
"euiCodeEditor.startEditing": "Press Enter to start editing.",
"euiCodeEditor.startInteracting": "Press Enter to start interacting with the code.",
"euiCodeEditor.stopEditing": "When you're done, press Escape to stop editing.",
"euiCodeEditor.stopInteracting": "When you're done, press Escape to stop interacting with the code.",
"euiCollapsedItemActions.allActions": "All actions",
"euiColorPicker.colorSelectionLabel": [Function],
"euiColorPicker.transparentColor": "transparent",
"euiComboBoxOptionsList.allOptionsSelected": "You've selected all available options",
"euiComboBoxOptionsList.alreadyAdded": [Function],
"euiComboBoxOptionsList.createCustomOption": [Function],
"euiComboBoxOptionsList.loadingOptions": "Loading options",
"euiComboBoxOptionsList.noAvailableOptions": "There aren't any options available",
"euiComboBoxOptionsList.noMatchingOptions": [Function],
"euiComboBoxPill.removeSelection": [Function],
"euiForm.addressFormErrors": "Please address the errors in your form.",
"euiFormControlLayoutClearButton.label": "Clear input",
"euiHeaderAlert.dismiss": "Dismiss",
"euiHeaderLinks.appNavigation": "App navigation",
"euiHeaderLinks.openNavigationMenu": "Open navigation menu",
"euiModal.closeModal": "Closes this modal window",
"euiPagination.jumpToLastPage": [Function],
"euiPagination.nextPage": "Next page",
"euiPagination.pageOfTotal": [Function],
"euiPagination.previousPage": "Previous page",
"euiPopover.screenReaderAnnouncement": "You are in a popup. To exit this popup, hit Escape.",
"euiStep.completeStep": "Step",
"euiStep.incompleteStep": "Incomplete Step",
"euiStepHorizontal.buttonTitle": [Function],
"euiStepHorizontal.step": "Step",
"euiStepNumber.hasErrors": "has errors",
"euiStepNumber.hasWarnings": "has warnings",
"euiStepNumber.isComplete": "complete",
"euiSuperSelect.screenReaderAnnouncement": [Function],
"euiSuperSelectControl.selectAnOption": [Function],
"euiTablePagination.rowsPerPage": "Rows per page",
"euiTableSortMobile.sorting": "Sorting",
"euiToast.dismissToast": "Dismiss toast",
"euiToast.newNotification": "A new notification appears",
"euiToast.notification": "Notification",
},
}
}
>
content
</MockEuiContext>
</MockI18nProvider>
`;
exports[`#start() returns \`Context\` component 1`] = `
<MockI18nProvider>
<MockEuiContext
i18n={
Object {
"mapping": Object {
"euiBasicTable.selectAllRows": "Select all rows",
"euiBasicTable.selectThisRow": "Select this row",
"euiBasicTable.tableDescription": [Function],
"euiBottomBar.screenReaderAnnouncement": "There is a new menu opening with page level controls at the end of the document.",
"euiCodeBlock.copyButton": "Copy",
"euiCodeEditor.startEditing": "Press Enter to start editing.",
"euiCodeEditor.startInteracting": "Press Enter to start interacting with the code.",
"euiCodeEditor.stopEditing": "When you're done, press Escape to stop editing.",
"euiCodeEditor.stopInteracting": "When you're done, press Escape to stop interacting with the code.",
"euiCollapsedItemActions.allActions": "All actions",
"euiColorPicker.colorSelectionLabel": [Function],
"euiColorPicker.transparentColor": "transparent",
"euiComboBoxOptionsList.allOptionsSelected": "You've selected all available options",
"euiComboBoxOptionsList.alreadyAdded": [Function],
"euiComboBoxOptionsList.createCustomOption": [Function],
"euiComboBoxOptionsList.loadingOptions": "Loading options",
"euiComboBoxOptionsList.noAvailableOptions": "There aren't any options available",
"euiComboBoxOptionsList.noMatchingOptions": [Function],
"euiComboBoxPill.removeSelection": [Function],
"euiForm.addressFormErrors": "Please address the errors in your form.",
"euiFormControlLayoutClearButton.label": "Clear input",
"euiHeaderAlert.dismiss": "Dismiss",
"euiHeaderLinks.appNavigation": "App navigation",
"euiHeaderLinks.openNavigationMenu": "Open navigation menu",
"euiModal.closeModal": "Closes this modal window",
"euiPagination.jumpToLastPage": [Function],
"euiPagination.nextPage": "Next page",
"euiPagination.pageOfTotal": [Function],
"euiPagination.previousPage": "Previous page",
"euiPopover.screenReaderAnnouncement": "You are in a popup. To exit this popup, hit Escape.",
"euiStep.completeStep": "Step",
"euiStep.incompleteStep": "Incomplete Step",
"euiStepHorizontal.buttonTitle": [Function],
"euiStepHorizontal.step": "Step",
"euiStepNumber.hasErrors": "has errors",
"euiStepNumber.hasWarnings": "has warnings",
"euiStepNumber.isComplete": "complete",
"euiSuperSelect.screenReaderAnnouncement": [Function],
"euiSuperSelectControl.selectAnOption": [Function],
"euiTablePagination.rowsPerPage": "Rows per page",
"euiTableSortMobile.sorting": "Sorting",
"euiToast.dismissToast": "Dismiss toast",
"euiToast.newNotification": "A new notification appears",
"euiToast.notification": "Notification",
},
}
}

View file

@ -30,17 +30,23 @@ const createSetupContractMock = () => {
return setupContract;
};
// Start contract is identical to setup
const createStartContractMock = createSetupContractMock;
type I18nServiceContract = PublicMethodsOf<I18nService>;
const createMock = () => {
const mocked: jest.Mocked<I18nServiceContract> = {
setup: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
};
mocked.setup.mockReturnValue(createSetupContractMock());
mocked.start.mockReturnValue(createStartContractMock());
return mocked;
};
export const i18nServiceMock = {
create: createMock,
createSetupContract: createSetupContractMock,
createStartContract: createStartContractMock,
};

View file

@ -53,3 +53,13 @@ describe('#setup()', () => {
expect(shallow(<i18n.Context>content</i18n.Context>)).toMatchSnapshot();
});
});
describe('#start()', () => {
it('returns `Context` component', () => {
const i18nService = new I18nService();
const i18n = i18nService.start();
expect(shallow(<i18n.Context>content</i18n.Context>)).toMatchSnapshot();
});
});

View file

@ -21,7 +21,11 @@ import React from 'react';
import { EuiContext } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { I18nProvider } from '@kbn/i18n/react';
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
interface EuiValues {
[key: string]: any;
}
/**
* Service that is responsible for i18n capabilities.
@ -30,8 +34,223 @@ import { I18nProvider } from '@kbn/i18n/react';
export class I18nService {
public setup() {
const mapping = {
'euiBasicTable.selectAllRows': i18n.translate('core.euiBasicTable.selectAllRows', {
defaultMessage: 'Select all rows',
description: 'ARIA and displayed label on a checkbox to select all table rows',
}),
'euiBasicTable.selectThisRow': i18n.translate('core.euiBasicTable.selectThisRow', {
defaultMessage: 'Select this row',
description: 'ARIA and displayed label on a checkbox to select a single table row',
}),
'euiBasicTable.tableDescription': ({ itemCount }: EuiValues) =>
i18n.translate('core.euiBasicTable.tableDescription', {
defaultMessage: 'Below is a table of {itemCount} items.',
values: { itemCount },
description: 'Screen reader text to describe the size of a table',
}),
'euiBottomBar.screenReaderAnnouncement': i18n.translate(
'core.euiBottomBar.screenReaderAnnouncement',
{
defaultMessage:
'There is a new menu opening with page level controls at the end of the document.',
description:
'Screen reader announcement that functionality is available in the page document',
}
),
'euiCodeBlock.copyButton': i18n.translate('core.euiCodeBlock.copyButton', {
defaultMessage: 'Copy',
description: 'ARIA label for a button that copies source code text to the clipboard',
}),
'euiCodeEditor.startEditing': i18n.translate('core.euiCodeEditor.startEditing', {
defaultMessage: 'Press Enter to start editing.',
}),
'euiCodeEditor.startInteracting': i18n.translate('core.euiCodeEditor.startInteracting', {
defaultMessage: 'Press Enter to start interacting with the code.',
}),
'euiCodeEditor.stopEditing': i18n.translate('core.euiCodeEditor.stopEditing', {
defaultMessage: "When you're done, press Escape to stop editing.",
}),
'euiCodeEditor.stopInteracting': i18n.translate('core.euiCodeEditor.stopInteracting', {
defaultMessage: "When you're done, press Escape to stop interacting with the code.",
}),
'euiCollapsedItemActions.allActions': i18n.translate(
'core.euiCollapsedItemActions.allActions',
{
defaultMessage: 'All actions',
description:
'ARIA label and tooltip content describing a button that expands an actions menu',
}
),
'euiColorPicker.colorSelectionLabel': ({ colorValue }: EuiValues) =>
i18n.translate('core.euiColorPicker.colorSelectionLabel', {
defaultMessage: 'Color selection is {colorValue}',
values: { colorValue },
}),
'euiColorPicker.transparentColor': i18n.translate('core.euiColorPicker.transparentColor', {
defaultMessage: 'transparent',
description: 'Describes a color that is fully transparent',
}),
'euiComboBoxOptionsList.allOptionsSelected': i18n.translate(
'core.euiComboBoxOptionsList.allOptionsSelected',
{
defaultMessage: "You've selected all available options",
}
),
'euiComboBoxOptionsList.alreadyAdded': ({ label }: EuiValues) => (
<FormattedMessage
id="core.euiComboBoxOptionsList.alreadyAdded"
defaultMessage="{label} has already been added"
values={{ label }}
/>
),
'euiComboBoxOptionsList.createCustomOption': ({ key, searchValue }: EuiValues) => (
<FormattedMessage
id="core.euiComboBoxOptionsList.createCustomOption"
defaultMessage="Hit {key} to add {searchValue} as a custom option"
values={{ key, searchValue }}
/>
),
'euiComboBoxOptionsList.loadingOptions': i18n.translate(
'core.euiComboBoxOptionsList.loadingOptions',
{
defaultMessage: 'Loading options',
description: 'Placeholder message while data is asynchronously loaded',
}
),
'euiComboBoxOptionsList.noAvailableOptions': i18n.translate(
'core.euiComboBoxOptionsList.noAvailableOptions',
{
defaultMessage: "There aren't any options available",
}
),
'euiComboBoxOptionsList.noMatchingOptions': ({ searchValue }: EuiValues) => (
<FormattedMessage
id="core.euiComboBoxOptionsList.noMatchingOptions"
defaultMessage="{searchValue} doesn't match any options"
values={{ searchValue }}
/>
),
'euiComboBoxPill.removeSelection': ({ children }: EuiValues) =>
i18n.translate('core.euiComboBoxPill.removeSelection', {
defaultMessage: 'Remove {children} from selection in this group',
values: { children },
description: 'ARIA label, `children` is the human-friendly value of an option',
}),
'euiForm.addressFormErrors': i18n.translate('core.euiForm.addressFormErrors', {
defaultMessage: 'Please address the errors in your form.',
}),
'euiFormControlLayoutClearButton.label': i18n.translate(
'core.euiFormControlLayoutClearButton.label',
{
defaultMessage: 'Clear input',
description: 'ARIA label on a button that removes any entry in a form field',
}
),
'euiHeaderAlert.dismiss': i18n.translate('core.euiHeaderAlert.dismiss', {
defaultMessage: 'Dismiss',
description: 'ARIA label on a button that dismisses/removes a notification',
}),
'euiHeaderLinks.appNavigation': i18n.translate('core.euiHeaderLinks.appNavigation', {
defaultMessage: 'App navigation',
description: 'ARIA label on a `nav` element',
}),
'euiHeaderLinks.openNavigationMenu': i18n.translate(
'core.euiHeaderLinks.openNavigationMenu',
{
defaultMessage: 'Open navigation menu',
}
),
'euiModal.closeModal': i18n.translate('core.euiModal.closeModal', {
defaultMessage: 'Closes this modal window',
}),
'euiPagination.jumpToLastPage': ({ pageCount }: EuiValues) =>
i18n.translate('core.euiPagination.jumpToLastPage', {
defaultMessage: 'Jump to the last page, number {pageCount}',
values: { pageCount },
}),
'euiPagination.nextPage': i18n.translate('core.euiPagination.nextPage', {
defaultMessage: 'Next page',
}),
'euiPagination.pageOfTotal': ({ page, total }: EuiValues) =>
i18n.translate('core.euiPagination.pageOfTotal', {
defaultMessage: 'Page {page} of {total}',
values: { page, total },
}),
'euiPagination.previousPage': i18n.translate('core.euiPagination.previousPage', {
defaultMessage: 'Previous page',
}),
'euiPopover.screenReaderAnnouncement': i18n.translate(
'core.euiPopover.screenReaderAnnouncement',
{
defaultMessage: 'You are in a popup. To exit this popup, hit Escape.',
}
),
'euiStep.completeStep': i18n.translate('core.euiStep.completeStep', {
defaultMessage: 'Step',
description:
'See https://elastic.github.io/eui/#/navigation/steps to know how Step control looks like',
}),
'euiStep.incompleteStep': i18n.translate('core.euiStep.incompleteStep', {
defaultMessage: 'Incomplete Step',
}),
'euiStepHorizontal.buttonTitle': ({ step, title, disabled, isComplete }: EuiValues) => {
return i18n.translate('core.euiStepHorizontal.buttonTitle', {
defaultMessage:
'Step {step}: {title}{titleAppendix, select, completed { is completed} disabled { is disabled} other {}}',
values: {
step,
title,
titleAppendix: disabled ? 'disabled' : isComplete ? 'completed' : '',
},
});
},
'euiStepHorizontal.step': i18n.translate('core.euiStepHorizontal.step', {
defaultMessage: 'Step',
description: 'Screen reader text announcing information about a step in some process',
}),
'euiStepNumber.hasErrors': i18n.translate('core.euiStepNumber.hasErrors', {
defaultMessage: 'has errors',
description:
'Used as the title attribute on an image or svg icon to indicate a given process step has errors',
}),
'euiStepNumber.hasWarnings': i18n.translate('core.euiStepNumber.hasWarnings', {
defaultMessage: 'has warnings',
description:
'Used as the title attribute on an image or svg icon to indicate a given process step has warnings',
}),
'euiStepNumber.isComplete': i18n.translate('core.euiStepNumber.isComplete', {
defaultMessage: 'complete',
description:
'Used as the title attribute on an image or svg icon to indicate a given process step is complete',
}),
'euiSuperSelect.screenReaderAnnouncement': ({ optionsCount }: EuiValues) =>
i18n.translate('core.euiSuperSelect.screenReaderAnnouncement', {
defaultMessage:
'You are in a form selector of {optionsCount} items and must select a single option. Use the Up and Down keys to navigate or Escape to close.',
values: { optionsCount },
}),
'euiSuperSelectControl.selectAnOption': ({ selectedValue }: EuiValues) =>
i18n.translate('core.euiSuperSelectControl.selectAnOption', {
defaultMessage: 'Select an option: {selectedValue}, is selected',
values: { selectedValue },
}),
'euiTablePagination.rowsPerPage': i18n.translate('core.euiTablePagination.rowsPerPage', {
defaultMessage: 'Rows per page',
description: 'Displayed in a button that toggles a table pagination menu',
}),
'euiTableSortMobile.sorting': i18n.translate('core.euiTableSortMobile.sorting', {
defaultMessage: 'Sorting',
description: 'Displayed in a button that toggles a table sorting menu',
}),
'euiToast.dismissToast': i18n.translate('core.euiToast.dismissToast', {
defaultMessage: 'Dismiss toast',
}),
'euiToast.newNotification': i18n.translate('core.euiToast.newNotification', {
defaultMessage: 'A new notification appears',
}),
'euiToast.notification': i18n.translate('core.euiToast.notification', {
defaultMessage: 'Notification',
description: 'ARIA label on an element containing a notification',
}),
};
@ -48,6 +267,10 @@ export class I18nService {
return setup;
}
public start() {
return this.setup();
}
public stop() {
// nothing to do here currently
}
@ -66,3 +289,5 @@ export interface I18nSetup {
*/
Context: ({ children }: { children: React.ReactNode }) => JSX.Element;
}
export type I18nStart = I18nSetup;

View file

@ -17,4 +17,4 @@
* under the License.
*/
export { I18nService, I18nSetup } from './i18n_service';
export { I18nService, I18nSetup, I18nStart } from './i18n_service';

View file

@ -18,14 +18,24 @@
*/
import { BasePathSetup } from './base_path';
import { Capabilities, CapabilitiesSetup } from './capabilities';
import { Capabilities, CapabilitiesStart } from './capabilities';
import { ChromeBrand, ChromeBreadcrumb, ChromeHelpExtension, ChromeSetup } from './chrome';
import { FatalErrorsSetup } from './fatal_errors';
import { HttpSetup } from './http';
import { I18nSetup } from './i18n';
import { InjectedMetadataParams, InjectedMetadataSetup } from './injected_metadata';
import { NotificationsSetup, Toast, ToastInput, ToastsSetup } from './notifications';
import { FlyoutRef, OverlaySetup } from './overlays';
import { I18nSetup, I18nStart } from './i18n';
import {
InjectedMetadataParams,
InjectedMetadataSetup,
InjectedMetadataStart,
} from './injected_metadata';
import {
NotificationsSetup,
Toast,
ToastInput,
ToastsApi,
NotificationsStart,
} from './notifications';
import { FlyoutRef, OverlayStart } from './overlays';
import { Plugin, PluginInitializer, PluginInitializerContext, PluginSetupContext } from './plugins';
import { UiSettingsClient, UiSettingsSetup, UiSettingsState } from './ui_settings';
@ -53,39 +63,51 @@ export interface CoreSetup {
http: HttpSetup;
/** {@link BasePathSetup} */
basePath: BasePathSetup;
/** {@link CapabilitiesSetup} */
capabilities: CapabilitiesSetup;
/** {@link UiSettingsSetup} */
uiSettings: UiSettingsSetup;
/** {@link ChromeSetup} */
chrome: ChromeSetup;
/** {@link OverlaySetup} */
overlays: OverlaySetup;
}
export interface CoreStart {
/** {@link CapabilitiesStart} */
capabilities: CapabilitiesStart;
/** {@link I18nStart} */
i18n: I18nStart;
/** {@link InjectedMetadataStart} */
injectedMetadata: InjectedMetadataStart;
/** {@link NotificationsStart} */
notifications: NotificationsStart;
/** {@link OverlayStart} */
overlays: OverlayStart;
}
export {
BasePathSetup,
HttpSetup,
FatalErrorsSetup,
CapabilitiesSetup,
Capabilities,
CapabilitiesStart,
ChromeSetup,
ChromeBreadcrumb,
ChromeBrand,
ChromeHelpExtension,
I18nSetup,
I18nStart,
InjectedMetadataSetup,
InjectedMetadataStart,
InjectedMetadataParams,
Plugin,
PluginInitializer,
PluginInitializerContext,
PluginSetupContext,
NotificationsSetup,
OverlaySetup,
NotificationsStart,
OverlayStart,
FlyoutRef,
Toast,
ToastInput,
ToastsSetup,
ToastsApi,
UiSettingsClient,
UiSettingsState,
UiSettingsSetup,

View file

@ -21,4 +21,5 @@ export {
InjectedMetadataService,
InjectedMetadataParams,
InjectedMetadataSetup,
InjectedMetadataStart,
} from './injected_metadata_service';

View file

@ -40,18 +40,18 @@ const createSetupContractMock = () => {
return setupContract;
};
const createStartContractMock = createSetupContractMock;
type InjectedMetadataServiceContract = PublicMethodsOf<InjectedMetadataService>;
const createMock = () => {
const mocked: jest.Mocked<InjectedMetadataServiceContract> = {
setup: jest.fn(),
getKibanaVersion: jest.fn(),
getKibanaBuildNumber: jest.fn(),
};
mocked.setup.mockReturnValue(createSetupContractMock());
return mocked;
};
const createMock = (): jest.Mocked<InjectedMetadataServiceContract> => ({
setup: jest.fn().mockReturnValue(createSetupContractMock()),
start: jest.fn().mockReturnValue(createStartContractMock()),
getKibanaVersion: jest.fn(),
getKibanaBuildNumber: jest.fn(),
});
export const injectedMetadataServiceMock = {
create: createMock,
createSetupContract: createSetupContractMock,
createStartContract: createStartContractMock,
};

View file

@ -105,6 +105,10 @@ export class InjectedMetadataService {
};
}
public start(): InjectedMetadataStart {
return this.setup();
}
public getKibanaVersion() {
return this.state.version;
}
@ -154,3 +158,6 @@ export interface InjectedMetadataSetup {
[key: string]: unknown;
};
}
/** @public */
export type InjectedMetadataStart = InjectedMetadataSetup;

View file

@ -6,7 +6,6 @@ Array [
"ui/i18n",
"ui/notify/fatal_error",
"ui/notify/toasts",
"ui/capabilities",
"ui/chrome/api/loading_count",
"ui/chrome/api/base_path",
"ui/chrome/api/ui_settings",
@ -27,7 +26,6 @@ Array [
"ui/i18n",
"ui/notify/fatal_error",
"ui/notify/toasts",
"ui/capabilities",
"ui/chrome/api/loading_count",
"ui/chrome/api/base_path",
"ui/chrome/api/ui_settings",
@ -42,6 +40,48 @@ Array [
]
`;
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/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/test_harness",
"legacy files",
"ui/capabilities",
]
`;
exports[`#stop() destroys the angular scope and empties the targetDomElement if angular is bootstrapped to targetDomElement 1`] = `
<div
class="ng-scope"

View file

@ -22,6 +22,7 @@ type LegacyPlatformServiceContract = PublicMethodsOf<LegacyPlatformService>;
const createMock = () => {
const mocked: jest.Mocked<LegacyPlatformServiceContract> = {
setup: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
};
return mocked;

View file

@ -25,7 +25,7 @@ const mockUiMetadataInit = jest.fn();
jest.mock('ui/metadata', () => {
mockLoadOrder.push('ui/metadata');
return {
__newPlatformInit__: mockUiMetadataInit,
__newPlatformSetup__: mockUiMetadataInit,
};
});
@ -49,7 +49,7 @@ const mockI18nContextInit = jest.fn();
jest.mock('ui/i18n', () => {
mockLoadOrder.push('ui/i18n');
return {
__newPlatformInit__: mockI18nContextInit,
__newPlatformSetup__: mockI18nContextInit,
};
});
@ -57,7 +57,7 @@ const mockUICapabilitiesInit = jest.fn();
jest.mock('ui/capabilities', () => {
mockLoadOrder.push('ui/capabilities');
return {
__newPlatformInit__: mockUICapabilitiesInit,
__newPlatformStart__: mockUICapabilitiesInit,
};
});
@ -65,7 +65,7 @@ const mockFatalErrorInit = jest.fn();
jest.mock('ui/notify/fatal_error', () => {
mockLoadOrder.push('ui/notify/fatal_error');
return {
__newPlatformInit__: mockFatalErrorInit,
__newPlatformSetup__: mockFatalErrorInit,
};
});
@ -73,7 +73,7 @@ const mockNotifyToastsInit = jest.fn();
jest.mock('ui/notify/toasts', () => {
mockLoadOrder.push('ui/notify/toasts');
return {
__newPlatformInit__: mockNotifyToastsInit,
__newPlatformSetup__: mockNotifyToastsInit,
};
});
@ -81,7 +81,7 @@ const mockHttpInit = jest.fn();
jest.mock('ui/chrome/api/loading_count', () => {
mockLoadOrder.push('ui/chrome/api/loading_count');
return {
__newPlatformInit__: mockHttpInit,
__newPlatformSetup__: mockHttpInit,
};
});
@ -89,7 +89,7 @@ const mockBasePathInit = jest.fn();
jest.mock('ui/chrome/api/base_path', () => {
mockLoadOrder.push('ui/chrome/api/base_path');
return {
__newPlatformInit__: mockBasePathInit,
__newPlatformSetup__: mockBasePathInit,
};
});
@ -97,7 +97,7 @@ const mockUiSettingsInit = jest.fn();
jest.mock('ui/chrome/api/ui_settings', () => {
mockLoadOrder.push('ui/chrome/api/ui_settings');
return {
__newPlatformInit__: mockUiSettingsInit,
__newPlatformSetup__: mockUiSettingsInit,
};
});
@ -105,7 +105,7 @@ const mockInjectedVarsInit = jest.fn();
jest.mock('ui/chrome/api/injected_vars', () => {
mockLoadOrder.push('ui/chrome/api/injected_vars');
return {
__newPlatformInit__: mockInjectedVarsInit,
__newPlatformSetup__: mockInjectedVarsInit,
};
});
@ -113,7 +113,7 @@ const mockChromeControlsInit = jest.fn();
jest.mock('ui/chrome/api/controls', () => {
mockLoadOrder.push('ui/chrome/api/controls');
return {
__newPlatformInit__: mockChromeControlsInit,
__newPlatformSetup__: mockChromeControlsInit,
};
});
@ -121,7 +121,7 @@ const mockChromeHelpExtensionInit = jest.fn();
jest.mock('ui/chrome/api/help_extension', () => {
mockLoadOrder.push('ui/chrome/api/help_extension');
return {
__newPlatformInit__: mockChromeHelpExtensionInit,
__newPlatformSetup__: mockChromeHelpExtensionInit,
};
});
@ -129,7 +129,7 @@ const mockChromeThemeInit = jest.fn();
jest.mock('ui/chrome/api/theme', () => {
mockLoadOrder.push('ui/chrome/api/theme');
return {
__newPlatformInit__: mockChromeThemeInit,
__newPlatformSetup__: mockChromeThemeInit,
};
});
@ -137,7 +137,7 @@ const mockChromeBreadcrumbsInit = jest.fn();
jest.mock('ui/chrome/api/breadcrumbs', () => {
mockLoadOrder.push('ui/chrome/api/breadcrumbs');
return {
__newPlatformInit__: mockChromeBreadcrumbsInit,
__newPlatformSetup__: mockChromeBreadcrumbsInit,
};
});
@ -145,7 +145,7 @@ const mockGlobalNavStateInit = jest.fn();
jest.mock('ui/chrome/services/global_nav_state', () => {
mockLoadOrder.push('ui/chrome/services/global_nav_state');
return {
__newPlatformInit__: mockGlobalNavStateInit,
__newPlatformSetup__: mockGlobalNavStateInit,
};
});
@ -168,28 +168,42 @@ const httpSetup = httpServiceMock.createSetupContract();
const i18nSetup = i18nServiceMock.createSetupContract();
const injectedMetadataSetup = injectedMetadataServiceMock.createSetupContract();
const notificationsSetup = notificationServiceMock.createSetupContract();
const capabilitiesSetup = capabilitiesServiceMock.createSetupContract();
const uiSettingsSetup = uiSettingsServiceMock.createSetupContract();
const overlaySetup = overlayServiceMock.createSetupContract();
const defaultParams = {
targetDomElement: document.createElement('div'),
requireLegacyFiles: jest.fn(() => {
mockLoadOrder.push('legacy files');
}),
};
const defaultSetupDeps = {
i18n: i18nSetup,
fatalErrors: fatalErrorsSetup,
injectedMetadata: injectedMetadataSetup,
notifications: notificationsSetup,
http: httpSetup,
basePath: basePathSetup,
capabilities: capabilitiesSetup,
uiSettings: uiSettingsSetup,
chrome: chromeSetup,
overlays: overlaySetup,
core: {
i18n: i18nSetup,
fatalErrors: fatalErrorsSetup,
injectedMetadata: injectedMetadataSetup,
notifications: notificationsSetup,
http: httpSetup,
basePath: basePathSetup,
uiSettings: uiSettingsSetup,
chrome: chromeSetup,
},
};
const capabilitiesStart = capabilitiesServiceMock.createStartContract();
const i18nStart = i18nServiceMock.createStartContract();
const injectedMetadataStart = injectedMetadataServiceMock.createStartContract();
const notificationsStart = notificationServiceMock.createStartContract();
const overlayStart = overlayServiceMock.createStartContract();
const defaultStartDeps = {
core: {
capabilities: capabilitiesStart,
i18n: i18nStart,
injectedMetadata: injectedMetadataStart,
notifications: notificationsStart,
overlays: overlayStart,
},
targetDomElement: document.createElement('div'),
};
afterEach(() => {
@ -226,17 +240,6 @@ describe('#setup()', () => {
expect(mockI18nContextInit).toHaveBeenCalledWith(i18nSetup.Context);
});
it('passes uiCapabilities to ui/capabilities', () => {
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
});
legacyPlatform.setup(defaultSetupDeps);
expect(mockUICapabilitiesInit).toHaveBeenCalledTimes(1);
expect(mockUICapabilitiesInit).toHaveBeenCalledWith(capabilitiesSetup);
});
it('passes fatalErrors service to ui/notify/fatal_errors', () => {
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
@ -357,35 +360,6 @@ describe('#setup()', () => {
expect(mockGlobalNavStateInit).toHaveBeenCalledTimes(1);
expect(mockGlobalNavStateInit).toHaveBeenCalledWith(chromeSetup);
});
describe('useLegacyTestHarness = false', () => {
it('passes the targetDomElement to ui/chrome', () => {
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
});
legacyPlatform.setup(defaultSetupDeps);
expect(mockUiTestHarnessBootstrap).not.toHaveBeenCalled();
expect(mockUiChromeBootstrap).toHaveBeenCalledTimes(1);
expect(mockUiChromeBootstrap).toHaveBeenCalledWith(defaultParams.targetDomElement);
});
});
describe('useLegacyTestHarness = true', () => {
it('passes the targetDomElement to ui/test_harness', () => {
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
useLegacyTestHarness: true,
});
legacyPlatform.setup(defaultSetupDeps);
expect(mockUiChromeBootstrap).not.toHaveBeenCalled();
expect(mockUiTestHarnessBootstrap).toHaveBeenCalledTimes(1);
expect(mockUiTestHarnessBootstrap).toHaveBeenCalledWith(defaultParams.targetDomElement);
});
});
});
describe('load order', () => {
@ -420,6 +394,84 @@ describe('#setup()', () => {
});
});
describe('#start()', () => {
it('passes uiCapabilities to ui/capabilities', () => {
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
});
legacyPlatform.setup(defaultSetupDeps);
legacyPlatform.start(defaultStartDeps);
expect(mockUICapabilitiesInit).toHaveBeenCalledTimes(1);
expect(mockUICapabilitiesInit).toHaveBeenCalledWith(capabilitiesStart);
});
describe('useLegacyTestHarness = false', () => {
it('passes the targetDomElement to ui/chrome', () => {
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
});
legacyPlatform.setup(defaultSetupDeps);
legacyPlatform.start(defaultStartDeps);
expect(mockUiTestHarnessBootstrap).not.toHaveBeenCalled();
expect(mockUiChromeBootstrap).toHaveBeenCalledTimes(1);
expect(mockUiChromeBootstrap).toHaveBeenCalledWith(defaultStartDeps.targetDomElement);
});
});
describe('useLegacyTestHarness = true', () => {
it('passes the targetDomElement to ui/test_harness', () => {
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
useLegacyTestHarness: true,
});
legacyPlatform.setup(defaultSetupDeps);
legacyPlatform.start(defaultStartDeps);
expect(mockUiChromeBootstrap).not.toHaveBeenCalled();
expect(mockUiTestHarnessBootstrap).toHaveBeenCalledTimes(1);
expect(mockUiTestHarnessBootstrap).toHaveBeenCalledWith(defaultStartDeps.targetDomElement);
});
});
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);
legacyPlatform.start(defaultStartDeps);
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);
legacyPlatform.start(defaultStartDeps);
expect(mockLoadOrder).toMatchSnapshot();
});
});
});
});
describe('#stop()', () => {
it('does nothing if angular was not bootstrapped to targetDomElement', () => {
const targetDomElement = document.createElement('div');
@ -429,20 +481,18 @@ describe('#stop()', () => {
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
targetDomElement,
});
legacyPlatform.stop();
expect(targetDomElement).toMatchSnapshot();
});
it('destroys the angular scope and empties the targetDomElement if angular is bootstrapped to targetDomElement', () => {
it('destroys the angular scope and empties the targetDomElement if angular is bootstrapped to targetDomElement', async () => {
const targetDomElement = document.createElement('div');
const scopeDestroySpy = jest.fn();
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
targetDomElement,
});
// simulate bootstrapping with a module "foo"
@ -459,6 +509,8 @@ describe('#stop()', () => {
angular.bootstrap(targetDomElement, ['foo']);
await legacyPlatform.setup(defaultSetupDeps);
legacyPlatform.start({ ...defaultStartDeps, targetDomElement });
legacyPlatform.stop();
expect(targetDomElement).toMatchSnapshot();

View file

@ -18,14 +18,27 @@
*/
import angular from 'angular';
import { CoreSetup } from '../';
import { CoreSetup, CoreStart } from '../';
/** @internal */
export interface LegacyPlatformParams {
targetDomElement: HTMLElement;
requireLegacyFiles: () => void;
useLegacyTestHarness?: boolean;
}
interface SetupDeps {
core: CoreSetup;
}
interface StartDeps {
core: CoreStart;
targetDomElement: HTMLElement;
}
interface BootstrapModule {
bootstrap: (targetDomElement: HTMLElement) => void;
}
/**
* The LegacyPlatformService is responsible for initializing
* the legacy platform by injecting parts of the new platform
@ -34,9 +47,12 @@ export interface LegacyPlatformParams {
* setup either the app or browser tests.
*/
export class LegacyPlatformService {
private bootstrapModule?: BootstrapModule;
private targetDomElement?: HTMLElement;
constructor(private readonly params: LegacyPlatformParams) {}
public setup(core: CoreSetup) {
public async setup({ core }: SetupDeps) {
const {
i18n,
injectedMetadata,
@ -44,40 +60,53 @@ export class LegacyPlatformService {
notifications,
http,
basePath,
capabilities,
uiSettings,
chrome,
} = core;
// 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').__newPlatformInit__(core);
require('ui/metadata').__newPlatformInit__(injectedMetadata.getLegacyMetadata());
require('ui/i18n').__newPlatformInit__(i18n.Context);
require('ui/notify/fatal_error').__newPlatformInit__(fatalErrors);
require('ui/notify/toasts').__newPlatformInit__(notifications.toasts);
require('ui/capabilities').__newPlatformInit__(capabilities);
require('ui/chrome/api/loading_count').__newPlatformInit__(http);
require('ui/chrome/api/base_path').__newPlatformInit__(basePath);
require('ui/chrome/api/ui_settings').__newPlatformInit__(uiSettings);
require('ui/chrome/api/injected_vars').__newPlatformInit__(injectedMetadata);
require('ui/chrome/api/controls').__newPlatformInit__(chrome);
require('ui/chrome/api/help_extension').__newPlatformInit__(chrome);
require('ui/chrome/api/theme').__newPlatformInit__(chrome);
require('ui/chrome/api/breadcrumbs').__newPlatformInit__(chrome);
require('ui/chrome/services/global_nav_state').__newPlatformInit__(chrome);
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/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/breadcrumbs').__newPlatformSetup__(chrome);
require('ui/chrome/services/global_nav_state').__newPlatformSetup__(chrome);
// Load the bootstrap module before loading the legacy platform files so that
// the bootstrap module can modify the environment a bit first
const bootstrapModule = this.loadBootstrapModule();
this.bootstrapModule = this.loadBootstrapModule();
// require the files that will tie into the legacy platform
this.params.requireLegacyFiles();
}
bootstrapModule.bootstrap(this.params.targetDomElement);
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.capabilities);
this.bootstrapModule.bootstrap(this.targetDomElement);
}
public stop() {
const angularRoot = angular.element(this.params.targetDomElement);
if (!this.targetDomElement) {
return;
}
const angularRoot = angular.element(this.targetDomElement);
const injector$ = angularRoot.injector();
// if we haven't gotten to the point of bootstraping
@ -90,12 +119,10 @@ export class LegacyPlatformService {
injector$.get('$rootScope').$destroy();
// clear the inner html of the root angular element
this.params.targetDomElement.textContent = '';
this.targetDomElement.textContent = '';
}
private loadBootstrapModule(): {
bootstrap: (targetDomElement: HTMLElement) => void;
} {
private loadBootstrapModule(): BootstrapModule {
if (this.params.useLegacyTestHarness) {
// wrapped in NODE_ENV check so the `ui/test_harness` module
// is not included in the distributable

View file

@ -17,5 +17,9 @@
* under the License.
*/
export { Toast, ToastInput, ToastsSetup } from './toasts';
export { NotificationsService, NotificationsSetup } from './notifications_service';
export { Toast, ToastInput, ToastsApi } from './toasts';
export {
NotificationsService,
NotificationsSetup,
NotificationsStart,
} from './notifications_service';

View file

@ -16,22 +16,35 @@
* specific language governing permissions and limitations
* under the License.
*/
import { NotificationsService, NotificationsSetup } from './notifications_service';
import {
NotificationsService,
NotificationsSetup,
NotificationsStart,
} from './notifications_service';
import { toastsServiceMock } from './toasts/toasts_service.mock';
import { ToastsSetup } from './toasts/toasts_start';
import { ToastsApi } from './toasts/toasts_api';
const createSetupContractMock = () => {
const setupContract: jest.Mocked<NotificationsSetup> = {
// we have to suppress type errors until decide how to mock es6 class
toasts: (toastsServiceMock.createSetupContract() as unknown) as ToastsSetup,
toasts: (toastsServiceMock.createSetupContract() as unknown) as ToastsApi,
};
return setupContract;
};
const createStartContractMock = () => {
const startContract: jest.Mocked<NotificationsStart> = {
// we have to suppress type errors until decide how to mock es6 class
toasts: (toastsServiceMock.createStartContract() as unknown) as ToastsApi,
};
return startContract;
};
type NotificationsServiceContract = PublicMethodsOf<NotificationsService>;
const createMock = () => {
const mocked: jest.Mocked<NotificationsServiceContract> = {
setup: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
};
mocked.setup.mockReturnValue(createSetupContractMock());
@ -41,4 +54,5 @@ const createMock = () => {
export const notificationServiceMock = {
create: createMock,
createSetupContract: createSetupContractMock,
createStartContract: createStartContractMock,
};

View file

@ -17,63 +17,73 @@
* under the License.
*/
import { Observable, Subject, Subscription } from 'rxjs';
import { I18nSetup } from '../i18n';
import { ToastsService } from './toasts';
import { i18n } from '@kbn/i18n';
interface NotificationServiceParams {
targetDomElement$: Observable<HTMLElement>;
import { Subscription } from 'rxjs';
import { I18nStart } from '../i18n';
import { ToastsService } from './toasts';
import { ToastsApi } from './toasts/toasts_api';
import { UiSettingsSetup } from '../ui_settings';
interface SetupDeps {
uiSettings: UiSettingsSetup;
}
interface NotificationsServiceDeps {
i18n: I18nSetup;
interface StartDeps {
i18n: I18nStart;
targetDomElement: HTMLElement;
}
/** @public */
export class NotificationsService {
private readonly toasts: ToastsService;
private readonly toastsContainer$: Subject<HTMLElement>;
private domElemSubscription?: Subscription;
private uiSettingsErrorSubscription?: Subscription;
private targetDomElement?: HTMLElement;
constructor(private readonly params: NotificationServiceParams) {
this.toastsContainer$ = new Subject<HTMLElement>();
this.toasts = new ToastsService({
targetDomElement$: this.toastsContainer$.asObservable(),
});
constructor() {
this.toasts = new ToastsService();
}
public setup({ i18n }: NotificationsServiceDeps) {
this.domElemSubscription = this.params.targetDomElement$.subscribe({
next: targetDomElement => {
this.cleanupTargetDomElement();
this.targetDomElement = targetDomElement;
public setup({ uiSettings }: SetupDeps): NotificationsSetup {
const notificationSetup = { toasts: this.toasts.setup() };
const toastsContainer = document.createElement('div');
targetDomElement.appendChild(toastsContainer);
this.toastsContainer$.next(toastsContainer);
},
this.uiSettingsErrorSubscription = uiSettings.getUpdateErrors$().subscribe(error => {
notificationSetup.toasts.addDanger({
title: i18n.translate('core.notifications.unableUpdateUISettingNotificationMessageTitle', {
defaultMessage: 'Unable to update UI setting',
}),
text: error.message,
});
});
return { toasts: this.toasts.setup({ i18n }) };
return notificationSetup;
}
public start({ i18n: i18nDep, targetDomElement }: StartDeps): NotificationsStart {
this.targetDomElement = targetDomElement;
const toastsContainer = document.createElement('div');
targetDomElement.appendChild(toastsContainer);
return { toasts: this.toasts.start({ i18n: i18nDep, targetDomElement: toastsContainer }) };
}
public stop() {
this.toasts.stop();
this.cleanupTargetDomElement();
if (this.domElemSubscription) {
this.domElemSubscription.unsubscribe();
}
}
private cleanupTargetDomElement() {
if (this.targetDomElement) {
this.targetDomElement.textContent = '';
}
if (this.uiSettingsErrorSubscription) {
this.uiSettingsErrorSubscription.unsubscribe();
}
}
}
/** @public */
export type NotificationsSetup = ReturnType<NotificationsService['setup']>;
export interface NotificationsSetup {
toasts: ToastsApi;
}
/** @public */
export type NotificationsStart = NotificationsSetup;

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`#setup() renders the GlobalToastList into the targetDomElement param 1`] = `
exports[`#start() renders the GlobalToastList into the targetDomElement param 1`] = `
Array [
Array [
<I18nContext>

View file

@ -18,5 +18,5 @@
*/
export { ToastsService } from './toasts_service';
export { ToastsSetup, ToastInput } from './toasts_start';
export { ToastsApi, ToastInput } from './toasts_api';
export { Toast } from '@elastic/eui';

View file

@ -19,9 +19,9 @@
import { take } from 'rxjs/operators';
import { ToastsSetup } from './toasts_start';
import { ToastsApi } from './toasts_api';
async function getCurrentToasts(toasts: ToastsSetup) {
async function getCurrentToasts(toasts: ToastsApi) {
return await toasts
.get$()
.pipe(take(1))
@ -30,7 +30,7 @@ async function getCurrentToasts(toasts: ToastsSetup) {
describe('#get$()', () => {
it('returns observable that emits NEW toast list when something added or removed', () => {
const toasts = new ToastsSetup();
const toasts = new ToastsApi();
const onToasts = jest.fn();
toasts.get$().subscribe(onToasts);
@ -57,7 +57,7 @@ describe('#get$()', () => {
});
it('does not emit a new toast list when unknown toast is passed to remove()', () => {
const toasts = new ToastsSetup();
const toasts = new ToastsApi();
const onToasts = jest.fn();
toasts.get$().subscribe(onToasts);
@ -71,14 +71,14 @@ describe('#get$()', () => {
describe('#add()', () => {
it('returns toast objects with auto assigned id', () => {
const toasts = new ToastsSetup();
const toasts = new ToastsApi();
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 ToastsSetup();
const toasts = new ToastsApi();
const toast = toasts.add({});
const currentToasts = await getCurrentToasts(toasts);
@ -87,27 +87,27 @@ describe('#add()', () => {
});
it('increments the toast ID for each additional toast', () => {
const toasts = new ToastsSetup();
const toasts = new ToastsApi();
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 ToastsSetup();
const toasts = new ToastsApi();
expect(toasts.add('foo')).toHaveProperty('title', 'foo');
});
});
describe('#remove()', () => {
it('removes a toast', async () => {
const toasts = new ToastsSetup();
const toasts = new ToastsApi();
toasts.remove(toasts.add('Test'));
expect(await getCurrentToasts(toasts)).toHaveLength(0);
});
it('ignores unknown toast', async () => {
const toasts = new ToastsSetup();
const toasts = new ToastsApi();
toasts.add('Test');
toasts.remove({ id: 'foo' });
@ -118,12 +118,12 @@ describe('#remove()', () => {
describe('#addSuccess()', () => {
it('adds a success toast', async () => {
const toasts = new ToastsSetup();
const toasts = new ToastsApi();
expect(toasts.addSuccess({})).toHaveProperty('color', 'success');
});
it('returns the created toast', async () => {
const toasts = new ToastsSetup();
const toasts = new ToastsApi();
const toast = toasts.addSuccess({});
const currentToasts = await getCurrentToasts(toasts);
expect(currentToasts[0]).toBe(toast);
@ -132,12 +132,12 @@ describe('#addSuccess()', () => {
describe('#addWarning()', () => {
it('adds a warning toast', async () => {
const toasts = new ToastsSetup();
const toasts = new ToastsApi();
expect(toasts.addWarning({})).toHaveProperty('color', 'warning');
});
it('returns the created toast', async () => {
const toasts = new ToastsSetup();
const toasts = new ToastsApi();
const toast = toasts.addWarning({});
const currentToasts = await getCurrentToasts(toasts);
expect(currentToasts[0]).toBe(toast);
@ -146,12 +146,12 @@ describe('#addWarning()', () => {
describe('#addDanger()', () => {
it('adds a danger toast', async () => {
const toasts = new ToastsSetup();
const toasts = new ToastsApi();
expect(toasts.addDanger({})).toHaveProperty('color', 'danger');
});
it('returns the created toast', async () => {
const toasts = new ToastsSetup();
const toasts = new ToastsApi();
const toast = toasts.addDanger({});
const currentToasts = await getCurrentToasts(toasts);
expect(currentToasts[0]).toBe(toast);

View file

@ -34,7 +34,7 @@ const normalizeToast = (toastOrTitle: ToastInput) => {
};
/** @public */
export class ToastsSetup {
export class ToastsApi {
private toasts$ = new Rx.BehaviorSubject<Toast[]>([]);
private idCounter = 0;

View file

@ -16,10 +16,10 @@
* specific language governing permissions and limitations
* under the License.
*/
import { ToastsSetup } from './toasts_start';
import { ToastsApi } from './toasts_api';
const createSetupContractMock = () => {
const setupContract: jest.Mocked<PublicMethodsOf<ToastsSetup>> = {
const createToastsApiMock = () => {
const api: jest.Mocked<PublicMethodsOf<ToastsApi>> = {
get$: jest.fn(),
add: jest.fn(),
remove: jest.fn(),
@ -27,9 +27,14 @@ const createSetupContractMock = () => {
addWarning: jest.fn(),
addDanger: jest.fn(),
};
return setupContract;
return api;
};
const createSetupContractMock = createToastsApiMock;
const createStartContractMock = createToastsApiMock;
export const toastsServiceMock = {
createSetupContract: createSetupContractMock,
createStartContract: createStartContractMock,
};

View file

@ -19,9 +19,8 @@
import { mockReactDomRender, mockReactDomUnmount } from './toasts_service.test.mocks';
import { of } from 'rxjs';
import { ToastsService } from './toasts_service';
import { ToastsSetup } from './toasts_start';
import { ToastsApi } from './toasts_api';
const mockI18n: any = {
Context: function I18nContext() {
@ -30,22 +29,31 @@ const mockI18n: any = {
};
describe('#setup()', () => {
it('returns a ToastsApi', () => {
const toasts = new ToastsService();
expect(toasts.setup()).toBeInstanceOf(ToastsApi);
});
});
describe('#start()', () => {
it('renders the GlobalToastList into the targetDomElement param', async () => {
const targetDomElement = document.createElement('div');
targetDomElement.setAttribute('test', 'target-dom-element');
const toasts = new ToastsService({ targetDomElement$: of(targetDomElement) });
const toasts = new ToastsService();
expect(mockReactDomRender).not.toHaveBeenCalled();
toasts.setup({ i18n: mockI18n });
toasts.setup();
toasts.start({ i18n: mockI18n, targetDomElement });
expect(mockReactDomRender.mock.calls).toMatchSnapshot();
});
it('returns a ToastsSetup', () => {
const toasts = new ToastsService({
targetDomElement$: of(document.createElement('div')),
});
it('returns a ToastsApi', () => {
const targetDomElement = document.createElement('div');
const toasts = new ToastsService();
expect(toasts.setup({ i18n: mockI18n })).toBeInstanceOf(ToastsSetup);
toasts.setup();
expect(toasts.start({ i18n: mockI18n, targetDomElement })).toBeInstanceOf(ToastsApi);
});
});
@ -53,9 +61,10 @@ describe('#stop()', () => {
it('unmounts the GlobalToastList from the targetDomElement', () => {
const targetDomElement = document.createElement('div');
targetDomElement.setAttribute('test', 'target-dom-element');
const toasts = new ToastsService({ targetDomElement$: of(targetDomElement) });
const toasts = new ToastsService();
toasts.setup({ i18n: mockI18n });
toasts.setup();
toasts.start({ i18n: mockI18n, targetDomElement });
expect(mockReactDomUnmount).not.toHaveBeenCalled();
toasts.stop();
@ -63,9 +72,7 @@ describe('#stop()', () => {
});
it('does not fail if setup() was never called', () => {
const targetDomElement = document.createElement('div');
targetDomElement.setAttribute('test', 'target-dom-element');
const toasts = new ToastsService({ targetDomElement$: of(targetDomElement) });
const toasts = new ToastsService();
expect(() => {
toasts.stop();
}).not.toThrowError();
@ -73,9 +80,10 @@ describe('#stop()', () => {
it('empties the content of the targetDomElement', () => {
const targetDomElement = document.createElement('div');
const toasts = new ToastsService({ targetDomElement$: of(targetDomElement) });
const toasts = new ToastsService();
toasts.setup({ i18n: mockI18n });
toasts.setup();
toasts.start({ i18n: mockI18n, targetDomElement });
toasts.stop();
expect(targetDomElement.childNodes).toHaveLength(0);
});

View file

@ -19,59 +19,43 @@
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { Observable, Subscription } from 'rxjs';
import { Toast } from '@elastic/eui';
import { I18nSetup } from '../../i18n';
import { GlobalToastList } from './global_toast_list';
import { ToastsSetup } from './toasts_start';
import { ToastsApi } from './toasts_api';
interface Params {
targetDomElement$: Observable<HTMLElement>;
}
interface Deps {
interface StartDeps {
i18n: I18nSetup;
targetDomElement: HTMLElement;
}
export class ToastsService {
private domElemSubscription?: Subscription;
private api?: ToastsApi;
private targetDomElement?: HTMLElement;
constructor(private readonly params: Params) {}
public setup() {
this.api = new ToastsApi();
return this.api!;
}
public setup({ i18n }: Deps) {
const toasts = new ToastsSetup();
public start({ i18n, targetDomElement }: StartDeps) {
this.targetDomElement = targetDomElement;
this.domElemSubscription = this.params.targetDomElement$.subscribe({
next: targetDomElement => {
this.cleanupTargetDomElement();
this.targetDomElement = targetDomElement;
render(
<i18n.Context>
<GlobalToastList
dismissToast={(toast: Toast) => this.api!.remove(toast)}
toasts$={this.api!.get$()}
/>
</i18n.Context>,
targetDomElement
);
render(
<i18n.Context>
<GlobalToastList
dismissToast={(toast: Toast) => toasts.remove(toast)}
toasts$={toasts.get$()}
/>
</i18n.Context>,
targetDomElement
);
},
});
return toasts;
return this.api!;
}
public stop() {
this.cleanupTargetDomElement();
if (this.domElemSubscription) {
this.domElemSubscription.unsubscribe();
}
}
private cleanupTargetDomElement() {
if (this.targetDomElement) {
unmountComponentAtNode(this.targetDomElement);
this.targetDomElement.textContent = '';

View file

@ -17,5 +17,5 @@
* under the License.
*/
export { OverlayService, OverlaySetup } from './overlay_service';
export { OverlayService, OverlayStart } from './overlay_service';
export { FlyoutRef } from './flyout';

View file

@ -16,24 +16,24 @@
* specific language governing permissions and limitations
* under the License.
*/
import { OverlayService, OverlaySetup } from './overlay_service';
import { OverlayService, OverlayStart } from './overlay_service';
const createSetupContractMock = () => {
const setupContract: jest.Mocked<PublicMethodsOf<OverlaySetup>> = {
const createStartContractMock = () => {
const startContract: jest.Mocked<PublicMethodsOf<OverlayStart>> = {
openFlyout: jest.fn(),
};
return setupContract;
return startContract;
};
const createMock = () => {
const mocked: jest.Mocked<PublicMethodsOf<OverlayService>> = {
setup: jest.fn(),
start: jest.fn(),
};
mocked.setup.mockReturnValue(createSetupContractMock());
mocked.start.mockReturnValue(createStartContractMock());
return mocked;
};
export const overlayServiceMock = {
create: createMock,
createSetupContract: createSetupContractMock,
createStartContract: createStartContractMock,
};

View file

@ -20,10 +20,10 @@
import { FlyoutService } from './flyout';
import { FlyoutRef } from '..';
import { I18nSetup } from '../i18n';
import { I18nStart } from '../i18n';
interface Deps {
i18n: I18nSetup;
interface StartDeps {
i18n: I18nStart;
}
/** @internal */
@ -34,7 +34,7 @@ export class OverlayService {
this.flyoutService = new FlyoutService(targetDomElement);
}
public setup({ i18n }: Deps): OverlaySetup {
public start({ i18n }: StartDeps): OverlayStart {
return {
openFlyout: this.flyoutService.openFlyout.bind(this.flyoutService, i18n),
};
@ -42,7 +42,7 @@ export class OverlayService {
}
/** @public */
export interface OverlaySetup {
export interface OverlayStart {
openFlyout: (
flyoutChildren: React.ReactNode,
flyoutProps?: {

View file

@ -19,6 +19,7 @@
export const mockPlugin = {
setup: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
};
export const mockInitializer = jest.fn(() => mockPlugin);

View file

@ -41,6 +41,7 @@ const addBasePath = (path: string) => path;
beforeEach(() => {
mockPluginLoader.mockClear();
mockPlugin.setup.mockClear();
mockPlugin.start.mockClear();
mockPlugin.stop.mockClear();
plugin = new PluginWrapper(createManifest('plugin-a'), initializerContext);
});
@ -58,13 +59,21 @@ describe('PluginWrapper', () => {
});
test('`setup` fails if plugin.setup is not a function', async () => {
mockInitializer.mockReturnValueOnce({ stop: jest.fn() } as any);
mockInitializer.mockReturnValueOnce({ start: jest.fn() } as any);
await plugin.load(addBasePath);
await expect(plugin.setup({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot(
`"Instance of plugin \\"plugin-a\\" does not define \\"setup\\" function."`
);
});
test('`setup` fails if plugin.start is not a function', async () => {
mockInitializer.mockReturnValueOnce({ setup: jest.fn() } as any);
await plugin.load(addBasePath);
await expect(plugin.setup({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot(
`"Instance of plugin \\"plugin-a\\" does not define \\"start\\" function."`
);
});
test('`setup` calls initializer with initializer context', async () => {
await plugin.load(addBasePath);
await plugin.setup({} as any, {} as any);
@ -79,6 +88,22 @@ describe('PluginWrapper', () => {
expect(mockPlugin.setup).toHaveBeenCalledWith(context, deps);
});
test('`start` fails if setup is not called first', async () => {
await plugin.load(addBasePath);
await expect(plugin.start({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot(
`"Plugin \\"plugin-a\\" can't be started since it isn't set up."`
);
});
test('`start` calls plugin.start with context and dependencies', async () => {
await plugin.load(addBasePath);
await plugin.setup({} as any, {} as any);
const context = { any: 'thing' } as any;
const deps = { otherDep: 'value' };
await plugin.start(context, deps);
expect(mockPlugin.start).toHaveBeenCalledWith(context, deps);
});
test('`stop` fails if plugin is not setup up', async () => {
expect(() => plugin.stop()).toThrowErrorMatchingInlineSnapshot(
`"Plugin \\"plugin-a\\" can't be stopped since it isn't set up."`
@ -93,7 +118,7 @@ describe('PluginWrapper', () => {
});
test('`stop` does not fail if plugin.stop does not exist', async () => {
mockInitializer.mockReturnValueOnce({ setup: jest.fn() } as any);
mockInitializer.mockReturnValueOnce({ setup: jest.fn(), start: jest.fn() } as any);
await plugin.load(addBasePath);
await plugin.setup({} as any, {} as any);
expect(() => plugin.stop()).not.toThrow();

View file

@ -18,7 +18,7 @@
*/
import { DiscoveredPlugin, PluginName } from '../../server';
import { PluginInitializerContext, PluginSetupContext } from './plugin_context';
import { PluginInitializerContext, PluginSetupContext, PluginStartContext } from './plugin_context';
import { loadPluginBundle } from './plugin_loader';
/**
@ -26,8 +26,14 @@ import { loadPluginBundle } from './plugin_loader';
*
* @public
*/
export interface Plugin<TSetup, TPluginsSetup extends Record<string, unknown> = {}> {
export interface Plugin<
TSetup,
TStart,
TPluginsSetup extends Record<string, unknown> = {},
TPluginsStart extends Record<string, unknown> = {}
> {
setup: (core: PluginSetupContext, plugins: TPluginsSetup) => TSetup | Promise<TSetup>;
start: (core: PluginStartContext, plugins: TPluginsStart) => TStart | Promise<TStart>;
stop?: () => void;
}
@ -37,9 +43,12 @@ export interface Plugin<TSetup, TPluginsSetup extends Record<string, unknown> =
*
* @public
*/
export type PluginInitializer<TSetup, TPluginsSetup extends Record<string, unknown> = {}> = (
core: PluginInitializerContext
) => Plugin<TSetup, TPluginsSetup>;
export type PluginInitializer<
TSetup,
TStart,
TPluginsSetup extends Record<string, unknown> = {},
TPluginsStart extends Record<string, unknown> = {}
> = (core: PluginInitializerContext) => Plugin<TSetup, TStart, TPluginsSetup, TPluginsStart>;
/**
* Lightweight wrapper around discovered plugin that is responsible for instantiating
@ -49,14 +58,16 @@ export type PluginInitializer<TSetup, TPluginsSetup extends Record<string, unkno
*/
export class PluginWrapper<
TSetup = unknown,
TPluginsSetup extends Record<PluginName, unknown> = Record<PluginName, unknown>
TStart = unknown,
TPluginsSetup extends Record<PluginName, unknown> = Record<PluginName, unknown>,
TPluginsStart extends Record<PluginName, unknown> = Record<PluginName, unknown>
> {
public readonly name: DiscoveredPlugin['id'];
public readonly configPath: DiscoveredPlugin['configPath'];
public readonly requiredPlugins: DiscoveredPlugin['requiredPlugins'];
public readonly optionalPlugins: DiscoveredPlugin['optionalPlugins'];
private initializer?: PluginInitializer<TSetup, TPluginsSetup>;
private instance?: Plugin<TSetup, TPluginsSetup>;
private initializer?: PluginInitializer<TSetup, TStart, TPluginsSetup, TPluginsStart>;
private instance?: Plugin<TSetup, TStart, TPluginsSetup, TPluginsStart>;
constructor(
readonly discoveredPlugin: DiscoveredPlugin,
@ -74,7 +85,10 @@ export class PluginWrapper<
* @param addBasePath Function that adds the base path to a string for plugin bundle path.
*/
public async load(addBasePath: (path: string) => string) {
this.initializer = await loadPluginBundle<TSetup, TPluginsSetup>(addBasePath, this.name);
this.initializer = await loadPluginBundle<TSetup, TStart, TPluginsSetup, TPluginsStart>(
addBasePath,
this.name
);
}
/**
@ -90,6 +104,21 @@ export class PluginWrapper<
return await this.instance.setup(setupContext, plugins);
}
/**
* Calls `setup` function exposed by the initialized plugin.
* @param startContext Context that consists of various core services tailored specifically
* for the `start` lifecycle event.
* @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) {
if (this.instance === undefined) {
throw new Error(`Plugin "${this.name}" can't be started since it isn't set up.`);
}
return await this.instance.start(startContext, plugins);
}
/**
* Calls optional `stop` function exposed by the plugin initializer.
*/
@ -114,6 +143,8 @@ export class PluginWrapper<
if (typeof instance.setup !== 'function') {
throw new Error(`Instance of plugin "${this.name}" does not define "setup" function.`);
} else if (typeof instance.start !== 'function') {
throw new Error(`Instance of plugin "${this.name}" does not define "start" function.`);
}
return instance;

View file

@ -22,11 +22,13 @@ import { BasePathSetup } from '../base_path';
import { ChromeSetup } from '../chrome';
import { CoreContext } from '../core_system';
import { FatalErrorsSetup } from '../fatal_errors';
import { I18nSetup } from '../i18n';
import { NotificationsSetup } from '../notifications';
import { I18nSetup, I18nStart } from '../i18n';
import { NotificationsSetup, NotificationsStart } from '../notifications';
import { UiSettingsSetup } from '../ui_settings';
import { PluginWrapper } from './plugin';
import { PluginsServiceSetupDeps } from './plugins_service';
import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service';
import { CapabilitiesStart } from '../capabilities';
import { OverlayStart } from '../overlays';
/**
* The available core services passed to a `PluginInitializer`
@ -50,6 +52,18 @@ export interface PluginSetupContext {
uiSettings: UiSettingsSetup;
}
/**
* The available core services passed to a plugin's `Plugin#start` method.
*
* @public
*/
export interface PluginStartContext {
capabilities: CapabilitiesStart;
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.
@ -75,10 +89,10 @@ export function createPluginInitializerContext(
* @param plugin
* @internal
*/
export function createPluginSetupContext<TPlugin, TPluginDependencies>(
export function createPluginSetupContext<TSetup, TStart, TPluginsSetup, TPluginsStart>(
coreContext: CoreContext,
deps: PluginsServiceSetupDeps,
plugin: PluginWrapper<TPlugin, TPluginDependencies>
plugin: PluginWrapper<TSetup, TStart, TPluginsSetup, TPluginsStart>
): PluginSetupContext {
return {
basePath: deps.basePath,
@ -89,3 +103,26 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>(
uiSettings: deps.uiSettings,
};
}
/**
* Provides a plugin-specific context passed to the plugin's `start` lifecycle event. Currently
* this returns a shallow copy the service start contracts, but in the future could provide
* plugin-scoped versions of the service.
*
* @param coreContext
* @param deps
* @param plugin
* @internal
*/
export function createPluginStartContext<TSetup, TStart, TPluginsSetup, TPluginsStart>(
coreContext: CoreContext,
deps: PluginsServiceStartDeps,
plugin: PluginWrapper<TSetup, TStart, TPluginsSetup, TPluginsStart>
): PluginStartContext {
return {
capabilities: deps.capabilities,
i18n: deps.i18n,
notifications: deps.notifications,
overlays: deps.overlays,
};
}

View file

@ -61,65 +61,74 @@ export const LOAD_TIMEOUT = 120 * 1000; // 2 minutes
*/
export const loadPluginBundle: LoadPluginBundle = <
TSetup,
TDependencies extends Record<string, unknown>
TStart,
TPluginsSetup extends Record<string, unknown>,
TPluginsStart extends Record<string, unknown>
>(
addBasePath: (path: string) => string,
pluginName: PluginName,
{ timeoutMs = LOAD_TIMEOUT } = {}
{ timeoutMs = LOAD_TIMEOUT }: { timeoutMs?: number } = {}
) =>
new Promise<PluginInitializer<TSetup, TDependencies>>((resolve, reject) => {
const script = document.createElement('script');
const coreWindow = (window as unknown) as CoreWindow;
new Promise<PluginInitializer<TSetup, TStart, TPluginsSetup, TPluginsStart>>(
(resolve, reject) => {
const script = document.createElement('script');
const coreWindow = (window as unknown) as CoreWindow;
// Assumes that all plugin bundles get put into the bundles/plugins subdirectory
const bundlePath = addBasePath(`/bundles/plugin/${pluginName}.bundle.js`);
script.setAttribute('src', bundlePath);
script.setAttribute('id', `kbn-plugin-${pluginName}`);
script.setAttribute('async', '');
// Assumes that all plugin bundles get put into the bundles/plugins subdirectory
const bundlePath = addBasePath(`/bundles/plugin/${pluginName}.bundle.js`);
script.setAttribute('src', bundlePath);
script.setAttribute('id', `kbn-plugin-${pluginName}`);
script.setAttribute('async', '');
// Add kbnNonce for CSP
script.setAttribute('nonce', coreWindow.__kbnNonce__);
// Add kbnNonce for CSP
script.setAttribute('nonce', coreWindow.__kbnNonce__);
const cleanupTag = () => {
clearTimeout(timeout);
// Set to null for IE memory leak issue. Webpack does the same thing.
// @ts-ignore
script.onload = script.onerror = null;
};
const cleanupTag = () => {
clearTimeout(timeout);
// Set to null for IE memory leak issue. Webpack does the same thing.
// @ts-ignore
script.onload = script.onerror = null;
};
// Wire up resolve and reject
script.onload = () => {
cleanupTag();
// Wire up resolve and reject
script.onload = () => {
cleanupTag();
const initializer = coreWindow.__kbnBundles__[`plugin/${pluginName}`];
if (!initializer || typeof initializer !== 'function') {
reject(
new Error(`Definition of plugin "${pluginName}" should be a function (${bundlePath}).`)
);
} else {
resolve(initializer as PluginInitializer<TSetup, TDependencies>);
}
};
const initializer = coreWindow.__kbnBundles__[`plugin/${pluginName}`];
if (!initializer || typeof initializer !== 'function') {
reject(
new Error(`Definition of plugin "${pluginName}" should be a function (${bundlePath}).`)
);
} else {
resolve(initializer as PluginInitializer<TSetup, TStart, TPluginsSetup, TPluginsStart>);
}
};
script.onerror = () => {
cleanupTag();
reject(new Error(`Failed to load "${pluginName}" bundle (${bundlePath})`));
};
script.onerror = () => {
cleanupTag();
reject(new Error(`Failed to load "${pluginName}" bundle (${bundlePath})`));
};
const timeout = setTimeout(() => {
cleanupTag();
reject(new Error(`Timeout reached when loading "${pluginName}" bundle (${bundlePath})`));
}, timeoutMs);
const timeout = setTimeout(() => {
cleanupTag();
reject(new Error(`Timeout reached when loading "${pluginName}" bundle (${bundlePath})`));
}, timeoutMs);
// Add the script tag to the end of the body to start downloading
document.body.appendChild(script);
});
// Add the script tag to the end of the body to start downloading
document.body.appendChild(script);
}
);
/**
* @internal
*/
export type LoadPluginBundle = <TSetup, TDependencies extends Record<string, unknown>>(
export type LoadPluginBundle = <
TSetup,
TStart,
TPluginsSetup extends Record<string, unknown>,
TPluginsStart extends Record<string, unknown>
>(
addBasePath: (path: string) => string,
pluginName: PluginName,
options?: { timeoutMs?: number }
) => Promise<PluginInitializer<TSetup, TDependencies>>;
) => Promise<PluginInitializer<TSetup, TStart, TPluginsSetup, TPluginsStart>>;

View file

@ -20,25 +20,36 @@
import { PluginsService, PluginsServiceSetup } from './plugins_service';
const createSetupContractMock = () => {
const setupContract: jest.Mocked<PublicMethodsOf<PluginsServiceSetup>> = {
pluginSetups: new Map(),
const setupContract: jest.Mocked<PluginsServiceSetup> = {
contracts: new Map(),
};
// we have to suppress type errors until decide how to mock es6 class
return (setupContract as unknown) as PluginsServiceSetup;
return setupContract as PluginsServiceSetup;
};
const createStartContractMock = () => {
const startContract: jest.Mocked<PluginsServiceSetup> = {
contracts: new Map(),
};
// we have to suppress type errors until decide how to mock es6 class
return startContract as PluginsServiceSetup;
};
type PluginsServiceContract = PublicMethodsOf<PluginsService>;
const createMock = () => {
const mocked: jest.Mocked<PluginsServiceContract> = {
setup: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
};
mocked.setup.mockResolvedValue(createSetupContractMock());
mocked.start.mockResolvedValue(createStartContractMock());
return mocked;
};
export const pluginsServiceMock = {
create: createMock,
createSetupContract: createSetupContractMock,
createStartContract: createStartContractMock,
};

View file

@ -27,7 +27,21 @@ import {
import { PluginName } from 'src/core/server';
import { CoreContext } from '../core_system';
import { PluginsService } from './plugins_service';
import {
PluginsService,
PluginsServiceStartDeps,
PluginsServiceSetupDeps,
} from './plugins_service';
import { notificationServiceMock } from '../notifications/notifications_service.mock';
import { capabilitiesServiceMock } from '../capabilities/capabilities_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';
export let mockPluginInitializers: Map<PluginName, MockedPluginInitializer>;
@ -35,41 +49,56 @@ mockPluginInitializerProvider.mockImplementation(
pluginName => mockPluginInitializers.get(pluginName)!
);
type DeeplyMocked<T> = { [P in keyof T]: jest.Mocked<T[P]> };
const mockCoreContext: CoreContext = {};
let mockDeps: any;
let mockInitContext: any;
let mockSetupDeps: DeeplyMocked<PluginsServiceSetupDeps>;
let mockSetupContext: DeeplyMocked<PluginSetupContext>;
let mockStartDeps: DeeplyMocked<PluginsServiceStartDeps>;
let mockStartContext: DeeplyMocked<PluginStartContext>;
beforeEach(() => {
mockDeps = {
injectedMetadata: {
getPlugins: jest.fn(() => [
mockSetupDeps = {
injectedMetadata: (function() {
const metadata = injectedMetadataServiceMock.createSetupContract();
metadata.getPlugins.mockReturnValue([
{ id: 'pluginA', plugin: createManifest('pluginA') },
{ id: 'pluginB', plugin: createManifest('pluginB', { required: ['pluginA'] }) },
{
id: 'pluginC',
plugin: createManifest('pluginC', { required: ['pluginA'], optional: ['nonexist'] }),
},
]),
},
basePath: {
addToPath(path: string) {
return path;
},
},
chrome: {},
fatalErrors: {},
i18n: {},
notifications: {},
uiSettings: {},
]);
return metadata;
})(),
basePath: (function() {
const basePath = basePathServiceMock.createSetupContract();
basePath.addToPath.mockImplementation(path => path);
return basePath;
})(),
chrome: chromeServiceMock.createSetupContract(),
fatalErrors: fatalErrorsServiceMock.createSetupContract(),
i18n: i18nServiceMock.createSetupContract(),
notifications: notificationServiceMock.createSetupContract(),
uiSettings: uiSettingsServiceMock.createSetupContract(),
} as any;
mockInitContext = omit(mockDeps, 'injectedMetadata');
mockSetupContext = omit(mockSetupDeps, 'injectedMetadata');
mockStartDeps = {
capabilities: capabilitiesServiceMock.createStartContract(),
i18n: i18nServiceMock.createStartContract(),
injectedMetadata: injectedMetadataServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
overlays: overlayServiceMock.createStartContract(),
};
mockStartContext = omit(mockStartDeps, 'injectedMetadata');
// Reset these for each test.
mockPluginInitializers = new Map<PluginName, MockedPluginInitializer>(([
[
'pluginA',
jest.fn(() => ({
setup: jest.fn(() => ({ exportedValue: 1 })),
setup: jest.fn(() => ({ setupValue: 1 })),
start: jest.fn(() => ({ startValue: 2 })),
stop: jest.fn(),
})),
],
@ -77,7 +106,10 @@ beforeEach(() => {
'pluginB',
jest.fn(() => ({
setup: jest.fn((core, deps: any) => ({
pluginAPlusB: deps.pluginA.exportedValue + 1,
pluginAPlusB: deps.pluginA.setupValue + 1,
})),
start: jest.fn((core, deps: any) => ({
pluginAPlusB: deps.pluginA.startValue + 1,
})),
stop: jest.fn(),
})),
@ -86,6 +118,7 @@ beforeEach(() => {
'pluginC',
jest.fn(() => ({
setup: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
})),
],
@ -113,7 +146,7 @@ test('`PluginsService.setup` fails if any bundle cannot be loaded', async () =>
mockLoadPluginBundle.mockRejectedValueOnce(new Error('Could not load bundle'));
const pluginsService = new PluginsService(mockCoreContext);
await expect(pluginsService.setup(mockDeps)).rejects.toThrowErrorMatchingInlineSnapshot(
await expect(pluginsService.setup(mockSetupDeps)).rejects.toThrowErrorMatchingInlineSnapshot(
`"Could not load bundle"`
);
});
@ -121,24 +154,24 @@ test('`PluginsService.setup` fails if any bundle cannot be loaded', async () =>
test('`PluginsService.setup` fails if any plugin instance does not have a setup function', async () => {
mockPluginInitializers.set('pluginA', (() => ({})) as any);
const pluginsService = new PluginsService(mockCoreContext);
await expect(pluginsService.setup(mockDeps)).rejects.toThrowErrorMatchingInlineSnapshot(
await expect(pluginsService.setup(mockSetupDeps)).rejects.toThrowErrorMatchingInlineSnapshot(
`"Instance of plugin \\"pluginA\\" does not define \\"setup\\" function."`
);
});
test('`PluginsService.setup` calls loadPluginBundles with basePath and plugins', async () => {
const pluginsService = new PluginsService(mockCoreContext);
await pluginsService.setup(mockDeps);
await pluginsService.setup(mockSetupDeps);
expect(mockLoadPluginBundle).toHaveBeenCalledTimes(3);
expect(mockLoadPluginBundle).toHaveBeenCalledWith(mockDeps.basePath.addToPath, 'pluginA');
expect(mockLoadPluginBundle).toHaveBeenCalledWith(mockDeps.basePath.addToPath, 'pluginB');
expect(mockLoadPluginBundle).toHaveBeenCalledWith(mockDeps.basePath.addToPath, 'pluginC');
expect(mockLoadPluginBundle).toHaveBeenCalledWith(mockSetupDeps.basePath.addToPath, 'pluginA');
expect(mockLoadPluginBundle).toHaveBeenCalledWith(mockSetupDeps.basePath.addToPath, 'pluginB');
expect(mockLoadPluginBundle).toHaveBeenCalledWith(mockSetupDeps.basePath.addToPath, 'pluginC');
});
test('`PluginsService.setup` initalizes plugins with CoreContext', async () => {
const pluginsService = new PluginsService(mockCoreContext);
await pluginsService.setup(mockDeps);
await pluginsService.setup(mockSetupDeps);
expect(mockPluginInitializers.get('pluginA')).toHaveBeenCalledWith(mockCoreContext);
expect(mockPluginInitializers.get('pluginB')).toHaveBeenCalledWith(mockCoreContext);
@ -147,50 +180,102 @@ test('`PluginsService.setup` initalizes plugins with CoreContext', async () => {
test('`PluginsService.setup` exposes dependent setup contracts to plugins', async () => {
const pluginsService = new PluginsService(mockCoreContext);
await pluginsService.setup(mockDeps);
await pluginsService.setup(mockSetupDeps);
const pluginAInstance = mockPluginInitializers.get('pluginA')!.mock.results[0].value;
const pluginBInstance = mockPluginInitializers.get('pluginB')!.mock.results[0].value;
const pluginCInstance = mockPluginInitializers.get('pluginC')!.mock.results[0].value;
expect(pluginAInstance.setup).toHaveBeenCalledWith(mockInitContext, {});
expect(pluginBInstance.setup).toHaveBeenCalledWith(mockInitContext, {
pluginA: { exportedValue: 1 },
expect(pluginAInstance.setup).toHaveBeenCalledWith(mockSetupContext, {});
expect(pluginBInstance.setup).toHaveBeenCalledWith(mockSetupContext, {
pluginA: { setupValue: 1 },
});
// Does not supply value for `nonexist` optional dep
expect(pluginCInstance.setup).toHaveBeenCalledWith(mockInitContext, {
pluginA: { exportedValue: 1 },
expect(pluginCInstance.setup).toHaveBeenCalledWith(mockSetupContext, {
pluginA: { setupValue: 1 },
});
});
test('`PluginsService.setup` does not set missing dependent setup contracts', async () => {
mockDeps.injectedMetadata.getPlugins.mockReturnValue([
mockSetupDeps.injectedMetadata.getPlugins.mockReturnValue([
{ id: 'pluginD', plugin: createManifest('pluginD', { required: ['missing'] }) },
]);
mockPluginInitializers.set('pluginD', jest.fn(() => ({ setup: jest.fn() })) as any);
mockPluginInitializers.set('pluginD', jest.fn(() => ({
setup: jest.fn(),
start: jest.fn(),
})) as any);
const pluginsService = new PluginsService(mockCoreContext);
await pluginsService.setup(mockDeps);
await pluginsService.setup(mockSetupDeps);
// If a dependency is missing it should not be in the deps at all, not even as undefined.
const pluginDInstance = mockPluginInitializers.get('pluginD')!.mock.results[0].value;
expect(pluginDInstance.setup).toHaveBeenCalledWith(mockInitContext, {});
expect(pluginDInstance.setup).toHaveBeenCalledWith(mockSetupContext, {});
const pluginDDeps = pluginDInstance.setup.mock.calls[0][1];
expect(pluginDDeps).not.toHaveProperty('missing');
});
test('`PluginsService.setup` returns plugin setup contracts', async () => {
const pluginsService = new PluginsService(mockCoreContext);
const { contracts } = await pluginsService.setup(mockDeps);
const { contracts } = await pluginsService.setup(mockSetupDeps);
// Verify that plugin contracts were available
expect((contracts.get('pluginA')! as any).exportedValue).toEqual(1);
expect((contracts.get('pluginA')! as any).setupValue).toEqual(1);
expect((contracts.get('pluginB')! as any).pluginAPlusB).toEqual(2);
});
test('`PluginsService.start` exposes dependent start contracts to plugins', async () => {
const pluginsService = new PluginsService(mockCoreContext);
await pluginsService.setup(mockSetupDeps);
await pluginsService.start(mockStartDeps);
const pluginAInstance = mockPluginInitializers.get('pluginA')!.mock.results[0].value;
const pluginBInstance = mockPluginInitializers.get('pluginB')!.mock.results[0].value;
const pluginCInstance = mockPluginInitializers.get('pluginC')!.mock.results[0].value;
expect(pluginAInstance.start).toHaveBeenCalledWith(mockStartContext, {});
expect(pluginBInstance.start).toHaveBeenCalledWith(mockStartContext, {
pluginA: { startValue: 2 },
});
// Does not supply value for `nonexist` optional dep
expect(pluginCInstance.start).toHaveBeenCalledWith(mockStartContext, {
pluginA: { startValue: 2 },
});
});
test('`PluginsService.start` does not set missing dependent start contracts', async () => {
mockSetupDeps.injectedMetadata.getPlugins.mockReturnValue([
{ id: 'pluginD', plugin: createManifest('pluginD', { required: ['missing'] }) },
]);
mockPluginInitializers.set('pluginD', jest.fn(() => ({
setup: jest.fn(),
start: jest.fn(),
})) as any);
const pluginsService = new PluginsService(mockCoreContext);
await pluginsService.setup(mockSetupDeps);
await pluginsService.start(mockStartDeps);
// If a dependency is missing it should not be in the deps at all, not even as undefined.
const pluginDInstance = mockPluginInitializers.get('pluginD')!.mock.results[0].value;
expect(pluginDInstance.start).toHaveBeenCalledWith(mockStartContext, {});
const pluginDDeps = pluginDInstance.start.mock.calls[0][1];
expect(pluginDDeps).not.toHaveProperty('missing');
});
test('`PluginsService.start` returns plugin start contracts', async () => {
const pluginsService = new PluginsService(mockCoreContext);
await pluginsService.setup(mockSetupDeps);
const { contracts } = await pluginsService.start(mockStartDeps);
// Verify that plugin contracts were available
expect((contracts.get('pluginA')! as any).startValue).toEqual(2);
expect((contracts.get('pluginB')! as any).pluginAPlusB).toEqual(3);
});
test('`PluginService.stop` calls the stop function on each plugin', async () => {
const pluginsService = new PluginsService(mockCoreContext);
await pluginsService.setup(mockDeps);
await pluginsService.setup(mockSetupDeps);
const pluginAInstance = mockPluginInitializers.get('pluginA')!.mock.results[0].value;
const pluginBInstance = mockPluginInitializers.get('pluginB')!.mock.results[0].value;

View file

@ -17,15 +17,21 @@
* under the License.
*/
import { CoreSetup } from '..';
import { CoreSetup, CoreStart } from '..';
import { PluginName } from '../../server';
import { CoreService } from '../../types';
import { CoreContext } from '../core_system';
import { PluginWrapper } from './plugin';
import { createPluginInitializerContext, createPluginSetupContext } from './plugin_context';
import {
createPluginInitializerContext,
createPluginSetupContext,
createPluginStartContext,
} from './plugin_context';
/** @internal */
export type PluginsServiceSetupDeps = CoreSetup;
/** @internal */
export type PluginsServiceStartDeps = CoreStart;
/** @internal */
export interface PluginsServiceSetup {
@ -98,6 +104,41 @@ export class PluginsService implements CoreService<PluginsServiceSetup> {
return { contracts };
}
public async start(deps: PluginsServiceStartDeps) {
// Setup each plugin with required and optional plugin contracts
const contracts = new Map<string, unknown>();
for (const [pluginName, plugin] of this.plugins.entries()) {
const pluginDeps = new Set([
...plugin.requiredPlugins,
...plugin.optionalPlugins.filter(optPlugin => this.plugins.get(optPlugin)),
]);
const pluginDepContracts = [...pluginDeps.keys()].reduce(
(depContracts, dependencyName) => {
// Only set if present. Could be absent if plugin does not have client-side code or is a
// missing optional plugin.
if (contracts.has(dependencyName)) {
depContracts[dependencyName] = contracts.get(dependencyName);
}
return depContracts;
},
{} as Record<PluginName, unknown>
);
contracts.set(
pluginName,
await plugin.start(
createPluginStartContext(this.coreContext, deps, plugin),
pluginDepContracts
)
);
}
// Expose start contracts
return { contracts };
}
public async stop() {
// Stop plugins in reverse topological order.
for (const pluginName of this.satupPlugins.reverse()) {

View file

@ -6,7 +6,6 @@
import * as CSS from 'csstype';
import { default } from 'react';
import { Observable } from 'rxjs';
import * as PropTypes from 'prop-types';
import * as Rx from 'rxjs';
import { Toast } from '@elastic/eui';
@ -29,7 +28,7 @@ export interface Capabilities {
}
// @public
export interface CapabilitiesSetup {
export interface CapabilitiesStart {
getCapabilities: () => Capabilities;
}
@ -66,8 +65,6 @@ export interface CoreSetup {
// (undocumented)
basePath: BasePathSetup;
// (undocumented)
capabilities: CapabilitiesSetup;
// (undocumented)
chrome: ChromeSetup;
// (undocumented)
fatalErrors: FatalErrorsSetup;
@ -80,11 +77,23 @@ export interface CoreSetup {
// (undocumented)
notifications: NotificationsSetup;
// (undocumented)
overlays: OverlaySetup;
// (undocumented)
uiSettings: UiSettingsSetup;
}
// @public (undocumented)
export interface CoreStart {
// (undocumented)
capabilities: CapabilitiesStart;
// (undocumented)
i18n: I18nStart;
// (undocumented)
injectedMetadata: InjectedMetadataStart;
// (undocumented)
notifications: NotificationsStart;
// (undocumented)
overlays: OverlayStart;
}
// @internal
export class CoreSystem {
constructor(params: Params);
@ -93,6 +102,8 @@ export class CoreSystem {
fatalErrors: import(".").FatalErrorsSetup;
} | undefined>;
// (undocumented)
start(): Promise<void>;
// (undocumented)
stop(): void;
}
@ -119,6 +130,9 @@ export interface I18nSetup {
}) => JSX.Element;
}
// @public (undocumented)
export type I18nStart = I18nSetup;
// @internal (undocumented)
export interface InjectedMetadataParams {
// (undocumented)
@ -197,10 +211,19 @@ export interface InjectedMetadataSetup {
}
// @public (undocumented)
export type NotificationsSetup = ReturnType<NotificationsService['setup']>;
export type InjectedMetadataStart = InjectedMetadataSetup;
// @public (undocumented)
export interface OverlaySetup {
export interface NotificationsSetup {
// (undocumented)
toasts: ToastsApi;
}
// @public (undocumented)
export type NotificationsStart = NotificationsSetup;
// @public (undocumented)
export interface OverlayStart {
// (undocumented)
openFlyout: (flyoutChildren: React.ReactNode, flyoutProps?: {
closeButtonAriaLabel?: string;
@ -209,15 +232,17 @@ export interface OverlaySetup {
}
// @public
export interface Plugin<TSetup, TPluginsSetup extends Record<string, unknown> = {}> {
export interface Plugin<TSetup, TStart, TPluginsSetup extends Record<string, unknown> = {}, TPluginsStart extends Record<string, unknown> = {}> {
// (undocumented)
setup: (core: PluginSetupContext, plugins: TPluginsSetup) => TSetup | Promise<TSetup>;
// (undocumented)
start: (core: PluginStartContext, plugins: TPluginsStart) => TStart | Promise<TStart>;
// (undocumented)
stop?: () => void;
}
// @public
export type PluginInitializer<TSetup, TPluginsSetup extends Record<string, unknown> = {}> = (core: PluginInitializerContext) => Plugin<TSetup, TPluginsSetup>;
export type PluginInitializer<TSetup, TStart, TPluginsSetup extends Record<string, unknown> = {}, TPluginsStart extends Record<string, unknown> = {}> = (core: PluginInitializerContext) => Plugin<TSetup, TStart, TPluginsSetup, TPluginsStart>;
// @public
export interface PluginInitializerContext {
@ -245,7 +270,7 @@ export { Toast }
export type ToastInput = string | Pick<Toast, Exclude<keyof Toast, 'id'>>;
// @public (undocumented)
export class ToastsSetup {
export class ToastsApi {
// (undocumented)
add(toastOrTitle: ToastInput): Toast;
// (undocumented)
@ -276,6 +301,7 @@ export class UiSettingsClient {
newValue: any;
oldValue: any;
}>;
getUpdateErrors$(): Rx.Observable<Error>;
isCustom(key: string): boolean;
isDeclared(key: string): boolean;
isDefault(key: string): boolean;

View file

@ -48,7 +48,6 @@ exports[`#setup constructs UiSettingsClient and UiSettingsApi: UiSettingsClient
"initialSettings": Object {
"legacyInjectedUiSettingUserValues": true,
},
"onUpdateError": [Function],
},
],
],

View file

@ -28,18 +28,15 @@ function setup(options: { defaults?: any; initialSettings?: any } = {}) {
settings: {},
}));
const onUpdateError = jest.fn();
const config = new UiSettingsClient({
defaults,
initialSettings,
api: {
batchSet,
} as any,
onUpdateError,
});
return { config, batchSet, onUpdateError };
return { config, batchSet };
}
describe('#get', () => {

Some files were not shown because too many files have changed in this diff Show more