diff --git a/.eslintrc.js b/.eslintrc.js index f279c0dd8a25a..86fbcfb13d0ab 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2,6 +2,8 @@ const { resolve } = require('path'); const { readdirSync } = require('fs'); const dedent = require('dedent'); +const restrictedModules = { paths: ['gulp-util'] }; + module.exports = { extends: ['@elastic/eslint-config-kibana', '@elastic/eslint-config-kibana/jest'], @@ -17,6 +19,11 @@ module.exports = { }, }, + rules: { + 'no-restricted-imports': [2, restrictedModules], + 'no-restricted-modules': [2, restrictedModules], + }, + overrides: [ /** * Prettier @@ -116,7 +123,7 @@ module.exports = { 'packages/kbn-ui-framework/generator-kui/**/*', 'packages/kbn-ui-framework/Gruntfile.js', 'packages/kbn-es/src/**/*', - 'x-pack/{dev-tools,gulp_helpers,scripts,test,build_chromium}/**/*', + 'x-pack/{dev-tools,tasks,scripts,test,build_chromium}/**/*', 'x-pack/**/{__tests__,__test__,__jest__,__fixtures__,__mocks__}/**/*', 'x-pack/**/*.test.js', 'x-pack/gulpfile.js', diff --git a/.i18nrc.json b/.i18nrc.json index d45b944ff0348..60484e5efc307 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -1,9 +1,9 @@ { "paths": { - "kbn": "src/core_plugins/kibana", - "common.server": "src/server", "common.ui": "src/ui", - "xpack.idxMgmt": "xpack/plugins/index_management" + "kbn": "src/core_plugins/kibana", + "statusPage": "src/core_plugins/status_page", + "xpack.idxMgmt": "x-pack/plugins/index_management" }, "exclude": [ "src/ui/ui_render/bootstrap/app_bootstrap.js", diff --git a/docs/api.asciidoc b/docs/api.asciidoc index 8376c81ff1812..afe7722a0cec5 100644 --- a/docs/api.asciidoc +++ b/docs/api.asciidoc @@ -29,12 +29,14 @@ entirely. * <> * <> +* <> * <> * <> -- include::api/role-management.asciidoc[] include::api/saved-objects.asciidoc[] +include::api/dashboard-import.asciidoc[] include::api/logstash-configuration-management.asciidoc[] include::api/url-shortening.asciidoc[] diff --git a/docs/api/dashboard-import.asciidoc b/docs/api/dashboard-import.asciidoc new file mode 100644 index 0000000000000..43ed037daf13b --- /dev/null +++ b/docs/api/dashboard-import.asciidoc @@ -0,0 +1,17 @@ +[[dashboard-import-api]] +== Dashboard Import API + +The dashboard import/export APIs allow people to import dashboards along with +all of their corresponding saved objects such as visualizations, saved +searches, and index patterns. + +Traditionally, developers would perform this level of integration by writing +documents directly to the `.kibana` index. *Do not do this!* Writing directly +to the `.kibana` index is not safe and it _will_ result in corrupted data that +permanently breaks Kibana in a future version. + +* <> +* <> + +include::dashboard-import/import.asciidoc[] +include::dashboard-import/export.asciidoc[] diff --git a/docs/api/dashboard-import/export.asciidoc b/docs/api/dashboard-import/export.asciidoc new file mode 100644 index 0000000000000..ddafeb35f7cc0 --- /dev/null +++ b/docs/api/dashboard-import/export.asciidoc @@ -0,0 +1,38 @@ +[[dashboard-import-api-export]] +=== Export Dashboard + +experimental[This functionality is *experimental* and may be changed or removed completely in a future release.] + +The dashboard export API allows people to export dashboards along with all of +their corresponding saved objects such as visualizations, saved searches, and +index patterns. + +==== Request + +`GET /api/kibana/dashboards/export` + +==== Query Parameters + +`dashboard` (optional):: + (array|string) The id(s) of the dashboard(s) to export + +==== Response body + +The response body will have a top level `objects` property that contains an +array of saved objects. The order of these objects is not guaranteed. You +should use this exact response body as the request body for the corresponding +<>. + +==== Examples + +The following example exports all saved objects associated with and including +the dashboard with id `942dcef0-b2cd-11e8-ad8e-85441f0c2e5c`. + +[source,js] +-------------------------------------------------- +GET api/kibana/dashboards/export?dashboard=942dcef0-b2cd-11e8-ad8e-85441f0c2e5c +-------------------------------------------------- +// KIBANA + +A successful call returns a response code of `200` along with the exported +objects as the response body. diff --git a/docs/api/dashboard-import/import.asciidoc b/docs/api/dashboard-import/import.asciidoc new file mode 100644 index 0000000000000..e95d15f1b20d0 --- /dev/null +++ b/docs/api/dashboard-import/import.asciidoc @@ -0,0 +1,96 @@ +[[dashboard-import-api-import]] +=== Import Dashboard + +experimental[This functionality is *experimental* and may be changed or removed completely in a future release.] + +The dashboard import API allows people to import dashboards along with all of +their corresponding saved objects such as visualizations, saved searches, and +index patterns. + +==== Request + +`POST /api/kibana/dashboards/import` + +==== Query Parameters + +`force` (optional):: + (boolean) Overwrite any existing objects on id conflict +`exclude` (optional):: + (array) Saved object types that should not be imported + +==== Request Body + +The request body is JSON, but you should not manually construct a payload to +this endpoint. Instead, use the complete response body from the +<> as the request body to +this import API. + +==== Response body + +The response body will have a top level `objects` property that contains an +array of the saved objects that were created. + +==== Examples + +The following example imports saved objects associated with and including the +dashboard with id `942dcef0-b2cd-11e8-ad8e-85441f0c2e5c`. + +[source,js] +-------------------------------------------------- +POST api/kibana/dashboards/import?exclude=index-pattern +{ + "objects": [ + { + "id": "80b956f0-b2cd-11e8-ad8e-85441f0c2e5c", + "type": "visualization", + "updated_at": "2018-09-07T18:40:33.247Z", + "version": 1, + "attributes": { + "title": "Count Example", + "visState": "{\"title\":\"Count Example\",\"type\":\"metric\",\"params\":{\"addTooltip\":true,\"addLegend\":false,\"type\":\"metric\",\"metric\":{\"percentageMode\":false,\"useRanges\":false,\"colorSchema\":\"Green to Red\",\"metricColorMode\":\"None\",\"colorsRange\":[{\"from\":0,\"to\":10000}],\"labels\":{\"show\":true},\"invertColors\":false,\"style\":{\"bgFill\":\"#000\",\"bgColor\":false,\"labelColor\":false,\"subText\":\"\",\"fontSize\":60}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"90943e30-9a47-11e8-b64d-95841ca0b247\",\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + } + } + }, + { + "id": "90943e30-9a47-11e8-b64d-95841ca0b247", + "type": "index-pattern", + "updated_at": "2018-09-07T18:39:47.683Z", + "version": 1, + "attributes": { + "title": "kibana_sample_data_logs", + "timeFieldName": "timestamp", + "fields": "", + "fieldFormatMap": "{\"hour_of_day\":{}}" + } + }, + { + "id": "942dcef0-b2cd-11e8-ad8e-85441f0c2e5c", + "type": "dashboard", + "updated_at": "2018-09-07T18:41:05.887Z", + "version": 1, + "attributes": { + "title": "Example Dashboard", + "hits": 0, + "description": "", + "panelsJSON": "[{\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"1\"},\"version\":\"7.0.0-alpha1\",\"panelIndex\":\"1\",\"type\":\"visualization\",\"id\":\"80b956f0-b2cd-11e8-ad8e-85441f0c2e5c\",\"embeddableConfig\":{}}]", + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "version": 1, + "timeRestore": false, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + } + } + } + ] +} +-------------------------------------------------- +// KIBANA + +A response code of `200` will be returned even if there are errors importing +individual saved objects. In that case, error information will be returned in +the response body on an object-by-object basis. diff --git a/docs/api/spaces-management/delete.asciidoc b/docs/api/spaces-management/delete.asciidoc new file mode 100644 index 0000000000000..c5ae025dd9e2e --- /dev/null +++ b/docs/api/spaces-management/delete.asciidoc @@ -0,0 +1,25 @@ +[[spaces-api-delete]] +=== Delete space + +experimental[This API is *experimental* and may be changed or removed completely in a future release. The underlying Spaces concepts are stable, but the APIs for managing Spaces are currently experimental.] + +[WARNING] +================================================== +Deleting a space will automatically delete all saved objects that belong to that space. This operation cannot be undone! +================================================== + +==== Request + +To delete a space, submit a DELETE request to the `/api/spaces/space/` +endpoint: + +[source,js] +-------------------------------------------------- +DELETE /api/spaces/space/marketing +-------------------------------------------------- +// KIBANA + +==== Response + +If the space is successfully deleted, the response code is `204`; otherwise, the response +code is 404. diff --git a/docs/api/spaces-management/get.asciidoc b/docs/api/spaces-management/get.asciidoc new file mode 100644 index 0000000000000..c79a883a80e4b --- /dev/null +++ b/docs/api/spaces-management/get.asciidoc @@ -0,0 +1,77 @@ +[[spaces-api-get]] +=== Get Space + +experimental[This API is *experimental* and may be changed or removed completely in a future release. The underlying Spaces concepts are stable, but the APIs for managing Spaces are currently experimental.] + +Retrieves all {kib} spaces, or a specific space. + +==== Get all {kib} spaces + +===== Request + +To retrieve all spaces, issue a GET request to the +/api/spaces/space endpoint. + +[source,js] +-------------------------------------------------- +GET /api/spaces/space +-------------------------------------------------- +// KIBANA + +===== Response + +A successful call returns a response code of `200` and a response body containing a JSON +representation of the spaces. + +[source,js] +-------------------------------------------------- +[ + { + "id": "default", + "name": "Default", + "description" : "This is the Default Space", + "_reserved": true + }, + { + "id": "marketing", + "name": "Marketing", + "description" : "This is the Marketing Space", + "color": "#aabbcc", + "initials": "MK" + }, + { + "id": "sales", + "name": "Sales", + "initials": "MK" + }, +] +-------------------------------------------------- + +==== Get a specific space + +===== Request + +To retrieve a specific space, issue a GET request to +the `/api/spaces/space/` endpoint: + +[source,js] +-------------------------------------------------- +GET /api/spaces/space/marketing +-------------------------------------------------- +// KIBANA + +===== Response + +A successful call returns a response code of `200` and a response body containing a JSON +representation of the space. + +[source,js] +-------------------------------------------------- +{ + "id": "marketing", + "name": "Marketing", + "description" : "This is the Marketing Space", + "color": "#aabbcc", + "initials": "MK" +} +-------------------------------------------------- diff --git a/docs/api/spaces-management/post.asciidoc b/docs/api/spaces-management/post.asciidoc new file mode 100644 index 0000000000000..569835c78b2f8 --- /dev/null +++ b/docs/api/spaces-management/post.asciidoc @@ -0,0 +1,50 @@ +[[spaces-api-post]] +=== Create Space + +experimental[This API is *experimental* and may be changed or removed completely in a future release. The underlying Spaces concepts are stable, but the APIs for managing Spaces are currently experimental.] + +Creates a new {kib} space. To update an existing space, use the PUT command. + +==== Request + +To create a space, issue a POST request to the +`/api/spaces/space` endpoint. + +[source,js] +-------------------------------------------------- +PUT /api/spaces/space +-------------------------------------------------- + +==== Request Body + +The following parameters can be specified in the body of a POST request to create a space: + +`id`:: (string) Required identifier for the space. This identifier becomes part of Kibana's URL when inside the space. This cannot be changed by the update operation. + +`name`:: (string) Required display name for the space. + +`description`:: (string) Optional description for the space. + +`initials`:: (string) Optionally specify the initials shown in the Space Avatar for this space. By default, the initials will be automatically generated from the space name. +If specified, initials should be either 1 or 2 characters. + +`color`:: (string) Optioanlly specify the hex color code used in the Space Avatar for this space. By default, the color will be automatically generated from the space name. + +===== Example + +[source,js] +-------------------------------------------------- +POST /api/spaces/space +{ + "id": "marketing", + "name": "Marketing", + "description" : "This is the Marketing Space", + "color": "#aabbcc", + "initials": "MK" +} +-------------------------------------------------- +// KIBANA + +==== Response + +A successful call returns a response code of `200` with the created Space. diff --git a/docs/api/spaces-management/put.asciidoc b/docs/api/spaces-management/put.asciidoc new file mode 100644 index 0000000000000..529742bf2ce66 --- /dev/null +++ b/docs/api/spaces-management/put.asciidoc @@ -0,0 +1,50 @@ +[[spaces-api-put]] +=== Update Space + +experimental[This API is *experimental* and may be changed or removed completely in a future release. The underlying Spaces concepts are stable, but the APIs for managing Spaces are currently experimental.] + +Updates an existing {kib} space. To create a new space, use the POST command. + +==== Request + +To update a space, issue a PUT request to the +`/api/spaces/space/` endpoint. + +[source,js] +-------------------------------------------------- +PUT /api/spaces/space/ +-------------------------------------------------- + +==== Request Body + +The following parameters can be specified in the body of a PUT request to update a space: + +`id`:: (string) Required identifier for the space. This identifier becomes part of Kibana's URL when inside the space. This cannot be changed by the update operation. + +`name`:: (string) Required display name for the space. + +`description`:: (string) Optional description for the space. + +`initials`:: (string) Optionally specify the initials shown in the Space Avatar for this space. By default, the initials will be automatically generated from the space name. +If specified, initials should be either 1 or 2 characters. + +`color`:: (string) Optioanlly specify the hex color code used in the Space Avatar for this space. By default, the color will be automatically generated from the space name. + +===== Example + +[source,js] +-------------------------------------------------- +PUT /api/spaces/space/marketing +{ + "id": "marketing", + "name": "Marketing", + "description" : "This is the Marketing Space", + "color": "#aabbcc", + "initials": "MK" +} +-------------------------------------------------- +// KIBANA + +==== Response + +A successful call returns a response code of `200` with the updated Space. diff --git a/docs/api/spaces.asciidoc b/docs/api/spaces.asciidoc new file mode 100644 index 0000000000000..ea66d50d396b9 --- /dev/null +++ b/docs/api/spaces.asciidoc @@ -0,0 +1,17 @@ +[role="xpack"] +[[spaces-api]] +== Kibana Spaces API + +experimental[This API is *experimental* and may be changed or removed completely in a future release. The underlying Spaces concepts are stable, but the APIs for managing Spaces are currently experimental.] + +The spaces API allows people to manage their spaces within {kib}. + +* <> +* <> +* <> +* <> + +include::spaces-management/put.asciidoc[] +include::spaces-management/post.asciidoc[] +include::spaces-management/get.asciidoc[] +include::spaces-management/delete.asciidoc[] diff --git a/docs/development/visualize/development-create-visualization.asciidoc b/docs/development/visualize/development-create-visualization.asciidoc index 437b4ff9e5f3d..f843904fcdc41 100644 --- a/docs/development/visualize/development-create-visualization.asciidoc +++ b/docs/development/visualize/development-create-visualization.asciidoc @@ -179,7 +179,7 @@ VisTypesRegistryProvider.register(MyNewVisType); [[development-react-visualization-type]] ==== React Visualization Type React visualization type assumes you are using React as your rendering technology. -Just pass in a React component to `visConfig.template`. +Just pass in a React component to `visConfig.component`. The visualization will receive `vis`, `appState`, `updateStatus` and `visData` as props. It also has a `renderComplete` property, which needs to be called once the rendering has completed. @@ -197,7 +197,7 @@ const MyNewVisType = (Private) => { icon: 'my_icon', description: 'Cool new chart', visConfig: { - template: ReactComponent + component: ReactComponent } }); } diff --git a/docs/migration/migrate_7_0.asciidoc b/docs/migration/migrate_7_0.asciidoc index 60272acc8ad28..c7e521c812326 100644 --- a/docs/migration/migrate_7_0.asciidoc +++ b/docs/migration/migrate_7_0.asciidoc @@ -56,3 +56,9 @@ considered unique based on its persistent UUID, which is written to the path.dat *Details:* The `/shorten` API has been deprecated since 6.5, when it was replaced by the `/api/shorten_url` API. *Impact:* The '/shorten' API has been removed. Use the '/api/shorten_url' API instead. + +[float] +=== Deprecated kibana.yml setting logging.useUTC has been replaced with logging.timezone +*Details:* Any timezone can now be specified by canonical id. + +*Impact:* The logging.useUTC flag will have to be replaced with a timezone id. If set to true the id is `UTC`. diff --git a/docs/monitoring/cluster-alerts.asciidoc b/docs/monitoring/cluster-alerts.asciidoc index 15df791ae2746..fce76965dd9ae 100644 --- a/docs/monitoring/cluster-alerts.asciidoc +++ b/docs/monitoring/cluster-alerts.asciidoc @@ -46,8 +46,6 @@ To receive email notifications for the Cluster Alerts: 1. Configure an email account as described in {xpack-ref}/actions-email.html#configuring-email[Configuring Email Accounts]. -2. Navigate to the *Management* page in {kib}. -3. Go to the *Advanced Settings* page, find the `xpack:defaultAdminEmail` -setting, and enter your email address. +2. Configure the `xpack.monitoring.cluster_alerts.email_notifications.email_address` setting in `kibana.yml` with your email address. Email notifications are sent only when Cluster Alerts are triggered and resolved. diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 1f09e5dc6e2a6..7a21705336313 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -73,7 +73,7 @@ error messages. [[logging-verbose]]`logging.verbose:`:: *Default: false* Set the value of this setting to `true` to log all events, including system usage information and all requests. Supported on Elastic Cloud Enterprise. -`logging.useUTC`:: *Default: true* Set the value of this setting to `false` to log events using the timezone of the server, rather than UTC. +`logging.timezone`:: *Default: UTC* Set to the canonical timezone id (e.g. `US/Pacific`) to log events using that timezone. A list of timezones can be referenced at https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. `map.includeElasticMapsService:`:: *Default: true* Turns on or off whether layers from the Elastic Maps Service should be included in the vector and tile layer option list. By turning this off, only the layers that are configured here will be included. diff --git a/package.json b/package.json index 5ea6728b6458e..3239cdefafc09 100644 --- a/package.json +++ b/package.json @@ -224,7 +224,7 @@ "@types/glob": "^5.0.35", "@types/hapi-latest": "npm:@types/hapi@17.0.12", "@types/has-ansi": "^3.0.0", - "@types/jest": "^22.2.3", + "@types/jest": "^23.3.1", "@types/joi": "^10.4.4", "@types/jquery": "3.3.1", "@types/js-yaml": "^3.11.1", @@ -233,17 +233,18 @@ "@types/minimatch": "^2.0.29", "@types/node": "^8.10.20", "@types/prop-types": "^15.5.3", + "@types/puppeteer": "^1.6.2", "@types/react": "^16.3.14", "@types/react-dom": "^16.0.5", "@types/react-redux": "^6.0.6", "@types/redux-actions": "^2.2.1", "@types/sinon": "^5.0.0", "@types/strip-ansi": "^3.0.0", - "@types/supertest": "^2.0.4", + "@types/supertest": "^2.0.5", "@types/type-detect": "^4.0.1", "angular-mocks": "1.4.7", "babel-eslint": "8.1.2", - "babel-jest": "^22.4.3", + "babel-jest": "^23.4.2", "backport": "4.4.1", "chai": "3.5.0", "chance": "1.0.10", @@ -259,7 +260,7 @@ "eslint-config-prettier": "^2.9.0", "eslint-plugin-babel": "4.1.2", "eslint-plugin-import": "2.8.0", - "eslint-plugin-jest": "^21.6.2", + "eslint-plugin-jest": "^21.22.0", "eslint-plugin-mocha": "4.11.0", "eslint-plugin-no-unsanitized": "^3.0.2", "eslint-plugin-prefer-object-spread": "1.2.1", @@ -284,8 +285,8 @@ "husky": "0.8.1", "image-diff": "1.6.0", "istanbul-instrumenter-loader": "3.0.0", - "jest": "^22.4.3", - "jest-cli": "^22.4.3", + "jest": "^23.5.0", + "jest-cli": "^23.5.0", "jest-raw-loader": "^1.0.1", "jimp": "0.2.28", "jsdom": "9.9.1", @@ -318,10 +319,10 @@ "simple-git": "1.37.0", "sinon": "^5.0.7", "strip-ansi": "^3.0.1", - "supertest": "3.0.0", - "supertest-as-promised": "4.0.2", + "supertest": "^3.1.0", + "supertest-as-promised": "^4.0.2", "tree-kill": "^1.1.0", - "ts-jest": "^22.4.6", + "ts-jest": "^23.1.4", "ts-loader": "^3.5.0", "ts-node": "^6.0.3", "tslint": "^5.10.0", diff --git a/packages/eslint-config-kibana/package.json b/packages/eslint-config-kibana/package.json index 0c04ea50a4c06..19574bb2df058 100644 --- a/packages/eslint-config-kibana/package.json +++ b/packages/eslint-config-kibana/package.json @@ -19,7 +19,7 @@ "eslint": "^4.1.0", "eslint-plugin-babel": "^4.1.1", "eslint-plugin-import": "^2.6.0", - "eslint-plugin-jest": "^21.0.0", + "eslint-plugin-jest": "^21.22.0", "eslint-plugin-mocha": "^4.9.0", "eslint-plugin-no-unsanitized": "^3.0.2", "eslint-plugin-prefer-object-spread": "^1.2.1", diff --git a/packages/kbn-datemath/package.json b/packages/kbn-datemath/package.json index 5338b65d83f2d..c469661070816 100644 --- a/packages/kbn-datemath/package.json +++ b/packages/kbn-datemath/package.json @@ -5,8 +5,9 @@ "license": "Apache-2.0", "private": true, "main": "target/index.js", + "typings": "target/index.d.ts", "scripts": { - "build": "babel src --out-dir target", + "build": "babel src --out-dir target --copy-files", "kbn:bootstrap": "yarn build", "kbn:watch": "yarn build --watch" }, diff --git a/packages/kbn-datemath/src/index.d.ts b/packages/kbn-datemath/src/index.d.ts new file mode 100644 index 0000000000000..e3389fb255700 --- /dev/null +++ b/packages/kbn-datemath/src/index.d.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +declare module '@kbn/datemath' { + const dateMath: { + parse: any; + unitsMap: any; + units: string[]; + unitsAsc: string[]; + unitsDesc: string[]; + }; + export default dateMath; +} diff --git a/packages/kbn-datemath/src/index.js b/packages/kbn-datemath/src/index.js index 17d91a530fdb3..6576a458fe77b 100644 --- a/packages/kbn-datemath/src/index.js +++ b/packages/kbn-datemath/src/index.js @@ -19,9 +19,20 @@ import moment from 'moment'; -const units = ['y', 'M', 'w', 'd', 'h', 'm', 's', 'ms']; -const unitsDesc = units; -const unitsAsc = [...unitsDesc].reverse(); +const unitsMap = { + ms: { weight: 1, type: 'fixed', base: 1 }, + s: { weight: 2, type: 'fixed', base: 1000 }, + m: { weight: 3, type: 'mixed', base: 1000 * 60 }, + h: { weight: 4, type: 'mixed', base: 1000 * 60 * 60 }, + d: { weight: 5, type: 'mixed', base: 1000 * 60 * 60 * 24 }, + w: { weight: 6, type: 'calendar' }, + M: { weight: 7, type: 'calendar' }, + // q: { weight: 8, type: 'calendar' }, // TODO: moment duration does not support quarter + y: { weight: 9, type: 'calendar' }, +}; +const units = Object.keys(unitsMap).sort((a, b) => unitsMap[b].weight - unitsMap[a].weight); +const unitsDesc = [...units]; +const unitsAsc = [...units].reverse(); const isDate = d => Object.prototype.toString.call(d) === '[object Date]'; @@ -142,6 +153,7 @@ function parseDateMath(mathString, time, roundUp) { export default { parse: parse, + unitsMap: Object.freeze(unitsMap), units: Object.freeze(units), unitsAsc: Object.freeze(unitsAsc), unitsDesc: Object.freeze(unitsDesc), diff --git a/packages/kbn-datemath/tsconfig.json b/packages/kbn-datemath/tsconfig.json new file mode 100644 index 0000000000000..c23b6635a5c19 --- /dev/null +++ b/packages/kbn-datemath/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "./target" + }, + "include": [ + "./src/**/*.ts" + ] +} diff --git a/packages/kbn-plugin-generator/sao_template/template/package.json b/packages/kbn-plugin-generator/sao_template/template/package.json index 70608f3c2d79a..f61e589960cd8 100755 --- a/packages/kbn-plugin-generator/sao_template/template/package.json +++ b/packages/kbn-plugin-generator/sao_template/template/package.json @@ -24,7 +24,7 @@ "eslint": "^4.11.0", "eslint-plugin-babel": "^4.1.1", "eslint-plugin-import": "^2.3.0", - "eslint-plugin-jest": "^21.3.2", + "eslint-plugin-jest": "^21.22.0", "eslint-plugin-mocha": "^4.9.0", "eslint-plugin-no-unsanitized": "^3.0.2", "eslint-plugin-prefer-object-spread": "^1.2.1", diff --git a/packages/kbn-pm/package.json b/packages/kbn-pm/package.json index f7faefa5f9d46..53b72582981a1 100644 --- a/packages/kbn-pm/package.json +++ b/packages/kbn-pm/package.json @@ -20,7 +20,7 @@ "@types/globby": "^6.1.0", "@types/has-ansi": "^3.0.0", "@types/indent-string": "^3.0.0", - "@types/jest": "^22.1.3", + "@types/jest": "^23.3.1", "@types/lodash.clonedeepwith": "^4.5.3", "@types/log-symbols": "^2.0.0", "@types/mkdirp": "^0.5.2", diff --git a/packages/kbn-pm/src/commands/bootstrap.test.ts b/packages/kbn-pm/src/commands/bootstrap.test.ts index 92785ebc91d3d..66bdc6d82ecda 100644 --- a/packages/kbn-pm/src/commands/bootstrap.test.ts +++ b/packages/kbn-pm/src/commands/bootstrap.test.ts @@ -53,6 +53,7 @@ const noop = () => { afterEach(() => { jest.resetAllMocks(); + jest.restoreAllMocks(); }); test('handles dependencies of dependencies', async () => { @@ -96,8 +97,6 @@ test('handles dependencies of dependencies', async () => { rootPath: '', }); - logMock.mockRestore(); - expect(mockInstallInDir.mock.calls).toMatchSnapshot('install in dir'); expect(logMock.mock.calls).toMatchSnapshot('logs'); }); @@ -127,8 +126,6 @@ test('does not run installer if no deps in package', async () => { rootPath: '', }); - logMock.mockRestore(); - expect(mockInstallInDir.mock.calls).toMatchSnapshot('install in dir'); expect(logMock.mock.calls).toMatchSnapshot('logs'); }); @@ -143,7 +140,7 @@ test('handles "frozen-lockfile"', async () => { const projects = new Map([['kibana', kibana]]); const projectGraph = buildProjectGraph(projects); - const logMock = jest.spyOn(console, 'log').mockImplementation(noop); + jest.spyOn(console, 'log').mockImplementation(noop); await BootstrapCommand.run(projects, projectGraph, { extraArgs: [], @@ -153,8 +150,6 @@ test('handles "frozen-lockfile"', async () => { rootPath: '', }); - logMock.mockRestore(); - expect(mockInstallInDir.mock.calls).toMatchSnapshot('install in dir'); }); @@ -177,7 +172,7 @@ test('calls "kbn:bootstrap" scripts and links executables after installing deps' const projects = new Map([['kibana', kibana], ['bar', bar]]); const projectGraph = buildProjectGraph(projects); - const logMock = jest.spyOn(console, 'log').mockImplementation(noop); + jest.spyOn(console, 'log').mockImplementation(noop); await BootstrapCommand.run(projects, projectGraph, { extraArgs: [], @@ -185,8 +180,6 @@ test('calls "kbn:bootstrap" scripts and links executables after installing deps' rootPath: '', }); - logMock.mockRestore(); - expect(mockLinkProjectExecutables.mock.calls).toMatchSnapshot('link bins'); expect(mockRunScriptInPackageStreaming.mock.calls).toMatchSnapshot('script'); }); diff --git a/packages/kbn-pm/src/utils/link_project_executables.test.ts b/packages/kbn-pm/src/utils/link_project_executables.test.ts index b618a28e642d3..ac0b69d01f798 100644 --- a/packages/kbn-pm/src/utils/link_project_executables.test.ts +++ b/packages/kbn-pm/src/utils/link_project_executables.test.ts @@ -81,6 +81,7 @@ expect.addSnapshotSerializer(stripAnsiSnapshotSerializer); afterEach(() => { jest.resetAllMocks(); + jest.restoreAllMocks(); }); describe('bin script points nowhere', () => { @@ -102,7 +103,6 @@ describe('bin script points to a file', () => { // noop }); await linkProjectExecutables(projectsByName, projectGraph); - logMock.mockRestore(); expect(getFsMockCalls()).toMatchSnapshot('fs module calls'); expect(logMock.mock.calls).toMatchSnapshot('logs'); diff --git a/packages/kbn-pm/yarn.lock b/packages/kbn-pm/yarn.lock index c16d37de830c8..a19ebad9dfb01 100644 --- a/packages/kbn-pm/yarn.lock +++ b/packages/kbn-pm/yarn.lock @@ -70,9 +70,9 @@ version "3.0.0" resolved "https://registry.yarnpkg.com/@types/indent-string/-/indent-string-3.0.0.tgz#9ebb391ceda548926f5819ad16405349641b999f" -"@types/jest@^22.1.3": - version "22.1.3" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-22.1.3.tgz#25da391935e6fac537551456f077ce03144ec168" +"@types/jest@^23.3.1": + version "23.3.1" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.3.1.tgz#a4319aedb071d478e6f407d1c4578ec8156829cf" "@types/lodash.clonedeepwith@^4.5.3": version "4.5.3" diff --git a/packages/kbn-system-loader/package.json b/packages/kbn-system-loader/package.json index 5a169283edb77..55c65747c9fd4 100644 --- a/packages/kbn-system-loader/package.json +++ b/packages/kbn-system-loader/package.json @@ -10,7 +10,7 @@ "kbn:bootstrap": "yarn build" }, "devDependencies": { - "@types/jest": "^22.2.2", + "@types/jest": "^23.3.1", "typescript": "^2.9.2" } } diff --git a/packages/kbn-system-loader/yarn.lock b/packages/kbn-system-loader/yarn.lock index 5e55ff3b0318a..4b0f2d57d94d5 100644 --- a/packages/kbn-system-loader/yarn.lock +++ b/packages/kbn-system-loader/yarn.lock @@ -2,9 +2,9 @@ # yarn lockfile v1 -"@types/jest@^22.2.2": - version "22.2.2" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-22.2.2.tgz#afe5dacbd00d65325f52da0ed3e76e259629ac9d" +"@types/jest@^23.3.1": + version "23.3.1" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.3.1.tgz#a4319aedb071d478e6f407d1c4578ec8156829cf" typescript@^2.9.2: version "2.9.2" diff --git a/packages/kbn-test/src/es/es_test_config.js b/packages/kbn-test/src/es/es_test_config.js index 2e9e91e50b4b2..e273172ef614b 100644 --- a/packages/kbn-test/src/es/es_test_config.js +++ b/packages/kbn-test/src/es/es_test_config.js @@ -53,8 +53,9 @@ export const esTestConfig = new class EsTestConfig { }; } - const username = process.env.TEST_KIBANA_USERNAME || adminTestUser.username; - const password = process.env.TEST_KIBANA_PASSWORD || adminTestUser.password; + const username = process.env.TEST_ES_USERNAME || adminTestUser.username; + const password = process.env.TEST_ES_PASSWORD || adminTestUser.password; + return { // Allow setting any individual component(s) of the URL, // or use default values (username and password from ../kbn/users.js) diff --git a/src/cli/cluster/_mock_cluster_fork.js b/src/cli/cluster/__mocks__/cluster.js similarity index 75% rename from src/cli/cluster/_mock_cluster_fork.js rename to src/cli/cluster/__mocks__/cluster.js index 4312f6a85c53a..14efc4b6f0150 100644 --- a/src/cli/cluster/_mock_cluster_fork.js +++ b/src/cli/cluster/__mocks__/cluster.js @@ -16,15 +16,14 @@ * specific language governing permissions and limitations * under the License. */ +/* eslint-env jest */ import EventEmitter from 'events'; import { assign, random } from 'lodash'; -import sinon from 'sinon'; -import cluster from 'cluster'; import { delay } from 'bluebird'; -export default class MockClusterFork extends EventEmitter { - constructor() { +class MockClusterFork extends EventEmitter { + constructor(cluster) { super(); let dead = true; @@ -35,7 +34,7 @@ export default class MockClusterFork extends EventEmitter { assign(this, { process: { - kill: sinon.spy(() => { + kill: jest.fn(() => { (async () => { await wait(); this.emit('disconnect'); @@ -46,13 +45,13 @@ export default class MockClusterFork extends EventEmitter { })(); }), }, - isDead: sinon.spy(() => dead), - send: sinon.stub() + isDead: jest.fn(() => dead), + send: jest.fn() }); - sinon.spy(this, 'on'); - sinon.spy(this, 'removeListener'); - sinon.spy(this, 'emit'); + jest.spyOn(this, 'on'); + jest.spyOn(this, 'removeListener'); + jest.spyOn(this, 'emit'); (async () => { await wait(); @@ -61,3 +60,12 @@ export default class MockClusterFork extends EventEmitter { })(); } } + +class MockCluster extends EventEmitter { + fork = jest.fn(() => new MockClusterFork(this)); + setupMaster = jest.fn(); +} + +export function mockCluster() { + return new MockCluster(); +} diff --git a/src/cli/cluster/cluster_manager.js b/src/cli/cluster/cluster_manager.js index 0a514138b09f2..1ea8a91eb21ef 100644 --- a/src/cli/cluster/cluster_manager.js +++ b/src/cli/cluster/cluster_manager.js @@ -19,31 +19,30 @@ import { resolve } from 'path'; import { debounce, invoke, bindAll, once, uniq } from 'lodash'; +import { fromEvent, race } from 'rxjs'; +import { first } from 'rxjs/operators'; import Log from '../log'; import Worker from './worker'; import { Config } from '../../server/config/config'; import { transformDeprecations } from '../../server/config/transform_deprecations'; -import { configureBasePathProxy } from './configure_base_path_proxy'; process.env.kbnWorkerType = 'managr'; export default class ClusterManager { - static async create(opts = {}, settings = {}) { - const transformedSettings = transformDeprecations(settings); - const config = Config.withDefaultSchema(transformedSettings); - - const basePathProxy = opts.basePath - ? await configureBasePathProxy(config) - : undefined; - - return new ClusterManager(opts, config, basePathProxy); + static create(opts, settings = {}, basePathProxy) { + return new ClusterManager( + opts, + Config.withDefaultSchema(transformDeprecations(settings)), + basePathProxy + ); } constructor(opts, config, basePathProxy) { this.log = new Log(opts.quiet, opts.silent); this.addedCount = 0; this.inReplMode = !!opts.repl; + this.basePathProxy = basePathProxy; const serverArgv = []; const optimizerArgv = [ @@ -51,17 +50,15 @@ export default class ClusterManager { '--server.autoListen=false', ]; - if (basePathProxy) { - this.basePathProxy = basePathProxy; - + if (this.basePathProxy) { optimizerArgv.push( - `--server.basePath=${this.basePathProxy.getBasePath()}`, + `--server.basePath=${this.basePathProxy.basePath}`, '--server.rewriteBasePath=true', ); serverArgv.push( - `--server.port=${this.basePathProxy.getTargetPort()}`, - `--server.basePath=${this.basePathProxy.getBasePath()}`, + `--server.port=${this.basePathProxy.targetPort}`, + `--server.basePath=${this.basePathProxy.basePath}`, '--server.rewriteBasePath=true', ); } @@ -82,12 +79,6 @@ export default class ClusterManager { }) ]; - if (basePathProxy) { - // Pass server worker to the basepath proxy so that it can hold off the - // proxying until server worker is ready. - this.basePathProxy.serverWorker = this.server; - } - // broker messages between workers this.workers.forEach((worker) => { worker.on('broadcast', (msg) => { @@ -130,7 +121,10 @@ export default class ClusterManager { this.setupManualRestart(); invoke(this.workers, 'start'); if (this.basePathProxy) { - this.basePathProxy.start(); + this.basePathProxy.start({ + blockUntil: this.blockUntil.bind(this), + shouldRedirectFromOldBasePath: this.shouldRedirectFromOldBasePath.bind(this), + }); } } @@ -222,4 +216,23 @@ export default class ClusterManager { this.log.bad('failed to watch files!\n', err.stack); process.exit(1); // eslint-disable-line no-process-exit } + + shouldRedirectFromOldBasePath(path) { + const isApp = path.startsWith('app/'); + const isKnownShortPath = ['login', 'logout', 'status'].includes(path); + + return isApp || isKnownShortPath; + } + + blockUntil() { + // Wait until `server` worker either crashes or starts to listen. + if (this.server.listening || this.server.crashed) { + return Promise.resolve(); + } + + return race( + fromEvent(this.server, 'listening'), + fromEvent(this.server, 'crashed') + ).pipe(first()).toPromise(); + } } diff --git a/src/cli/cluster/cluster_manager.test.js b/src/cli/cluster/cluster_manager.test.js index b80ee62da29c3..ab42c4a369bb8 100644 --- a/src/cli/cluster/cluster_manager.test.js +++ b/src/cli/cluster/cluster_manager.test.js @@ -17,36 +17,43 @@ * under the License. */ -import sinon from 'sinon'; +import { mockCluster } from './__mocks__/cluster'; +jest.mock('cluster', () => mockCluster()); +jest.mock('readline', () => ({ + createInterface: jest.fn(() => ({ + on: jest.fn(), + prompt: jest.fn(), + setPrompt: jest.fn(), + })), +})); + import cluster from 'cluster'; import { sample } from 'lodash'; import ClusterManager from './cluster_manager'; import Worker from './worker'; -describe('CLI cluster manager', function () { - const sandbox = sinon.createSandbox(); - - beforeEach(function () { - sandbox.stub(cluster, 'fork').callsFake(() => { +describe('CLI cluster manager', () => { + beforeEach(() => { + cluster.fork.mockImplementation(() => { return { process: { - kill: sinon.stub(), + kill: jest.fn(), }, - isDead: sinon.stub().returns(false), - removeListener: sinon.stub(), - on: sinon.stub(), - send: sinon.stub() + isDead: jest.fn().mockReturnValue(false), + removeListener: jest.fn(), + addListener: jest.fn(), + send: jest.fn() }; }); }); - afterEach(function () { - sandbox.restore(); + afterEach(() => { + cluster.fork.mockReset(); }); - it('has two workers', async function () { - const manager = await ClusterManager.create({}); + test('has two workers', () => { + const manager = ClusterManager.create({}); expect(manager.workers).toHaveLength(2); for (const worker of manager.workers) expect(worker).toBeInstanceOf(Worker); @@ -55,8 +62,8 @@ describe('CLI cluster manager', function () { expect(manager.server).toBeInstanceOf(Worker); }); - it('delivers broadcast messages to other workers', async function () { - const manager = await ClusterManager.create({}); + test('delivers broadcast messages to other workers', () => { + const manager = ClusterManager.create({}); for (const worker of manager.workers) { Worker.prototype.start.call(worker);// bypass the debounced start method @@ -69,10 +76,111 @@ describe('CLI cluster manager', function () { messenger.emit('broadcast', football); for (const worker of manager.workers) { if (worker === messenger) { - expect(worker.fork.send.callCount).toBe(0); + expect(worker.fork.send).not.toHaveBeenCalled(); } else { - expect(worker.fork.send.firstCall.args[0]).toBe(football); + expect(worker.fork.send).toHaveBeenCalledTimes(1); + expect(worker.fork.send).toHaveBeenCalledWith(football); } } }); + + describe('interaction with BasePathProxy', () => { + test('correctly configures `BasePathProxy`.', async () => { + const basePathProxyMock = { start: jest.fn() }; + + ClusterManager.create({}, {}, basePathProxyMock); + + expect(basePathProxyMock.start).toHaveBeenCalledWith({ + shouldRedirectFromOldBasePath: expect.any(Function), + blockUntil: expect.any(Function), + }); + }); + + describe('proxy is configured with the correct `shouldRedirectFromOldBasePath` and `blockUntil` functions.', () => { + let clusterManager; + let shouldRedirectFromOldBasePath; + let blockUntil; + beforeEach(async () => { + const basePathProxyMock = { start: jest.fn() }; + + clusterManager = ClusterManager.create({}, {}, basePathProxyMock); + + jest.spyOn(clusterManager.server, 'addListener'); + jest.spyOn(clusterManager.server, 'removeListener'); + + [[{ blockUntil, shouldRedirectFromOldBasePath }]] = basePathProxyMock.start.mock.calls; + }); + + test('`shouldRedirectFromOldBasePath()` returns `false` for unknown paths.', () => { + expect(shouldRedirectFromOldBasePath('')).toBe(false); + expect(shouldRedirectFromOldBasePath('some-path/')).toBe(false); + expect(shouldRedirectFromOldBasePath('some-other-path')).toBe(false); + }); + + test('`shouldRedirectFromOldBasePath()` returns `true` for `app` and other known paths.', () => { + expect(shouldRedirectFromOldBasePath('app/')).toBe(true); + expect(shouldRedirectFromOldBasePath('login')).toBe(true); + expect(shouldRedirectFromOldBasePath('logout')).toBe(true); + expect(shouldRedirectFromOldBasePath('status')).toBe(true); + }); + + test('`blockUntil()` resolves immediately if worker has already crashed.', async () => { + clusterManager.server.crashed = true; + + await expect(blockUntil()).resolves.not.toBeDefined(); + expect(clusterManager.server.addListener).not.toHaveBeenCalled(); + expect(clusterManager.server.removeListener).not.toHaveBeenCalled(); + }); + + test('`blockUntil()` resolves immediately if worker is already listening.', async () => { + clusterManager.server.listening = true; + + await expect(blockUntil()).resolves.not.toBeDefined(); + expect(clusterManager.server.addListener).not.toHaveBeenCalled(); + expect(clusterManager.server.removeListener).not.toHaveBeenCalled(); + }); + + test('`blockUntil()` resolves when worker crashes.', async () => { + const blockUntilPromise = blockUntil(); + + expect(clusterManager.server.addListener).toHaveBeenCalledTimes(2); + expect(clusterManager.server.addListener).toHaveBeenCalledWith( + 'crashed', + expect.any(Function) + ); + + const [, [eventName, onCrashed]] = clusterManager.server.addListener.mock.calls; + // Check event name to make sure we call the right callback, + // in Jest 23 we could use `toHaveBeenNthCalledWith` instead. + expect(eventName).toBe('crashed'); + expect(clusterManager.server.removeListener).not.toHaveBeenCalled(); + + onCrashed(); + await expect(blockUntilPromise).resolves.not.toBeDefined(); + + expect(clusterManager.server.removeListener).toHaveBeenCalledTimes(2); + }); + + test('`blockUntil()` resolves when worker starts listening.', async () => { + const blockUntilPromise = blockUntil(); + + expect(clusterManager.server.addListener).toHaveBeenCalledTimes(2); + expect(clusterManager.server.addListener).toHaveBeenCalledWith( + 'listening', + expect.any(Function) + ); + + const [[eventName, onListening]] = clusterManager.server.addListener.mock.calls; + // Check event name to make sure we call the right callback, + // in Jest 23 we could use `toHaveBeenNthCalledWith` instead. + expect(eventName).toBe('listening'); + expect(clusterManager.server.removeListener).not.toHaveBeenCalled(); + + onListening(); + await expect(blockUntilPromise).resolves.not.toBeDefined(); + + expect(clusterManager.server.removeListener).toHaveBeenCalledTimes(2); + }); + }); + }); }); diff --git a/src/cli/cluster/configure_base_path_proxy.js b/src/cli/cluster/configure_base_path_proxy.js deleted file mode 100644 index 477b10053d1e6..0000000000000 --- a/src/cli/cluster/configure_base_path_proxy.js +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Server } from 'hapi'; -import { createBasePathProxy } from '../../core'; -import { setupLogging } from '../../server/logging'; - -export async function configureBasePathProxy(config) { - // New platform forwards all logs to the legacy platform so we need HapiJS server - // here just for logging purposes and nothing else. - const server = new Server(); - setupLogging(server, config); - - const basePathProxy = createBasePathProxy({ server, config }); - - await basePathProxy.configure({ - shouldRedirectFromOldBasePath: path => { - const isApp = path.startsWith('app/'); - const isKnownShortPath = ['login', 'logout', 'status'].includes(path); - - return isApp || isKnownShortPath; - }, - - blockUntil: () => { - // Wait until `serverWorker either crashes or starts to listen. - // The `serverWorker` property should be set by the ClusterManager - // once it creates the worker. - const serverWorker = basePathProxy.serverWorker; - if (serverWorker.listening || serverWorker.crashed) { - return Promise.resolve(); - } - - return new Promise(resolve => { - const done = () => { - serverWorker.removeListener('listening', done); - serverWorker.removeListener('crashed', done); - - resolve(); - }; - - serverWorker.on('listening', done); - serverWorker.on('crashed', done); - }); - }, - }); - - return basePathProxy; -} diff --git a/src/cli/cluster/configure_base_path_proxy.test.js b/src/cli/cluster/configure_base_path_proxy.test.js deleted file mode 100644 index 01cbaf0bcc900..0000000000000 --- a/src/cli/cluster/configure_base_path_proxy.test.js +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -jest.mock('../../core', () => ({ - createBasePathProxy: jest.fn(), -})); - -jest.mock('../../server/logging', () => ({ - setupLogging: jest.fn(), -})); - -import { Server } from 'hapi'; -import { createBasePathProxy as createBasePathProxyMock } from '../../core'; -import { setupLogging as setupLoggingMock } from '../../server/logging'; -import { configureBasePathProxy } from './configure_base_path_proxy'; - -describe('configureBasePathProxy()', () => { - it('returns `BasePathProxy` instance.', async () => { - const basePathProxyMock = { configure: jest.fn() }; - createBasePathProxyMock.mockReturnValue(basePathProxyMock); - - const basePathProxy = await configureBasePathProxy({}); - - expect(basePathProxy).toBe(basePathProxyMock); - }); - - it('correctly configures `BasePathProxy`.', async () => { - const configMock = {}; - const basePathProxyMock = { configure: jest.fn() }; - createBasePathProxyMock.mockReturnValue(basePathProxyMock); - - await configureBasePathProxy(configMock); - - // Check that logging is configured with the right parameters. - expect(setupLoggingMock).toHaveBeenCalledWith( - expect.any(Server), - configMock - ); - - const [[server]] = setupLoggingMock.mock.calls; - expect(createBasePathProxyMock).toHaveBeenCalledWith({ - config: configMock, - server, - }); - - expect(basePathProxyMock.configure).toHaveBeenCalledWith({ - shouldRedirectFromOldBasePath: expect.any(Function), - blockUntil: expect.any(Function), - }); - }); - - describe('configured with the correct `shouldRedirectFromOldBasePath` and `blockUntil` functions.', async () => { - let serverWorkerMock; - let shouldRedirectFromOldBasePath; - let blockUntil; - beforeEach(async () => { - serverWorkerMock = { - listening: false, - crashed: false, - on: jest.fn(), - removeListener: jest.fn(), - }; - - const basePathProxyMock = { - configure: jest.fn(), - serverWorker: serverWorkerMock, - }; - - createBasePathProxyMock.mockReturnValue(basePathProxyMock); - - await configureBasePathProxy({}); - - [[{ blockUntil, shouldRedirectFromOldBasePath }]] = basePathProxyMock.configure.mock.calls; - }); - - it('`shouldRedirectFromOldBasePath()` returns `false` for unknown paths.', async () => { - expect(shouldRedirectFromOldBasePath('')).toBe(false); - expect(shouldRedirectFromOldBasePath('some-path/')).toBe(false); - expect(shouldRedirectFromOldBasePath('some-other-path')).toBe(false); - }); - - it('`shouldRedirectFromOldBasePath()` returns `true` for `app` and other known paths.', async () => { - expect(shouldRedirectFromOldBasePath('app/')).toBe(true); - expect(shouldRedirectFromOldBasePath('login')).toBe(true); - expect(shouldRedirectFromOldBasePath('logout')).toBe(true); - expect(shouldRedirectFromOldBasePath('status')).toBe(true); - }); - - it('`blockUntil()` resolves immediately if worker has already crashed.', async () => { - serverWorkerMock.crashed = true; - - await expect(blockUntil()).resolves.not.toBeDefined(); - expect(serverWorkerMock.on).not.toHaveBeenCalled(); - expect(serverWorkerMock.removeListener).not.toHaveBeenCalled(); - }); - - it('`blockUntil()` resolves immediately if worker is already listening.', async () => { - serverWorkerMock.listening = true; - - await expect(blockUntil()).resolves.not.toBeDefined(); - expect(serverWorkerMock.on).not.toHaveBeenCalled(); - expect(serverWorkerMock.removeListener).not.toHaveBeenCalled(); - }); - - it('`blockUntil()` resolves when worker crashes.', async () => { - const blockUntilPromise = blockUntil(); - - expect(serverWorkerMock.on).toHaveBeenCalledTimes(2); - expect(serverWorkerMock.on).toHaveBeenCalledWith( - 'crashed', - expect.any(Function) - ); - - const [, [eventName, onCrashed]] = serverWorkerMock.on.mock.calls; - // Check event name to make sure we call the right callback, - // in Jest 23 we could use `toHaveBeenNthCalledWith` instead. - expect(eventName).toBe('crashed'); - expect(serverWorkerMock.removeListener).not.toHaveBeenCalled(); - - onCrashed(); - await expect(blockUntilPromise).resolves.not.toBeDefined(); - - expect(serverWorkerMock.removeListener).toHaveBeenCalledTimes(2); - }); - - it('`blockUntil()` resolves when worker starts listening.', async () => { - const blockUntilPromise = blockUntil(); - - expect(serverWorkerMock.on).toHaveBeenCalledTimes(2); - expect(serverWorkerMock.on).toHaveBeenCalledWith( - 'listening', - expect.any(Function) - ); - - const [[eventName, onListening]] = serverWorkerMock.on.mock.calls; - // Check event name to make sure we call the right callback, - // in Jest 23 we could use `toHaveBeenNthCalledWith` instead. - expect(eventName).toBe('listening'); - expect(serverWorkerMock.removeListener).not.toHaveBeenCalled(); - - onListening(); - await expect(blockUntilPromise).resolves.not.toBeDefined(); - - expect(serverWorkerMock.removeListener).toHaveBeenCalledTimes(2); - }); - }); -}); diff --git a/src/cli/cluster/worker.test.js b/src/cli/cluster/worker.test.js index c166956bcbf34..24687d640438a 100644 --- a/src/cli/cluster/worker.test.js +++ b/src/cli/cluster/worker.test.js @@ -17,26 +17,25 @@ * under the License. */ -import sinon from 'sinon'; +import { mockCluster } from './__mocks__/cluster'; +jest.mock('cluster', () => mockCluster()); + import cluster from 'cluster'; -import { findIndex } from 'lodash'; -import MockClusterFork from './_mock_cluster_fork'; import Worker from './worker'; import Log from '../log'; const workersToShutdown = []; function assertListenerAdded(emitter, event) { - sinon.assert.calledWith(emitter.on, event); + expect(emitter.on).toHaveBeenCalledWith(event, expect.any(Function)); } function assertListenerRemoved(emitter, event) { - sinon.assert.calledWith( - emitter.removeListener, - event, - emitter.on.args[findIndex(emitter.on.args, { 0: event })][1] - ); + const [, onEventListener] = emitter.on.mock.calls.find(([eventName]) => { + return eventName === event; + }); + expect(emitter.removeListener).toHaveBeenCalledWith(event, onEventListener); } function setup(opts = {}) { @@ -50,81 +49,82 @@ function setup(opts = {}) { return worker; } -describe('CLI cluster manager', function () { - const sandbox = sinon.createSandbox(); - - beforeEach(function () { - sandbox.stub(cluster, 'fork').callsFake(() => new MockClusterFork()); - }); - - afterEach(async function () { - sandbox.restore(); +describe('CLI cluster manager', () => { + afterEach(async () => { + while(workersToShutdown.length > 0) { + const worker = workersToShutdown.pop(); + // If `fork` exists we should set `exitCode` to the non-zero value to + // prevent worker from auto restart. + if (worker.fork) { + worker.fork.exitCode = 1; + } - for (const worker of workersToShutdown) { await worker.shutdown(); } + + cluster.fork.mockClear(); }); - describe('#onChange', function () { - describe('opts.watch = true', function () { - it('restarts the fork', function () { + describe('#onChange', () => { + describe('opts.watch = true', () => { + test('restarts the fork', () => { const worker = setup({ watch: true }); - sinon.stub(worker, 'start'); + jest.spyOn(worker, 'start').mockImplementation(() => {}); worker.onChange('/some/path'); expect(worker.changes).toEqual(['/some/path']); - sinon.assert.calledOnce(worker.start); + expect(worker.start).toHaveBeenCalledTimes(1); }); }); - describe('opts.watch = false', function () { - it('does not restart the fork', function () { + describe('opts.watch = false', () => { + test('does not restart the fork', () => { const worker = setup({ watch: false }); - sinon.stub(worker, 'start'); + jest.spyOn(worker, 'start').mockImplementation(() => {}); worker.onChange('/some/path'); expect(worker.changes).toEqual([]); - sinon.assert.notCalled(worker.start); + expect(worker.start).not.toHaveBeenCalled(); }); }); }); - describe('#shutdown', function () { - describe('after starting()', function () { - it('kills the worker and unbinds from message, online, and disconnect events', async function () { + describe('#shutdown', () => { + describe('after starting()', () => { + test('kills the worker and unbinds from message, online, and disconnect events', async () => { const worker = setup(); await worker.start(); expect(worker).toHaveProperty('online', true); const fork = worker.fork; - sinon.assert.notCalled(fork.process.kill); + expect(fork.process.kill).not.toHaveBeenCalled(); assertListenerAdded(fork, 'message'); assertListenerAdded(fork, 'online'); assertListenerAdded(fork, 'disconnect'); worker.shutdown(); - sinon.assert.calledOnce(fork.process.kill); + expect(fork.process.kill).toHaveBeenCalledTimes(1); assertListenerRemoved(fork, 'message'); assertListenerRemoved(fork, 'online'); assertListenerRemoved(fork, 'disconnect'); }); }); - describe('before being started', function () { - it('does nothing', function () { + describe('before being started', () => { + test('does nothing', () => { const worker = setup(); worker.shutdown(); }); }); }); - describe('#parseIncomingMessage()', function () { - describe('on a started worker', function () { - it(`is bound to fork's message event`, async function () { + describe('#parseIncomingMessage()', () => { + describe('on a started worker', () => { + test(`is bound to fork's message event`, async () => { const worker = setup(); await worker.start(); - sinon.assert.calledWith(worker.fork.on, 'message'); + expect(worker.fork.on).toHaveBeenCalledWith('message', expect.any(Function)); }); }); - describe('do after', function () { - it('ignores non-array messages', function () { + describe('do after', () => { + test('ignores non-array messages', () => { const worker = setup(); worker.parseIncomingMessage('some string thing'); worker.parseIncomingMessage(0); @@ -134,39 +134,39 @@ describe('CLI cluster manager', function () { worker.parseIncomingMessage(/weird/); }); - it('calls #onMessage with message parts', function () { + test('calls #onMessage with message parts', () => { const worker = setup(); - const stub = sinon.stub(worker, 'onMessage'); + jest.spyOn(worker, 'onMessage').mockImplementation(() => {}); worker.parseIncomingMessage([10, 100, 1000, 10000]); - sinon.assert.calledWith(stub, 10, 100, 1000, 10000); + expect(worker.onMessage).toHaveBeenCalledWith(10, 100, 1000, 10000); }); }); }); - describe('#onMessage', function () { - describe('when sent WORKER_BROADCAST message', function () { - it('emits the data to be broadcasted', function () { + describe('#onMessage', () => { + describe('when sent WORKER_BROADCAST message', () => { + test('emits the data to be broadcasted', () => { const worker = setup(); const data = {}; - const stub = sinon.stub(worker, 'emit'); + jest.spyOn(worker, 'emit').mockImplementation(() => {}); worker.onMessage('WORKER_BROADCAST', data); - sinon.assert.calledWithExactly(stub, 'broadcast', data); + expect(worker.emit).toHaveBeenCalledWith('broadcast', data); }); }); - describe('when sent WORKER_LISTENING message', function () { - it('sets the listening flag and emits the listening event', function () { + describe('when sent WORKER_LISTENING message', () => { + test('sets the listening flag and emits the listening event', () => { const worker = setup(); - const stub = sinon.stub(worker, 'emit'); + jest.spyOn(worker, 'emit').mockImplementation(() => {}); expect(worker).toHaveProperty('listening', false); worker.onMessage('WORKER_LISTENING'); expect(worker).toHaveProperty('listening', true); - sinon.assert.calledWithExactly(stub, 'listening'); + expect(worker.emit).toHaveBeenCalledWith('listening'); }); }); - describe('when passed an unknown message', function () { - it('does nothing', function () { + describe('when passed an unknown message', () => { + test('does nothing', () => { const worker = setup(); worker.onMessage('asdlfkajsdfahsdfiohuasdofihsdoif'); worker.onMessage({}); @@ -175,46 +175,46 @@ describe('CLI cluster manager', function () { }); }); - describe('#start', function () { - describe('when not started', function () { - // TODO This test is flaky, see https://github.com/elastic/kibana/issues/15888 - it.skip('creates a fork and waits for it to come online', async function () { + describe('#start', () => { + describe('when not started', () => { + test('creates a fork and waits for it to come online', async () => { const worker = setup(); - sinon.spy(worker, 'on'); + jest.spyOn(worker, 'on'); await worker.start(); - sinon.assert.calledOnce(cluster.fork); - sinon.assert.calledWith(worker.on, 'fork:online'); + expect(cluster.fork).toHaveBeenCalledTimes(1); + expect(worker.on).toHaveBeenCalledWith('fork:online', expect.any(Function)); }); - // TODO This test is flaky, see https://github.com/elastic/kibana/issues/15888 - it.skip('listens for cluster and process "exit" events', async function () { + test('listens for cluster and process "exit" events', async () => { const worker = setup(); - sinon.spy(process, 'on'); - sinon.spy(cluster, 'on'); + jest.spyOn(process, 'on'); + jest.spyOn(cluster, 'on'); await worker.start(); - sinon.assert.calledOnce(cluster.on); - sinon.assert.calledWith(cluster.on, 'exit'); - sinon.assert.calledOnce(process.on); - sinon.assert.calledWith(process.on, 'exit'); + expect(cluster.on).toHaveBeenCalledTimes(1); + expect(cluster.on).toHaveBeenCalledWith('exit', expect.any(Function)); + expect(process.on).toHaveBeenCalledTimes(1); + expect(process.on).toHaveBeenCalledWith('exit', expect.any(Function)); }); }); - describe('when already started', function () { - it('calls shutdown and waits for the graceful shutdown to cause a restart', async function () { + describe('when already started', () => { + test('calls shutdown and waits for the graceful shutdown to cause a restart', async () => { const worker = setup(); await worker.start(); - sinon.spy(worker, 'shutdown'); - sinon.spy(worker, 'on'); + + jest.spyOn(worker, 'shutdown'); + jest.spyOn(worker, 'on'); worker.start(); - sinon.assert.calledOnce(worker.shutdown); - sinon.assert.calledWith(worker.on, 'online'); + + expect(worker.shutdown).toHaveBeenCalledTimes(1); + expect(worker.on).toHaveBeenCalledWith('online', expect.any(Function)); }); }); }); diff --git a/src/cli/color.js b/src/cli/color.js index b678376ef7c24..a02fb551c4181 100644 --- a/src/cli/color.js +++ b/src/cli/color.js @@ -17,9 +17,8 @@ * under the License. */ -import _ from 'lodash'; import chalk from 'chalk'; -export const green = _.flow(chalk.black, chalk.bgGreen); -export const red = _.flow(chalk.white, chalk.bgRed); -export const yellow = _.flow(chalk.black, chalk.bgYellow); +export const green = chalk.black.bgGreen; +export const red = chalk.white.bgRed; +export const yellow = chalk.black.bgYellow; diff --git a/src/cli/serve/integration_tests/__snapshots__/invalid_config.test.js.snap b/src/cli/serve/integration_tests/__snapshots__/invalid_config.test.js.snap index 0e702ed6123bd..47b98f740af58 100644 --- a/src/cli/serve/integration_tests/__snapshots__/invalid_config.test.js.snap +++ b/src/cli/serve/integration_tests/__snapshots__/invalid_config.test.js.snap @@ -4,12 +4,15 @@ exports[`cli invalid config support exits with statusCode 64 and logs a single l Array [ Object { "@timestamp": "## @timestamp ##", + "error": "## Error with stack trace ##", + "level": "fatal", "message": "\\"unknown.key\\", \\"other.unknown.key\\", \\"other.third\\", \\"some.flat.key\\", and \\"some.array\\" settings were not applied. Check for spelling errors and ensure that expected plugins are installed.", "pid": "## PID ##", "tags": Array [ "fatal", + "root", ], - "type": "log", + "type": "error", }, ] `; diff --git a/src/cli/serve/integration_tests/invalid_config.test.js b/src/cli/serve/integration_tests/invalid_config.test.js index 335fb1dbcaf9f..495bfbeaa939e 100644 --- a/src/cli/serve/integration_tests/invalid_config.test.js +++ b/src/cli/serve/integration_tests/invalid_config.test.js @@ -39,7 +39,8 @@ describe('cli invalid config support', function () { .map(obj => ({ ...obj, pid: '## PID ##', - '@timestamp': '## @timestamp ##' + '@timestamp': '## @timestamp ##', + error: '## Error with stack trace ##', })); expect(error).toBe(undefined); diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index 08495566d845e..2820ac6a64ea4 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -19,20 +19,15 @@ import _ from 'lodash'; import { statSync, lstatSync, realpathSync } from 'fs'; -import { isWorker } from 'cluster'; import { resolve } from 'path'; import { fromRoot } from '../../utils'; import { getConfig } from '../../server/path'; -import { Config } from '../../server/config/config'; -import { getConfigFromFiles } from '../../core/server/config'; +import { bootstrap } from '../../core/server'; import { readKeystore } from './read_keystore'; -import { transformDeprecations } from '../../server/config/transform_deprecations'; import { DEV_SSL_CERT_PATH, DEV_SSL_KEY_PATH } from '../dev_ssl'; -const { startRepl } = canRequire('../repl') ? require('../repl') : { }; - function canRequire(path) { try { require.resolve(path); @@ -60,6 +55,9 @@ function isSymlinkTo(link, dest) { const CLUSTER_MANAGER_PATH = resolve(__dirname, '../cluster/cluster_manager'); const CAN_CLUSTER = canRequire(CLUSTER_MANAGER_PATH); +const REPL_PATH = resolve(__dirname, '../repl'); +const CAN_REPL = canRequire(REPL_PATH); + // xpack is installed in both dev and the distributable, it's optional if // install is a link to the source, not an actual install const XPACK_INSTALLED_DIR = resolve(__dirname, '../../../node_modules/x-pack'); @@ -79,12 +77,11 @@ const configPathCollector = pathCollector(); const pluginDirCollector = pathCollector(); const pluginPathCollector = pathCollector(); -function readServerSettings(opts, extraCliOptions) { - const settings = getConfigFromFiles([].concat(opts.config || [])); - const set = _.partial(_.set, settings); - const get = _.partial(_.get, settings); - const has = _.partial(_.has, settings); - const merge = _.partial(_.merge, settings); +function applyConfigOverrides(rawConfig, opts, extraCliOptions) { + const set = _.partial(_.set, rawConfig); + const get = _.partial(_.get, rawConfig); + const has = _.partial(_.has, rawConfig); + const merge = _.partial(_.merge, rawConfig); if (opts.dev) { set('env', 'development'); @@ -133,7 +130,7 @@ function readServerSettings(opts, extraCliOptions) { merge(extraCliOptions); merge(readKeystore(get('path.data'))); - return settings; + return rawConfig; } export default function (program) { @@ -175,7 +172,7 @@ export default function (program) { ) .option('--plugins ', 'an alias for --plugin-dir', pluginDirCollector); - if (!!startRepl) { + if (CAN_REPL) { command.option('--repl', 'Run the server with a REPL prompt and access to the server object'); } @@ -205,81 +202,25 @@ export default function (program) { } } - const getCurrentSettings = () => readServerSettings(opts, this.getUnknownOptions()); - const settings = getCurrentSettings(); - - if (CAN_CLUSTER && opts.dev && !isWorker) { - // stop processing the action and handoff to cluster manager - const ClusterManager = require(CLUSTER_MANAGER_PATH); - await ClusterManager.create(opts, settings); - return; - } - - let kbnServer = {}; - const KbnServer = require('../../server/kbn_server'); - try { - kbnServer = new KbnServer(settings); - if (shouldStartRepl(opts)) { - startRepl(kbnServer); - } - await kbnServer.ready(); - } catch (error) { - const { server } = kbnServer; - - switch (error.code) { - case 'EADDRINUSE': - logFatal(`Port ${error.port} is already in use. Another instance of Kibana may be running!`, server); - break; - - case 'InvalidConfig': - logFatal(error.message, server); - break; - - default: - logFatal(error, server); - break; - } - - kbnServer.close(); - const exitCode = error.processExitCode == null ? 1 : error.processExitCode; - // eslint-disable-next-line no-process-exit - process.exit(exitCode); - } - - process.on('SIGHUP', async function reloadConfig() { - const settings = transformDeprecations(getCurrentSettings()); - const config = new Config(kbnServer.config.getSchema(), settings); - - kbnServer.server.log(['info', 'config'], 'Reloading logging configuration due to SIGHUP.'); - await kbnServer.applyLoggingConfiguration(config); - kbnServer.server.log(['info', 'config'], 'Reloaded logging configuration due to SIGHUP.'); - - // If new platform config subscription is active, let's notify it with the updated config. - if (kbnServer.newPlatform) { - kbnServer.newPlatform.updateConfig(config.get()); - } + const unknownOptions = this.getUnknownOptions(); + await bootstrap({ + configs: [].concat(opts.config || []), + cliArgs: { + dev: !!opts.dev, + envName: unknownOptions.env ? unknownOptions.env.name : undefined, + quiet: !!opts.quiet, + silent: !!opts.silent, + watch: !!opts.watch, + repl: !!opts.repl, + basePath: !!opts.basePath, + }, + features: { + isClusterModeSupported: CAN_CLUSTER, + isOssModeSupported: XPACK_OPTIONAL, + isXPackInstalled: XPACK_INSTALLED, + isReplModeSupported: CAN_REPL, + }, + applyConfigOverrides: rawConfig => applyConfigOverrides(rawConfig, opts, unknownOptions), }); - - return kbnServer; }); } - -function shouldStartRepl(opts) { - if (opts.repl && !startRepl) { - throw new Error('Kibana REPL mode can only be run in development mode.'); - } - - // The kbnWorkerType check is necessary to prevent the repl - // from being started multiple times in different processes. - // We only want one REPL. - return opts.repl && process.env.kbnWorkerType === 'server'; -} - -function logFatal(message, server) { - if (server) { - server.log(['fatal'], message); - } - - // It's possible for the Hapi logger to not be setup - console.error('FATAL', message); -} diff --git a/src/core/README.md b/src/core/README.md index c3b056f981726..196946ed9e4a3 100644 --- a/src/core/README.md +++ b/src/core/README.md @@ -5,26 +5,17 @@ Core is a set of systems (frontend, backend etc.) that Kibana and its plugins ar ## Integration with the "legacy" Kibana Most of the existing core functionality is still spread over "legacy" Kibana and it will take some time to upgrade it. -Kibana is still started using existing "legacy" CLI and bootstraps `core` only when needed. At the moment `core` manages -HTTP connections, handles TLS configuration and base path proxy. All requests to Kibana server will hit HTTP server -exposed by the `core` first and it will decide whether request can be solely handled by the new platform or request should -be proxied to the "legacy" Kibana. This setup allows `core` to gradually introduce any "pre-route" processing -logic, expose new routes or replace old ones handled by the "legacy" Kibana currently. +Kibana is started using existing "legacy" CLI that bootstraps `core` which in turn creates the "legacy" Kibana server. +At the moment `core` manages HTTP connections, handles TLS configuration and base path proxy. All requests to Kibana server +will hit HTTP server exposed by the `core` first and it will decide whether request can be solely handled by the new +platform or request should be proxied to the "legacy" Kibana. This setup allows `core` to gradually introduce any "pre-route" +processing logic, expose new routes or replace old ones handled by the "legacy" Kibana currently. -Once config has been loaded and validated by the "legacy" Kibana it's passed to the `core` where some of its parts will -be additionally validated so that we can make config validation stricter with the new config validation system. Even though -the new validation system provided by the `core` is also based on Joi internally it is complemented with custom rules -tailored to our needs (e.g. `byteSize`, `duration` etc.). That means that config values that are accepted by the "legacy" -Kibana may be rejected by the `core`. - -One can also define new configuration keys under `__newPlatform` if these keys are supposed to be used by the `core` only -and should not be validated by the "legacy" Kibana, e.g. - -```yaml -__newPlatform: - plugins: - scanDirs: ['./example_plugins'] -``` +Once config has been loaded and some of its parts were validated by the `core` it's passed to the "legacy" Kibana where +it will be additionally validated so that we can make config validation stricter with the new config validation system. +Even though the new validation system provided by the `core` is also based on Joi internally it is complemented with custom +rules tailored to our needs (e.g. `byteSize`, `duration` etc.). That means that config values that were previously accepted +by the "legacy" Kibana may be rejected by the `core` now. Even though `core` has its own logging system it doesn't output log records directly (e.g. to file or terminal), but instead forward them to the "legacy" Kibana so that they look the same as the rest of the log records throughout Kibana. diff --git a/src/core/public/base_path/base_path_service.test.ts b/src/core/public/base_path/base_path_service.test.ts new file mode 100644 index 0000000000000..ed44c322f158c --- /dev/null +++ b/src/core/public/base_path/base_path_service.test.ts @@ -0,0 +1,104 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { BasePathService } from './base_path_service'; + +function setup(options: any = {}) { + const injectedBasePath: string = + options.injectedBasePath === undefined ? '/foo/bar' : options.injectedBasePath; + + const service = new BasePathService(); + + const injectedMetadata = { + getBasePath: jest.fn().mockReturnValue(injectedBasePath), + } as any; + + const startContract = service.start({ + injectedMetadata, + }); + + return { + service, + startContract, + injectedBasePath, + }; +} + +describe('startContract.get()', () => { + it('returns an empty string if no basePath is injected', () => { + const { startContract } = setup({ injectedBasePath: null }); + expect(startContract.get()).toBe(''); + }); + + it('returns the injected basePath', () => { + const { startContract } = setup(); + expect(startContract.get()).toBe('/foo/bar'); + }); +}); + +describe('startContract.addToPath()', () => { + it('adds the base path to the path if it is relative and starts with a slash', () => { + const { startContract } = setup(); + expect(startContract.addToPath('/a/b')).toBe('/foo/bar/a/b'); + }); + + it('leaves the query string and hash of path unchanged', () => { + const { startContract } = setup(); + expect(startContract.addToPath('/a/b?x=y#c/d/e')).toBe('/foo/bar/a/b?x=y#c/d/e'); + }); + + it('returns the path unchanged if it does not start with a slash', () => { + const { startContract } = setup(); + expect(startContract.addToPath('a/b')).toBe('a/b'); + }); + + it('returns the path unchanged it it has a hostname', () => { + const { startContract } = setup(); + expect(startContract.addToPath('http://localhost:5601/a/b')).toBe('http://localhost:5601/a/b'); + }); +}); + +describe('startContract.removeFromPath()', () => { + it('removes the basePath if relative path starts with it', () => { + const { startContract } = setup(); + expect(startContract.removeFromPath('/foo/bar/a/b')).toBe('/a/b'); + }); + + it('leaves query string and hash intact', () => { + const { startContract } = setup(); + expect(startContract.removeFromPath('/foo/bar/a/b?c=y#1234')).toBe('/a/b?c=y#1234'); + }); + + it('ignores urls with hostnames', () => { + const { startContract } = setup(); + expect(startContract.removeFromPath('http://localhost:5601/foo/bar/a/b')).toBe( + 'http://localhost:5601/foo/bar/a/b' + ); + }); + + it('returns slash if path is just basePath', () => { + const { startContract } = setup(); + expect(startContract.removeFromPath('/foo/bar')).toBe('/'); + }); + + it('returns full path if basePath is not its own segment', () => { + const { startContract } = setup(); + expect(startContract.removeFromPath('/foo/barhop')).toBe('/foo/barhop'); + }); +}); diff --git a/src/core/public/base_path/base_path_service.ts b/src/core/public/base_path/base_path_service.ts new file mode 100644 index 0000000000000..bd6f665abdf9e --- /dev/null +++ b/src/core/public/base_path/base_path_service.ts @@ -0,0 +1,74 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { InjectedMetadataStartContract } from '../injected_metadata'; +import { modifyUrl } from '../utils'; + +interface Deps { + injectedMetadata: InjectedMetadataStartContract; +} + +export class BasePathService { + public start({ injectedMetadata }: Deps) { + const basePath = injectedMetadata.getBasePath() || ''; + + return { + /** + * Get the current basePath as defined by the server + */ + get() { + return basePath; + }, + + /** + * Add the current basePath to a path string. + * @param path A relative url including the leading `/`, otherwise it will be returned without modification + */ + addToPath(path: string) { + return modifyUrl(path, parts => { + if (!parts.hostname && parts.pathname && parts.pathname.startsWith('/')) { + parts.pathname = `${basePath}${parts.pathname}`; + } + }); + }, + + /** + * Remove the basePath from a path that starts with it + * @param path A relative url that starts with the basePath, which will be stripped + */ + removeFromPath(path: string) { + if (!basePath) { + return path; + } + + if (path === basePath) { + return '/'; + } + + if (path.startsWith(basePath + '/')) { + return path.slice(basePath.length); + } + + return path; + }, + }; + } +} + +export type BasePathStartContract = ReturnType; diff --git a/src/core/index.ts b/src/core/public/base_path/index.ts similarity index 90% rename from src/core/index.ts rename to src/core/public/base_path/index.ts index 326d08e0ec43f..13ff2350cab84 100644 --- a/src/core/index.ts +++ b/src/core/public/base_path/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { injectIntoKbnServer, createBasePathProxy } from './server/legacy_compat'; +export { BasePathService, BasePathStartContract } from './base_path_service'; diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts index d867c8f49b6a0..64f71cd4fb00c 100644 --- a/src/core/public/core_system.test.ts +++ b/src/core/public/core_system.test.ts @@ -17,11 +17,13 @@ * under the License. */ +import { BasePathService } from './base_path'; import { FatalErrorsService } from './fatal_errors'; import { InjectedMetadataService } from './injected_metadata'; import { LegacyPlatformService } from './legacy_platform'; import { LoadingCountService } from './loading_count'; import { NotificationsService } from './notifications'; +import { UiSettingsService } from './ui_settings'; const MockLegacyPlatformService = jest.fn( function _MockLegacyPlatformService(this: any) { @@ -76,6 +78,24 @@ jest.mock('./loading_count', () => ({ LoadingCountService: MockLoadingCountService, })); +const mockBasePathStartContract = {}; +const MockBasePathService = jest.fn(function _MockNotificationsService(this: any) { + this.start = jest.fn().mockReturnValue(mockBasePathStartContract); +}); +jest.mock('./base_path', () => ({ + BasePathService: MockBasePathService, +})); + +const mockUiSettingsContract = {}; +const MockUiSettingsService = jest.fn(function _MockNotificationsService( + this: any +) { + this.start = jest.fn().mockReturnValue(mockUiSettingsContract); +}); +jest.mock('./ui_settings', () => ({ + UiSettingsService: MockUiSettingsService, +})); + import { CoreSystem } from './core_system'; jest.spyOn(CoreSystem.prototype, 'stop'); @@ -101,6 +121,8 @@ describe('constructor', () => { expect(MockFatalErrorsService).toHaveBeenCalledTimes(1); expect(MockNotificationsService).toHaveBeenCalledTimes(1); expect(MockLoadingCountService).toHaveBeenCalledTimes(1); + expect(MockBasePathService).toHaveBeenCalledTimes(1); + expect(MockUiSettingsService).toHaveBeenCalledTimes(1); }); it('passes injectedMetadata param to InjectedMetadataService', () => { @@ -221,6 +243,27 @@ describe('#start()', () => { }); }); + it('calls basePath#start()', () => { + startCore(); + const [mockInstance] = MockBasePathService.mock.instances; + expect(mockInstance.start).toHaveBeenCalledTimes(1); + expect(mockInstance.start).toHaveBeenCalledWith({ + injectedMetadata: mockInjectedMetadataStartContract, + }); + }); + + it('calls uiSettings#start()', () => { + startCore(); + const [mockInstance] = MockUiSettingsService.mock.instances; + expect(mockInstance.start).toHaveBeenCalledTimes(1); + expect(mockInstance.start).toHaveBeenCalledWith({ + notifications: mockNotificationStartContract, + loadingCount: mockLoadingCountContract, + injectedMetadata: mockInjectedMetadataStartContract, + basePath: mockBasePathStartContract, + }); + }); + it('calls fatalErrors#start()', () => { startCore(); const [mockInstance] = MockFatalErrorsService.mock.instances; diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index db5500cb2ffdb..05c00e5a633aa 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -18,11 +18,14 @@ */ import './core.css'; + +import { BasePathService } from './base_path'; import { FatalErrorsService } from './fatal_errors'; import { InjectedMetadataParams, InjectedMetadataService } from './injected_metadata'; import { LegacyPlatformParams, LegacyPlatformService } from './legacy_platform'; import { LoadingCountService } from './loading_count'; import { NotificationsService } from './notifications'; +import { UiSettingsService } from './ui_settings'; interface Params { rootDomElement: HTMLElement; @@ -43,6 +46,8 @@ export class CoreSystem { private readonly legacyPlatform: LegacyPlatformService; private readonly notifications: NotificationsService; private readonly loadingCount: LoadingCountService; + private readonly uiSettings: UiSettingsService; + private readonly basePath: BasePathService; private readonly rootDomElement: HTMLElement; private readonly notificationsTargetDomElement: HTMLDivElement; @@ -71,6 +76,8 @@ export class CoreSystem { }); this.loadingCount = new LoadingCountService(); + this.basePath = new BasePathService(); + this.uiSettings = new UiSettingsService(); this.legacyPlatformTargetDomElement = document.createElement('div'); this.legacyPlatform = new LegacyPlatformService({ @@ -92,7 +99,21 @@ export class CoreSystem { const injectedMetadata = this.injectedMetadata.start(); const fatalErrors = this.fatalErrors.start(); const loadingCount = this.loadingCount.start({ fatalErrors }); - this.legacyPlatform.start({ injectedMetadata, fatalErrors, notifications, loadingCount }); + const basePath = this.basePath.start({ injectedMetadata }); + const uiSettings = this.uiSettings.start({ + notifications, + loadingCount, + injectedMetadata, + basePath, + }); + this.legacyPlatform.start({ + injectedMetadata, + fatalErrors, + notifications, + loadingCount, + basePath, + uiSettings, + }); } catch (error) { this.fatalErrors.add(error); } diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index e756d99b1f854..85c3fce0ba9de 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -23,6 +23,7 @@ export interface InjectedMetadataParams { injectedMetadata: { version: string; buildNumber: number; + basePath: string; legacyMetadata: { [key: string]: any; }; @@ -42,6 +43,14 @@ export class InjectedMetadataService { public start() { return { + getBasePath: () => { + return this.state.basePath; + }, + + getKibanaVersion: () => { + return this.getKibanaVersion(); + }, + getLegacyMetadata: () => { return this.state.legacyMetadata; }, diff --git a/src/core/public/legacy_platform/__snapshots__/legacy_platform_service.test.ts.snap b/src/core/public/legacy_platform/__snapshots__/legacy_platform_service.test.ts.snap index e012b43d5977a..8d318e8e57673 100644 --- a/src/core/public/legacy_platform/__snapshots__/legacy_platform_service.test.ts.snap +++ b/src/core/public/legacy_platform/__snapshots__/legacy_platform_service.test.ts.snap @@ -1,5 +1,31 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`#start() load order useLegacyTestHarness = false loads ui/modules before ui/chrome, and both before legacy files 1`] = ` +Array [ + "ui/metadata", + "ui/notify/fatal_error", + "ui/notify/toasts", + "ui/chrome/api/loading_count", + "ui/chrome/api/base_path", + "ui/chrome/api/ui_settings", + "ui/chrome", + "legacy files", +] +`; + +exports[`#start() load order useLegacyTestHarness = true loads ui/modules before ui/test_harness, and both before legacy files 1`] = ` +Array [ + "ui/metadata", + "ui/notify/fatal_error", + "ui/notify/toasts", + "ui/chrome/api/loading_count", + "ui/chrome/api/base_path", + "ui/chrome/api/ui_settings", + "ui/test_harness", + "legacy files", +] +`; + exports[`#stop() destroys the angular scope and empties the targetDomElement if angular is bootstraped to targetDomElement 1`] = `
{ }; }); +const mockBasePathInit = jest.fn(); +jest.mock('ui/chrome/api/base_path', () => { + mockLoadOrder.push('ui/chrome/api/base_path'); + return { + __newPlatformInit__: mockBasePathInit, + }; +}); + +const mockUiSettingsInit = jest.fn(); +jest.mock('ui/chrome/api/ui_settings', () => { + mockLoadOrder.push('ui/chrome/api/ui_settings'); + return { + __newPlatformInit__: mockUiSettingsInit, + }; +}); + import { LegacyPlatformService } from './legacy_platform_service'; const fatalErrorsStartContract = {} as any; @@ -77,7 +93,8 @@ const notificationsStartContract = { toasts: {}, } as any; -const injectedMetadataStartContract = { +const injectedMetadataStartContract: any = { + getBasePath: jest.fn(), getLegacyMetadata: jest.fn(), }; @@ -86,6 +103,14 @@ const loadingCountStartContract = { getCount$: jest.fn().mockImplementation(() => new Rx.Observable(observer => observer.next(0))), }; +const basePathStartContract = { + get: jest.fn(), + addToPath: jest.fn(), + removeFromPath: jest.fn(), +}; + +const uiSettingsStartContract: any = {}; + const defaultParams = { targetDomElement: document.createElement('div'), requireLegacyFiles: jest.fn(() => { @@ -98,6 +123,8 @@ const defaultStartDeps = { injectedMetadata: injectedMetadataStartContract, notifications: notificationsStartContract, loadingCount: loadingCountStartContract, + basePath: basePathStartContract, + uiSettings: uiSettingsStartContract, }; afterEach(() => { @@ -156,6 +183,28 @@ describe('#start()', () => { expect(mockLoadingCountInit).toHaveBeenCalledWith(loadingCountStartContract); }); + it('passes basePath service to ui/chrome/api/base_path', () => { + const legacyPlatform = new LegacyPlatformService({ + ...defaultParams, + }); + + legacyPlatform.start(defaultStartDeps); + + expect(mockBasePathInit).toHaveBeenCalledTimes(1); + expect(mockBasePathInit).toHaveBeenCalledWith(basePathStartContract); + }); + + it('passes basePath service to ui/chrome/api/ui_settings', () => { + const legacyPlatform = new LegacyPlatformService({ + ...defaultParams, + }); + + legacyPlatform.start(defaultStartDeps); + + expect(mockUiSettingsInit).toHaveBeenCalledTimes(1); + expect(mockUiSettingsInit).toHaveBeenCalledWith(uiSettingsStartContract); + }); + describe('useLegacyTestHarness = false', () => { it('passes the targetDomElement to ui/chrome', () => { const legacyPlatform = new LegacyPlatformService({ @@ -169,6 +218,7 @@ describe('#start()', () => { expect(mockUiChromeBootstrap).toHaveBeenCalledWith(defaultParams.targetDomElement); }); }); + describe('useLegacyTestHarness = true', () => { it('passes the targetDomElement to ui/test_harness', () => { const legacyPlatform = new LegacyPlatformService({ @@ -196,14 +246,7 @@ describe('#start()', () => { legacyPlatform.start(defaultStartDeps); - expect(mockLoadOrder).toEqual([ - 'ui/metadata', - 'ui/notify/fatal_error', - 'ui/notify/toasts', - 'ui/chrome/api/loading_count', - 'ui/chrome', - 'legacy files', - ]); + expect(mockLoadOrder).toMatchSnapshot(); }); }); @@ -218,14 +261,7 @@ describe('#start()', () => { legacyPlatform.start(defaultStartDeps); - expect(mockLoadOrder).toEqual([ - 'ui/metadata', - 'ui/notify/fatal_error', - 'ui/notify/toasts', - 'ui/chrome/api/loading_count', - 'ui/test_harness', - 'legacy files', - ]); + expect(mockLoadOrder).toMatchSnapshot(); }); }); }); diff --git a/src/core/public/legacy_platform/legacy_platform_service.ts b/src/core/public/legacy_platform/legacy_platform_service.ts index 52d2534c8b8e6..45c7cf76c3cb6 100644 --- a/src/core/public/legacy_platform/legacy_platform_service.ts +++ b/src/core/public/legacy_platform/legacy_platform_service.ts @@ -18,16 +18,20 @@ */ import angular from 'angular'; +import { BasePathStartContract } from '../base_path'; import { FatalErrorsStartContract } from '../fatal_errors'; import { InjectedMetadataStartContract } from '../injected_metadata'; import { LoadingCountStartContract } from '../loading_count'; import { NotificationsStartContract } from '../notifications'; +import { UiSettingsClient } from '../ui_settings'; interface Deps { injectedMetadata: InjectedMetadataStartContract; fatalErrors: FatalErrorsStartContract; notifications: NotificationsStartContract; loadingCount: LoadingCountStartContract; + basePath: BasePathStartContract; + uiSettings: UiSettingsClient; } export interface LegacyPlatformParams { @@ -46,13 +50,22 @@ export interface LegacyPlatformParams { export class LegacyPlatformService { constructor(private readonly params: LegacyPlatformParams) {} - public start({ injectedMetadata, fatalErrors, notifications, loadingCount }: Deps) { + public start({ + injectedMetadata, + fatalErrors, + notifications, + loadingCount, + basePath, + uiSettings, + }: Deps) { // 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/metadata').__newPlatformInit__(injectedMetadata.getLegacyMetadata()); require('ui/notify/fatal_error').__newPlatformInit__(fatalErrors); require('ui/notify/toasts').__newPlatformInit__(notifications.toasts); require('ui/chrome/api/loading_count').__newPlatformInit__(loadingCount); + require('ui/chrome/api/base_path').__newPlatformInit__(basePath); + require('ui/chrome/api/ui_settings').__newPlatformInit__(uiSettings); // Load the bootstrap module before loading the legacy platform files so that // the bootstrap module can modify the environment a bit first diff --git a/src/core/public/ui_settings/__snapshots__/ui_settings_api.test.ts.snap b/src/core/public/ui_settings/__snapshots__/ui_settings_api.test.ts.snap new file mode 100644 index 0000000000000..1f69bc37b81cd --- /dev/null +++ b/src/core/public/ui_settings/__snapshots__/ui_settings_api.test.ts.snap @@ -0,0 +1,168 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`#batchSet Buffers are always clear of previously buffered changes: two requests, second only sends bar, not foo 1`] = ` +Object { + "matched": Array [ + Array [ + "/foo/bar/api/kibana/settings", + Object { + "body": "{\\"changes\\":{\\"foo\\":\\"bar\\"}}", + "credentials": "same-origin", + "headers": Object { + "accept": "application/json", + "content-type": "application/json", + "kbn-version": "v9.9.9", + }, + "method": "POST", + }, + ], + Array [ + "/foo/bar/api/kibana/settings", + Object { + "body": "{\\"changes\\":{\\"bar\\":\\"box\\"}}", + "credentials": "same-origin", + "headers": Object { + "accept": "application/json", + "content-type": "application/json", + "kbn-version": "v9.9.9", + }, + "method": "POST", + }, + ], + ], + "unmatched": Array [], +} +`; + +exports[`#batchSet Overwrites previously buffered values with new values for the same key: two requests, foo=d in final 1`] = ` +Object { + "matched": Array [ + Array [ + "/foo/bar/api/kibana/settings", + Object { + "body": "{\\"changes\\":{\\"foo\\":\\"a\\"}}", + "credentials": "same-origin", + "headers": Object { + "accept": "application/json", + "content-type": "application/json", + "kbn-version": "v9.9.9", + }, + "method": "POST", + }, + ], + Array [ + "/foo/bar/api/kibana/settings", + Object { + "body": "{\\"changes\\":{\\"foo\\":\\"d\\"}}", + "credentials": "same-origin", + "headers": Object { + "accept": "application/json", + "content-type": "application/json", + "kbn-version": "v9.9.9", + }, + "method": "POST", + }, + ], + ], + "unmatched": Array [], +} +`; + +exports[`#batchSet buffers changes while first request is in progress, sends buffered changes after first request completes: final, includes both requests 1`] = ` +Object { + "matched": Array [ + Array [ + "/foo/bar/api/kibana/settings", + Object { + "body": "{\\"changes\\":{\\"foo\\":\\"bar\\"}}", + "credentials": "same-origin", + "headers": Object { + "accept": "application/json", + "content-type": "application/json", + "kbn-version": "v9.9.9", + }, + "method": "POST", + }, + ], + Array [ + "/foo/bar/api/kibana/settings", + Object { + "body": "{\\"changes\\":{\\"box\\":\\"bar\\"}}", + "credentials": "same-origin", + "headers": Object { + "accept": "application/json", + "content-type": "application/json", + "kbn-version": "v9.9.9", + }, + "method": "POST", + }, + ], + ], + "unmatched": Array [], +} +`; + +exports[`#batchSet buffers changes while first request is in progress, sends buffered changes after first request completes: initial, only one request 1`] = ` +Object { + "matched": Array [ + Array [ + "/foo/bar/api/kibana/settings", + Object { + "body": "{\\"changes\\":{\\"foo\\":\\"bar\\"}}", + "credentials": "same-origin", + "headers": Object { + "accept": "application/json", + "content-type": "application/json", + "kbn-version": "v9.9.9", + }, + "method": "POST", + }, + ], + ], + "unmatched": Array [], +} +`; + +exports[`#batchSet rejects all promises for batched requests that fail: promise rejections 1`] = ` +Array [ + Object { + "error": [Error: Request failed with status code: 400], + "isRejected": true, + }, + Object { + "error": [Error: Request failed with status code: 400], + "isRejected": true, + }, + Object { + "error": [Error: Request failed with status code: 400], + "isRejected": true, + }, +] +`; + +exports[`#batchSet rejects on 301 1`] = `"Request failed with status code: 301"`; + +exports[`#batchSet rejects on 404 response 1`] = `"Request failed with status code: 404"`; + +exports[`#batchSet rejects on 500 1`] = `"Request failed with status code: 500"`; + +exports[`#batchSet sends a single change immediately: synchronous fetch 1`] = ` +Object { + "matched": Array [ + Array [ + "/foo/bar/api/kibana/settings", + Object { + "body": "{\\"changes\\":{\\"foo\\":\\"bar\\"}}", + "credentials": "same-origin", + "headers": Object { + "accept": "application/json", + "content-type": "application/json", + "kbn-version": "v9.9.9", + }, + "method": "POST", + }, + ], + ], + "unmatched": Array [], +} +`; diff --git a/src/ui/ui_settings/public/__snapshots__/ui_settings_client.test.js.snap b/src/core/public/ui_settings/__snapshots__/ui_settings_client.test.ts.snap similarity index 87% rename from src/ui/ui_settings/public/__snapshots__/ui_settings_client.test.js.snap rename to src/core/public/ui_settings/__snapshots__/ui_settings_client.test.ts.snap index 8915553b36bf1..e49c546f3550c 100644 --- a/src/ui/ui_settings/public/__snapshots__/ui_settings_client.test.js.snap +++ b/src/core/public/ui_settings/__snapshots__/ui_settings_client.test.ts.snap @@ -20,7 +20,29 @@ You can use \`config.get(\\"throwableProperty\\", defaultValue)\`, which will ju \`defaultValue\` when the key is unrecognized." `; -exports[`#overrideLocalDefault #assertUpdateAllowed() throws error when keys is overridden 1`] = `"Unable to update \\"foo\\" because its value is overridden by the Kibana server"`; +exports[`#getUpdate$ sends { key, newValue, oldValue } notifications when config changes 1`] = ` +Array [ + Array [ + Object { + "key": "foo", + "newValue": "bar", + "oldValue": undefined, + }, + ], +] +`; + +exports[`#getUpdate$ sends { key, newValue, oldValue } notifications when config changes 2`] = ` +Array [ + Array [ + Object { + "key": "foo", + "newValue": "baz", + "oldValue": "bar", + }, + ], +] +`; exports[`#overrideLocalDefault key has no user value calls subscriber with new and previous value: single subscriber call 1`] = ` Array [ @@ -100,39 +122,3 @@ Object { exports[`#remove throws an error if key is overridden 1`] = `"Unable to update \\"bar\\" because its value is overridden by the Kibana server"`; exports[`#set throws an error if key is overridden 1`] = `"Unable to update \\"foo\\" because its value is overridden by the Kibana server"`; - -exports[`#subscribe calls handler with { key, newValue, oldValue } when config changes 1`] = ` -Array [ - Array [ - Object { - "key": "foo", - "newValue": "bar", - "oldValue": undefined, - }, - ], -] -`; - -exports[`#subscribe calls handler with { key, newValue, oldValue } when config changes 2`] = ` -Array [ - Array [ - Object { - "key": "foo", - "newValue": "baz", - "oldValue": "bar", - }, - ], -] -`; - -exports[`#subscribe returns a subscription object which unsubs when .unsubscribe() is called 1`] = ` -Array [ - Array [ - Object { - "key": "foo", - "newValue": "bar", - "oldValue": undefined, - }, - ], -] -`; diff --git a/src/core/public/ui_settings/__snapshots__/ui_settings_service.test.ts.snap b/src/core/public/ui_settings/__snapshots__/ui_settings_service.test.ts.snap new file mode 100644 index 0000000000000..e7e42c42c8b87 --- /dev/null +++ b/src/core/public/ui_settings/__snapshots__/ui_settings_service.test.ts.snap @@ -0,0 +1,78 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`#start constructs UiSettingsClient and UiSettingsApi: UiSettingsApi args 1`] = ` +[MockFunction MockUiSettingsApi] { + "calls": Array [ + Array [ + Object { + "basePathStartContract": true, + }, + "kibanaVersion", + ], + ], + "results": Array [ + Object { + "isThrow": false, + "value": undefined, + }, + ], +} +`; + +exports[`#start constructs UiSettingsClient and UiSettingsApi: UiSettingsClient args 1`] = ` +[MockFunction MockUiSettingsClient] { + "calls": Array [ + Array [ + Object { + "api": MockUiSettingsApi { + "getLoadingCount$": [MockFunction] { + "calls": Array [ + Array [], + ], + "results": Array [ + Object { + "isThrow": false, + "value": Object { + "loadingCountObservable": true, + }, + }, + ], + }, + "stop": [MockFunction], + }, + "defaults": Object { + "legacyInjectedUiSettingDefaults": true, + }, + "initialSettings": Object { + "legacyInjectedUiSettingUserValues": true, + }, + "onUpdateError": [Function], + }, + ], + ], + "results": Array [ + Object { + "isThrow": false, + "value": undefined, + }, + ], +} +`; + +exports[`#start passes the uiSettings loading count to the loading count api: loadingCount.add calls 1`] = ` +[MockFunction] { + "calls": Array [ + Array [ + Object { + "loadingCountObservable": true, + }, + ], + ], + "results": Array [ + Object { + "isThrow": false, + "value": undefined, + }, + ], +} +`; diff --git a/src/core/public/ui_settings/index.ts b/src/core/public/ui_settings/index.ts new file mode 100644 index 0000000000000..36c3d864d8119 --- /dev/null +++ b/src/core/public/ui_settings/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { UiSettingsService, UiSettingsStartContract } from './ui_settings_service'; +export { UiSettingsClient } from './ui_settings_client'; diff --git a/src/core/public/ui_settings/types.ts b/src/core/public/ui_settings/types.ts new file mode 100644 index 0000000000000..4fa4109c7bc26 --- /dev/null +++ b/src/core/public/ui_settings/types.ts @@ -0,0 +1,39 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// properties that come from legacyInjectedMetadata.uiSettings.defaults +interface InjectedUiSettingsDefault { + name?: string; + value?: any; + description?: string; + category?: string[]; + type?: string; + readOnly?: boolean; + options?: string[] | { [key: string]: any }; +} + +// properties that come from legacyInjectedMetadata.uiSettings.user +interface InjectedUiSettingsUser { + userValue?: any; + isOverridden?: boolean; +} + +export interface UiSettingsState { + [key: string]: InjectedUiSettingsDefault & InjectedUiSettingsUser; +} diff --git a/src/core/public/ui_settings/ui_settings_api.test.ts b/src/core/public/ui_settings/ui_settings_api.test.ts new file mode 100644 index 0000000000000..75358297a5661 --- /dev/null +++ b/src/core/public/ui_settings/ui_settings_api.test.ts @@ -0,0 +1,242 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import fetchMock from 'fetch-mock'; +import * as Rx from 'rxjs'; +import { takeUntil, toArray } from 'rxjs/operators'; + +import { UiSettingsApi } from './ui_settings_api'; + +function setup() { + const basePath: any = { + addToPath: jest.fn(path => `/foo/bar${path}`), + }; + + const uiSettingsApi = new UiSettingsApi(basePath, 'v9.9.9'); + + return { + basePath, + uiSettingsApi, + }; +} + +async function settlePromise(promise: Promise) { + try { + return { + isResolved: true, + result: await promise, + }; + } catch (error) { + return { + isRejected: true, + error, + }; + } +} + +afterEach(() => { + fetchMock.restore(); +}); + +describe('#batchSet', () => { + it('sends a single change immediately', () => { + fetchMock.mock('*', { + body: { settings: {} }, + }); + + const { uiSettingsApi } = setup(); + uiSettingsApi.batchSet('foo', 'bar'); + expect(fetchMock.calls()).toMatchSnapshot('synchronous fetch'); + }); + + it('buffers changes while first request is in progress, sends buffered changes after first request completes', async () => { + fetchMock.mock('*', { + body: { settings: {} }, + }); + + const { uiSettingsApi } = setup(); + + uiSettingsApi.batchSet('foo', 'bar'); + const finalPromise = uiSettingsApi.batchSet('box', 'bar'); + + expect(fetchMock.calls()).toMatchSnapshot('initial, only one request'); + await finalPromise; + expect(fetchMock.calls()).toMatchSnapshot('final, includes both requests'); + }); + + it('Overwrites previously buffered values with new values for the same key', async () => { + fetchMock.mock('*', { + body: { settings: {} }, + }); + + const { uiSettingsApi } = setup(); + + uiSettingsApi.batchSet('foo', 'a'); + uiSettingsApi.batchSet('foo', 'b'); + uiSettingsApi.batchSet('foo', 'c'); + await uiSettingsApi.batchSet('foo', 'd'); + + expect(fetchMock.calls()).toMatchSnapshot('two requests, foo=d in final'); + }); + + it('Buffers are always clear of previously buffered changes', async () => { + fetchMock.mock('*', { + body: { settings: {} }, + }); + + const { uiSettingsApi } = setup(); + uiSettingsApi.batchSet('foo', 'bar'); + uiSettingsApi.batchSet('bar', 'foo'); + await uiSettingsApi.batchSet('bar', 'box'); + + expect(fetchMock.calls()).toMatchSnapshot('two requests, second only sends bar, not foo'); + }); + + it('rejects on 404 response', async () => { + fetchMock.mock('*', { + status: 404, + body: 'not found', + }); + + const { uiSettingsApi } = setup(); + await expect(uiSettingsApi.batchSet('foo', 'bar')).rejects.toThrowErrorMatchingSnapshot(); + }); + + it('rejects on 301', async () => { + fetchMock.mock('*', { + status: 301, + body: 'redirect', + }); + + const { uiSettingsApi } = setup(); + await expect(uiSettingsApi.batchSet('foo', 'bar')).rejects.toThrowErrorMatchingSnapshot(); + }); + + it('rejects on 500', async () => { + fetchMock.mock('*', { + status: 500, + body: 'redirect', + }); + + const { uiSettingsApi } = setup(); + await expect(uiSettingsApi.batchSet('foo', 'bar')).rejects.toThrowErrorMatchingSnapshot(); + }); + + it('rejects all promises for batched requests that fail', async () => { + fetchMock.once('*', { + body: { settings: {} }, + }); + fetchMock.once('*', { + status: 400, + body: 'invalid', + }); + + const { uiSettingsApi } = setup(); + // trigger the initial sync request, which enabled buffering + uiSettingsApi.batchSet('foo', 'bar'); + + // buffer some requests so they will be sent together + await expect( + Promise.all([ + settlePromise(uiSettingsApi.batchSet('foo', 'a')), + settlePromise(uiSettingsApi.batchSet('bar', 'b')), + settlePromise(uiSettingsApi.batchSet('baz', 'c')), + ]) + ).resolves.toMatchSnapshot('promise rejections'); + + // ensure only two requests were sent + expect(fetchMock.calls().matched).toHaveLength(2); + }); +}); + +describe('#getLoadingCount$()', () => { + it('emits the current number of active requests', async () => { + fetchMock.once('*', { + body: { settings: {} }, + }); + + const { uiSettingsApi } = setup(); + const done$ = new Rx.Subject(); + const promise = uiSettingsApi + .getLoadingCount$() + .pipe( + takeUntil(done$), + toArray() + ) + .toPromise(); + + await uiSettingsApi.batchSet('foo', 'bar'); + done$.next(); + + await expect(promise).resolves.toEqual([0, 1, 0]); + }); + + it('decrements loading count when requests fail', async () => { + fetchMock.once('*', { + body: { settings: {} }, + }); + fetchMock.once('*', { + status: 400, + body: 'invalid', + }); + + const { uiSettingsApi } = setup(); + const done$ = new Rx.Subject(); + const promise = uiSettingsApi + .getLoadingCount$() + .pipe( + takeUntil(done$), + toArray() + ) + .toPromise(); + + await uiSettingsApi.batchSet('foo', 'bar'); + await expect(uiSettingsApi.batchSet('foo', 'bar')).rejects.toThrowError(); + + done$.next(); + await expect(promise).resolves.toEqual([0, 1, 0, 1, 0]); + }); +}); + +describe('#stop', () => { + it('completes any loading count observables', async () => { + fetchMock.once('*', { + body: { settings: {} }, + }); + + const { uiSettingsApi } = setup(); + const promise = Promise.all([ + uiSettingsApi + .getLoadingCount$() + .pipe(toArray()) + .toPromise(), + uiSettingsApi + .getLoadingCount$() + .pipe(toArray()) + .toPromise(), + ]); + + const batchSetPromise = uiSettingsApi.batchSet('foo', 'bar'); + uiSettingsApi.stop(); + + // both observables should emit the same values, and complete before the request is done loading + await expect(promise).resolves.toEqual([[0, 1], [0, 1]]); + await batchSetPromise; + }); +}); diff --git a/src/core/public/ui_settings/ui_settings_api.ts b/src/core/public/ui_settings/ui_settings_api.ts new file mode 100644 index 0000000000000..6d43384fa6d02 --- /dev/null +++ b/src/core/public/ui_settings/ui_settings_api.ts @@ -0,0 +1,163 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { BehaviorSubject } from 'rxjs'; + +import { BasePathStartContract } from '../base_path'; +import { UiSettingsState } from './types'; + +export interface UiSettingsApiResponse { + settings: UiSettingsState; +} + +interface Changes { + values: { + [key: string]: any; + }; + + callback(error?: Error, response?: UiSettingsApiResponse): void; +} + +const NOOP_CHANGES = { + values: {}, + callback: () => { + // noop + }, +}; + +export class UiSettingsApi { + private pendingChanges?: Changes; + private sendInProgress = false; + + private readonly loadingCount$ = new BehaviorSubject(0); + + constructor( + private readonly basePath: BasePathStartContract, + private readonly kibanaVersion: string + ) {} + + /** + * Adds a key+value that will be sent to the server ASAP. If a request is + * already in progress it will wait until the previous request is complete + * before sending the next request + */ + public batchSet(key: string, value: any) { + return new Promise((resolve, reject) => { + const prev = this.pendingChanges || NOOP_CHANGES; + + this.pendingChanges = { + values: { + ...prev.values, + [key]: value, + }, + + callback(error, resp) { + prev.callback(error, resp); + + if (error) { + reject(error); + } else { + resolve(resp); + } + }, + }; + + this.flushPendingChanges(); + }); + } + + /** + * Gets an observable that notifies subscribers of the current number of active requests + */ + public getLoadingCount$() { + return this.loadingCount$.asObservable(); + } + + /** + * Prepares the uiSettings API to be discarded + */ + public stop() { + this.loadingCount$.complete(); + } + + /** + * If there are changes that need to be sent to the server and there is not already a + * request in progress, this method will start a request sending those changes. Once + * the request is complete `flushPendingChanges()` will be called again, and if the + * prerequisites are still true (because changes were queued while the request was in + * progress) then another request will be started until all pending changes have been + * sent to the server. + */ + private async flushPendingChanges() { + if (!this.pendingChanges) { + return; + } + + if (this.sendInProgress) { + return; + } + + const changes = this.pendingChanges; + this.pendingChanges = undefined; + + try { + this.sendInProgress = true; + changes.callback( + undefined, + await this.sendRequest('POST', '/api/kibana/settings', { + changes: changes.values, + }) + ); + } catch (error) { + changes.callback(error); + } finally { + this.sendInProgress = false; + this.flushPendingChanges(); + } + } + + /** + * Calls window.fetch() with the proper headers and error handling logic. + * + * TODO: migrate this to kfetch or whatever the new platform equivalent is once it exists + */ + private async sendRequest(method: string, path: string, body: any) { + try { + this.loadingCount$.next(this.loadingCount$.getValue() + 1); + const response = await fetch(this.basePath.addToPath(path), { + method, + body: JSON.stringify(body), + headers: { + accept: 'application/json', + 'content-type': 'application/json', + 'kbn-version': this.kibanaVersion, + }, + credentials: 'same-origin', + }); + + if (response.status >= 300) { + throw new Error(`Request failed with status code: ${response.status}`); + } + + return await response.json(); + } finally { + this.loadingCount$.next(this.loadingCount$.getValue() - 1); + } + } +} diff --git a/src/ui/ui_settings/public/ui_settings_client.test.js b/src/core/public/ui_settings/ui_settings_client.test.ts similarity index 82% rename from src/ui/ui_settings/public/ui_settings_client.test.js rename to src/core/public/ui_settings/ui_settings_client.test.ts index f41c9bc0018ca..53cf4b7347e1b 100644 --- a/src/ui/ui_settings/public/ui_settings_client.test.js +++ b/src/core/public/ui_settings/ui_settings_client.test.ts @@ -18,41 +18,26 @@ */ import { UiSettingsClient } from './ui_settings_client'; -import { sendRequest } from './send_request'; -jest.useFakeTimers(); -jest.mock('./send_request', () => ({ - sendRequest: jest.fn(() => ({})) -})); - -beforeEach(() => { - sendRequest.mockRestore(); - jest.clearAllMocks(); -}); - -function setup(options = {}) { - const { - defaults = { dateFormat: { value: 'Browser' } }, - initialSettings = {} - } = options; +function setup(options: { defaults?: any; initialSettings?: any } = {}) { + const { defaults = { dateFormat: { value: 'Browser' } }, initialSettings = {} } = options; const batchSet = jest.fn(() => ({ - settings: {} + settings: {}, })); + const onUpdateError = jest.fn(); + const config = new UiSettingsClient({ defaults, initialSettings, api: { - batchSet - }, - notify: { - log: jest.fn(), - error: jest.fn(), - } + batchSet, + } as any, + onUpdateError, }); - return { config, batchSet }; + return { config, batchSet, onUpdateError }; } describe('#get', () => { @@ -88,7 +73,7 @@ describe('#get', () => { expect(config.get('dataFormat', defaultDateFormat)).toBe(defaultDateFormat); }); - it('throws on unknown properties that don\'t have a value yet.', () => { + it("throws on unknown properties that don't have a value yet.", () => { const { config } = setup(); expect(() => config.get('throwableProperty')).toThrowErrorMatchingSnapshot(); }); @@ -129,9 +114,9 @@ describe('#set', () => { initialSettings: { foo: { isOverridden: true, - value: 'bar' - } - } + value: 'bar', + }, + }, }); await expect(config.set('foo', true)).rejects.toThrowErrorMatchingSnapshot(); }); @@ -158,9 +143,9 @@ describe('#remove', () => { initialSettings: { bar: { isOverridden: true, - userValue: true - } - } + userValue: true, + }, + }, }); await expect(config.remove('bar')).rejects.toThrowErrorMatchingSnapshot(); }); @@ -209,12 +194,12 @@ describe('#isCustom', () => { }); }); -describe('#subscribe', () => { - it('calls handler with { key, newValue, oldValue } when config changes', () => { +describe('#getUpdate$', () => { + it('sends { key, newValue, oldValue } notifications when config changes', () => { const handler = jest.fn(); const { config } = setup(); - config.subscribe(handler); + config.getUpdate$().subscribe(handler); expect(handler).not.toHaveBeenCalled(); config.set('foo', 'bar'); @@ -227,21 +212,17 @@ describe('#subscribe', () => { expect(handler.mock.calls).toMatchSnapshot(); }); - it('returns a subscription object which unsubs when .unsubscribe() is called', () => { - const handler = jest.fn(); + it('observables complete when client is stopped', () => { + const onComplete = jest.fn(); const { config } = setup(); - const subscription = config.subscribe(handler); - expect(handler).not.toHaveBeenCalled(); - - config.set('foo', 'bar'); - expect(handler).toHaveBeenCalledTimes(1); - expect(handler.mock.calls).toMatchSnapshot(); - handler.mockClear(); + config.getUpdate$().subscribe({ + complete: onComplete, + }); - subscription.unsubscribe(); - config.set('foo', 'baz'); - expect(handler).not.toHaveBeenCalled(); + expect(onComplete).not.toHaveBeenCalled(); + config.stop(); + expect(onComplete).toHaveBeenCalled(); }); }); @@ -267,7 +248,7 @@ describe('#overrideLocalDefault', () => { const handler = jest.fn(); const { config } = setup(); - config.subscribe(handler); + config.getUpdate$().subscribe(handler); config.overrideLocalDefault('dateFormat', 'bar'); expect(handler.mock.calls).toMatchSnapshot('single subscriber call'); }); @@ -297,7 +278,7 @@ describe('#overrideLocalDefault', () => { const { config } = setup(); config.set('dateFormat', 'foo'); - config.subscribe(handler); + config.getUpdate$().subscribe(handler); config.overrideLocalDefault('dateFormat', 'bar'); expect(handler).not.toHaveBeenCalled(); }); @@ -323,55 +304,40 @@ describe('#overrideLocalDefault', () => { const { config } = setup(); expect(config.isOverridden('foo')).toBe(false); }); + it('returns false if key is no overridden', () => { const { config } = setup({ initialSettings: { foo: { - userValue: 1 + userValue: 1, }, bar: { isOverridden: true, - userValue: 2 - } - } + userValue: 2, + }, + }, }); expect(config.isOverridden('foo')).toBe(false); }); + it('returns true when key is overridden', () => { const { config } = setup({ initialSettings: { foo: { - userValue: 1 + userValue: 1, }, bar: { isOverridden: true, - userValue: 2 + userValue: 2, }, - } + }, }); expect(config.isOverridden('bar')).toBe(true); }); + it('returns false for object prototype properties', () => { const { config } = setup(); expect(config.isOverridden('hasOwnProperty')).toBe(false); }); }); - - describe('#assertUpdateAllowed()', () => { - it('returns false if no settings defined', () => { - const { config } = setup(); - expect(config.assertUpdateAllowed('foo')).toBe(undefined); - }); - it('throws error when keys is overridden', () => { - const { config } = setup({ - initialSettings: { - foo: { - isOverridden: true, - userValue: 'bar' - } - } - }); - expect(() => config.assertUpdateAllowed('foo')).toThrowErrorMatchingSnapshot(); - }); - }); }); diff --git a/src/core/public/ui_settings/ui_settings_client.ts b/src/core/public/ui_settings/ui_settings_client.ts new file mode 100644 index 0000000000000..3eb818ee453aa --- /dev/null +++ b/src/core/public/ui_settings/ui_settings_client.ts @@ -0,0 +1,251 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { cloneDeep, defaultsDeep } from 'lodash'; +import { Subject } from 'rxjs'; + +import { UiSettingsState } from './types'; +import { UiSettingsApi } from './ui_settings_api'; + +interface Params { + api: UiSettingsApi; + onUpdateError: UiSettingsClient['onUpdateError']; + defaults: UiSettingsState; + initialSettings: UiSettingsState; +} + +export class UiSettingsClient { + private readonly update$ = new Subject<{ key: string; newValue: any; oldValue: any }>(); + + private readonly api: UiSettingsApi; + private readonly onUpdateError: (error: Error) => void; + private readonly defaults: UiSettingsState; + private cache: UiSettingsState; + + constructor(readonly params: Params) { + this.api = params.api; + this.onUpdateError = params.onUpdateError; + this.defaults = cloneDeep(params.defaults); + this.cache = defaultsDeep({}, this.defaults, cloneDeep(params.initialSettings)); + } + + /** + * Gets the metadata about all uiSettings, including the type, default value, and user value + * for each key. + */ + public getAll() { + return cloneDeep(this.cache); + } + + /** + * Gets the value for a specific uiSetting. If this setting has no user-defined value + * then the `defaultOverride` parameter is returned (and parsed if setting is of type + * "json" or "number). If the parameter is not defined and the key is not defined by a + * uiSettingDefaults then an error is thrown, otherwise the default is read + * from the uiSettingDefaults. + */ + public get(key: string, defaultOverride?: any) { + const declared = this.isDeclared(key); + + if (!declared && defaultOverride !== undefined) { + return defaultOverride; + } + + if (!declared) { + throw new Error( + `Unexpected \`config.get("${key}")\` call on unrecognized configuration setting "${key}". +Setting an initial value via \`config.set("${key}", value)\` before attempting to retrieve +any custom setting value for "${key}" may fix this issue. +You can use \`config.get("${key}", defaultValue)\`, which will just return +\`defaultValue\` when the key is unrecognized.` + ); + } + + const type = this.cache[key].type; + const userValue = this.cache[key].userValue; + const defaultValue = defaultOverride !== undefined ? defaultOverride : this.cache[key].value; + const value = userValue == null ? defaultValue : userValue; + + if (type === 'json') { + return JSON.parse(value); + } + + if (type === 'number') { + return parseFloat(value); + } + + return value; + } + + /** + * Sets the value for a uiSetting. If the setting is not defined in the uiSettingDefaults + * it will be stored as a custom setting. The new value will be synchronously available via + * the `get()` method and sent to the server in the background. If the request to the + * server fails then a toast notification will be displayed and the setting will be + * reverted it its value before `set()` was called. + */ + public async set(key: string, val: any) { + return await this.update(key, val); + } + + /** + * Removes the user-defined value for a setting, causing it to revert to the default. This + * method behaves the same as calling `set(key, null)`, including the synchronization, custom + * setting, and error behavior of that method. + */ + public async remove(key: string) { + return await this.update(key, null); + } + + /** + * 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 `set()` method. + */ + public isDeclared(key: string) { + return key in this.cache; + } + + /** + * Returns true if the setting has no user-defined value or is unknown + */ + public isDefault(key: string) { + return !this.isDeclared(key) || this.cache[key].userValue == null; + } + + /** + * Returns true if the setting is not a part of the uiSettingDefaults, but was either + * added directly via `set()`, or is an unknown setting found in the uiSettings saved + * object + */ + public isCustom(key: string) { + return this.isDeclared(key) && !('value' in this.cache[key]); + } + + /** + * Returns true if a settings value is overridden by the server. When a setting is overridden + * its value can not be changed via `set()` or `remove()`. + */ + public isOverridden(key: string) { + return this.isDeclared(key) && Boolean(this.cache[key].isOverridden); + } + + /** + * Overrides the default value for a setting in this specific browser tab. If the page + * is reloaded the default override is lost. + */ + public overrideLocalDefault(key: string, newDefault: any) { + // capture the previous value + const prevDefault = this.defaults[key] ? this.defaults[key].value : undefined; + + // update defaults map + this.defaults[key] = { + ...(this.defaults[key] || {}), + value: newDefault, + }; + + // update cached default value + this.cache[key] = { + ...(this.cache[key] || {}), + value: newDefault, + }; + + // don't broadcast change if userValue was already overriding the default + if (this.cache[key].userValue == null) { + this.update$.next({ + key, + newValue: newDefault, + oldValue: prevDefault, + }); + } + } + + /** + * Returns an Observable that notifies subscribers of each update to the uiSettings, + * including the key, newValue, and oldValue of the setting that changed. + */ + public getUpdate$() { + return this.update$.asObservable(); + } + + /** + * Prepares the uiSettingsClient to be discarded, completing any update$ observables + * that have been created. + */ + public stop() { + this.update$.complete(); + } + + private assertUpdateAllowed(key: string) { + if (this.isOverridden(key)) { + throw new Error( + `Unable to update "${key}" because its value is overridden by the Kibana server` + ); + } + } + + private async update(key: string, newVal: any) { + this.assertUpdateAllowed(key); + + const declared = this.isDeclared(key); + const defaults = this.defaults; + + const oldVal = declared ? this.cache[key].userValue : undefined; + + const unchanged = oldVal === newVal; + if (unchanged) { + return true; + } + + const initialVal = declared ? this.get(key) : undefined; + this.setLocally(key, newVal); + + try { + const { settings } = await this.api.batchSet(key, newVal); + this.cache = defaultsDeep({}, defaults, settings); + return true; + } catch (error) { + this.setLocally(key, initialVal); + this.onUpdateError(error); + return false; + } + } + + private setLocally(key: string, newValue: any) { + this.assertUpdateAllowed(key); + + if (!this.isDeclared(key)) { + this.cache[key] = {}; + } + + const oldValue = this.get(key); + + if (newValue === null) { + delete this.cache[key].userValue; + } else { + const { type } = this.cache[key]; + if (type === 'json' && typeof newValue !== 'string') { + this.cache[key].userValue = JSON.stringify(newValue); + } else { + this.cache[key].userValue = newValue; + } + } + + this.update$.next({ key, newValue, oldValue }); + } +} diff --git a/src/core/public/ui_settings/ui_settings_service.test.ts b/src/core/public/ui_settings/ui_settings_service.test.ts new file mode 100644 index 0000000000000..2b31cedd07094 --- /dev/null +++ b/src/core/public/ui_settings/ui_settings_service.test.ts @@ -0,0 +1,127 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +function mockClass( + module: string, + Class: { new (...args: any[]): T }, + setup: (instance: any, args: any[]) => void +) { + const MockClass = jest.fn(function(this: any, ...args: any[]) { + setup(this, args); + }); + + // define the mock name which is used in some snapshots + MockClass.mockName(`Mock${Class.name}`); + + // define the class name for the MockClass which is used in other snapshots + Object.defineProperty(MockClass, 'name', { + value: `Mock${Class.name}`, + }); + + jest.mock(module, () => ({ + [Class.name]: MockClass, + })); + + return MockClass; +} + +// Mock the UiSettingsApi class +import { UiSettingsApi } from './ui_settings_api'; +const MockUiSettingsApi = mockClass('./ui_settings_api', UiSettingsApi, inst => { + inst.stop = jest.fn(); + inst.getLoadingCount$ = jest.fn().mockReturnValue({ + loadingCountObservable: true, + }); +}); + +// Mock the UiSettingsClient class +import { UiSettingsClient } from './ui_settings_client'; +const MockUiSettingsClient = mockClass('./ui_settings_client', UiSettingsClient, inst => { + inst.stop = jest.fn(); +}); + +// Load the service +import { UiSettingsService } from './ui_settings_service'; + +const loadingCountStartContract = { + loadingCountStartContract: true, + add: jest.fn(), +}; + +const defaultDeps: any = { + notifications: { + notificationsStartContract: true, + }, + loadingCount: loadingCountStartContract, + injectedMetadata: { + injectedMetadataStartContract: true, + getKibanaVersion: jest.fn().mockReturnValue('kibanaVersion'), + getLegacyMetadata: jest.fn().mockReturnValue({ + uiSettings: { + defaults: { legacyInjectedUiSettingDefaults: true }, + user: { legacyInjectedUiSettingUserValues: true }, + }, + }), + }, + basePath: { + basePathStartContract: true, + }, +}; + +afterEach(() => { + jest.clearAllMocks(); +}); + +describe('#start', () => { + it('returns an instance of UiSettingsClient', () => { + const start = new UiSettingsService().start(defaultDeps); + expect(start).toBeInstanceOf(MockUiSettingsClient); + }); + + it('constructs UiSettingsClient and UiSettingsApi', () => { + new UiSettingsService().start(defaultDeps); + + expect(MockUiSettingsApi).toMatchSnapshot('UiSettingsApi args'); + expect(MockUiSettingsClient).toMatchSnapshot('UiSettingsClient args'); + }); + + it('passes the uiSettings loading count to the loading count api', () => { + new UiSettingsService().start(defaultDeps); + + expect(loadingCountStartContract.add).toMatchSnapshot('loadingCount.add calls'); + }); +}); + +describe('#stop', () => { + it('runs fine if service never started', () => { + const service = new UiSettingsService(); + expect(() => service.stop()).not.toThrowError(); + }); + + it('stops the uiSettingsClient and uiSettingsApi', () => { + const service = new UiSettingsService(); + const client = service.start(defaultDeps); + const [[{ api }]] = MockUiSettingsClient.mock.calls; + jest.spyOn(client, 'stop'); + jest.spyOn(api, 'stop'); + service.stop(); + expect(api.stop).toHaveBeenCalledTimes(1); + expect(client.stop).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/core/public/ui_settings/ui_settings_service.ts b/src/core/public/ui_settings/ui_settings_service.ts new file mode 100644 index 0000000000000..e11f903507dc4 --- /dev/null +++ b/src/core/public/ui_settings/ui_settings_service.ts @@ -0,0 +1,72 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { BasePathStartContract } from '../base_path'; +import { InjectedMetadataStartContract } from '../injected_metadata'; +import { LoadingCountStartContract } from '../loading_count'; +import { NotificationsStartContract } from '../notifications'; + +import { UiSettingsApi } from './ui_settings_api'; +import { UiSettingsClient } from './ui_settings_client'; + +interface Deps { + notifications: NotificationsStartContract; + loadingCount: LoadingCountStartContract; + injectedMetadata: InjectedMetadataStartContract; + basePath: BasePathStartContract; +} + +export class UiSettingsService { + private uiSettingsApi?: UiSettingsApi; + private uiSettingsClient?: UiSettingsClient; + + public start({ notifications, loadingCount, injectedMetadata, basePath }: Deps) { + this.uiSettingsApi = new UiSettingsApi(basePath, injectedMetadata.getKibanaVersion()); + loadingCount.add(this.uiSettingsApi.getLoadingCount$()); + + // TODO: when we have time to refactor the UiSettingsClient and all consumers + // we should stop using the legacy format and pick a better one + const legacyMetadata = injectedMetadata.getLegacyMetadata(); + this.uiSettingsClient = new UiSettingsClient({ + api: this.uiSettingsApi, + onUpdateError: error => { + notifications.toasts.addDanger({ + title: 'Unable to update UI setting', + text: error.message, + }); + }, + defaults: legacyMetadata.uiSettings.defaults, + initialSettings: legacyMetadata.uiSettings.user, + }); + + return this.uiSettingsClient; + } + + public stop() { + if (this.uiSettingsClient) { + this.uiSettingsClient.stop(); + } + + if (this.uiSettingsApi) { + this.uiSettingsApi.stop(); + } + } +} + +export type UiSettingsStartContract = UiSettingsClient; diff --git a/src/functional_test_runner/__tests__/lib/index.js b/src/core/public/utils/index.ts similarity index 95% rename from src/functional_test_runner/__tests__/lib/index.js rename to src/core/public/utils/index.ts index a92d22e2738bb..17de85bbfecce 100644 --- a/src/functional_test_runner/__tests__/lib/index.js +++ b/src/core/public/utils/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { startupKibana } from './kibana'; +export { modifyUrl } from './modify_url'; diff --git a/src/core/public/utils/modify_url.test.ts b/src/core/public/utils/modify_url.test.ts new file mode 100644 index 0000000000000..d1b7081093c28 --- /dev/null +++ b/src/core/public/utils/modify_url.test.ts @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { modifyUrl } from './modify_url'; + +it('supports returning a new url spec', () => { + expect(modifyUrl('http://localhost', () => ({}))).toBe(''); +}); + +it('supports modifying the passed object', () => { + expect( + modifyUrl('http://localhost', parsed => { + parsed.port = 9999; + parsed.auth = 'foo:bar'; + }) + ).toBe('http://foo:bar@localhost:9999/'); +}); + +it('supports changing pathname', () => { + expect( + modifyUrl('http://localhost/some/path', parsed => { + parsed.pathname += '/subpath'; + }) + ).toBe('http://localhost/some/path/subpath'); +}); + +it('supports changing port', () => { + expect( + modifyUrl('http://localhost:5601', parsed => { + parsed.port = parsed.port! + 1; + }) + ).toBe('http://localhost:5602/'); +}); + +it('supports changing protocol', () => { + expect( + modifyUrl('http://localhost', parsed => { + parsed.protocol = 'mail'; + parsed.slashes = false; + parsed.pathname = undefined; + }) + ).toBe('mail:localhost'); +}); diff --git a/src/utils/modify_url.js b/src/core/public/utils/modify_url.ts similarity index 77% rename from src/utils/modify_url.js rename to src/core/public/utils/modify_url.ts index f988d5218ebf3..15a5532226c60 100644 --- a/src/utils/modify_url.js +++ b/src/core/public/utils/modify_url.ts @@ -17,7 +17,29 @@ * under the License. */ -import { parse as parseUrl, format as formatUrl } from 'url'; +import { format as formatUrl, parse as parseUrl } from 'url'; + +interface UrlParts { + protocol?: string; + slashes?: boolean; + auth?: string; + hostname?: string; + port?: number; + pathname?: string; + query: { [key: string]: string | string[] | undefined }; + hash?: string; +} + +interface UrlFormatParts { + protocol?: string; + slashes?: boolean; + auth?: string; + hostname?: string; + port?: string | number; + pathname?: string; + query?: { [key: string]: string | string[] | undefined }; + hash?: string; +} /** * Takes a URL and a function that takes the meaningful parts @@ -42,17 +64,12 @@ import { parse as parseUrl, format as formatUrl } from 'url'; * lead to the modifications being ignored (depending on which * property was modified) * - It's not always clear wither to use path/pathname, host/hostname, - * so this trys to add helpful constraints + * so this tries to add helpful constraints * - * @param {String} url - the url to parse - * @param {Function} block - a function that will modify the parsed url, or return a new one - * @return {String} the modified and reformatted url + * @param url the url to parse + * @param block a function that will modify the parsed url, or return a new one */ -export function modifyUrl(url, block) { - if (typeof block !== 'function') { - throw new TypeError('You must pass a block to define the modifications desired'); - } - +export function modifyUrl(url: string, block: (parts: UrlParts) => UrlFormatParts | void) { const parsed = parseUrl(url, true); // copy over the most specific version of each @@ -66,7 +83,7 @@ export function modifyUrl(url, block) { slashes: parsed.slashes, auth: parsed.auth, hostname: parsed.hostname, - port: parsed.port, + port: parsed.port ? Number(parsed.port) : undefined, pathname: parsed.pathname, query: parsed.query || {}, hash: parsed.hash, diff --git a/src/core/server/__snapshots__/index.test.ts.snap b/src/core/server/__snapshots__/index.test.ts.snap new file mode 100644 index 0000000000000..8c3022a07d074 --- /dev/null +++ b/src/core/server/__snapshots__/index.test.ts.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`does not fail on "start" if there are unused paths detected: unused paths logs 1`] = ` +Object { + "debug": Array [ + Array [ + "starting server", + ], + ], + "error": Array [], + "fatal": Array [], + "info": Array [], + "log": Array [], + "trace": Array [ + Array [ + "some config paths are not handled by the core: [\\"some.path\\",\\"another.path\\"]", + ], + ], + "warn": Array [], +} +`; diff --git a/src/core/server/bootstrap.ts b/src/core/server/bootstrap.ts new file mode 100644 index 0000000000000..69b1d751010c9 --- /dev/null +++ b/src/core/server/bootstrap.ts @@ -0,0 +1,113 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import chalk from 'chalk'; +import { isMaster } from 'cluster'; +import { CliArgs, Env, RawConfigService } from './config'; +import { LegacyObjectToConfigAdapter } from './legacy_compat'; +import { Root } from './root'; + +interface KibanaFeatures { + // Indicates whether we can run Kibana in a so called cluster mode in which + // Kibana is run as a "worker" process together with optimizer "worker" process + // that are orchestrated by the "master" process (dev mode only feature). + isClusterModeSupported: boolean; + + // Indicates whether we can run Kibana without X-Pack plugin pack even if it's + // installed (dev mode only feature). + isOssModeSupported: boolean; + + // Indicates whether we can run Kibana in REPL mode (dev mode only feature). + isReplModeSupported: boolean; + + // Indicates whether X-Pack plugin pack is installed and available. + isXPackInstalled: boolean; +} + +interface BootstrapArgs { + configs: string[]; + cliArgs: CliArgs; + applyConfigOverrides: (config: Record) => Record; + features: KibanaFeatures; +} + +export async function bootstrap({ + configs, + cliArgs, + applyConfigOverrides, + features, +}: BootstrapArgs) { + if (cliArgs.repl && !features.isReplModeSupported) { + onRootShutdown('Kibana REPL mode can only be run in development mode.'); + } + + const env = Env.createDefault({ + configs, + cliArgs, + isDevClusterMaster: isMaster && cliArgs.dev && features.isClusterModeSupported, + }); + + const rawConfigService = new RawConfigService( + env.configs, + rawConfig => new LegacyObjectToConfigAdapter(applyConfigOverrides(rawConfig)) + ); + + rawConfigService.loadConfig(); + + const root = new Root(rawConfigService.getConfig$(), env, onRootShutdown); + + function shutdown(reason?: Error) { + rawConfigService.stop(); + return root.shutdown(reason); + } + + try { + await root.start(); + } catch (err) { + await shutdown(err); + } + + process.on('SIGHUP', () => { + const cliLogger = root.logger.get('cli'); + cliLogger.info('Reloading logging configuration due to SIGHUP.', { tags: ['config'] }); + + try { + rawConfigService.reloadConfig(); + } catch (err) { + return shutdown(err); + } + + cliLogger.info('Reloaded logging configuration due to SIGHUP.', { tags: ['config'] }); + }); + + process.on('SIGINT', () => shutdown()); + process.on('SIGTERM', () => shutdown()); +} + +function onRootShutdown(reason?: any) { + if (reason !== undefined) { + // There is a chance that logger wasn't configured properly and error that + // that forced root to shut down could go unnoticed. To prevent this we always + // mirror such fatal errors in standard output with `console.error`. + // tslint:disable no-console + console.error(`\n${chalk.white.bgRed(' FATAL ')} ${reason}\n`); + } + + process.exit(reason === undefined ? 0 : (reason as any).processExitCode || 1); +} diff --git a/src/core/server/config/__tests__/__fixtures__/config.yml b/src/core/server/config/__fixtures__/config.yml similarity index 100% rename from src/core/server/config/__tests__/__fixtures__/config.yml rename to src/core/server/config/__fixtures__/config.yml diff --git a/src/core/server/config/__tests__/__fixtures__/config_flat.yml b/src/core/server/config/__fixtures__/config_flat.yml similarity index 100% rename from src/core/server/config/__tests__/__fixtures__/config_flat.yml rename to src/core/server/config/__fixtures__/config_flat.yml diff --git a/src/core/server/config/__tests__/__fixtures__/en_var_ref_config.yml b/src/core/server/config/__fixtures__/en_var_ref_config.yml similarity index 100% rename from src/core/server/config/__tests__/__fixtures__/en_var_ref_config.yml rename to src/core/server/config/__fixtures__/en_var_ref_config.yml diff --git a/src/core/server/config/__tests__/__fixtures__/one.yml b/src/core/server/config/__fixtures__/one.yml similarity index 100% rename from src/core/server/config/__tests__/__fixtures__/one.yml rename to src/core/server/config/__fixtures__/one.yml diff --git a/src/core/server/config/__tests__/__fixtures__/two.yml b/src/core/server/config/__fixtures__/two.yml similarity index 100% rename from src/core/server/config/__tests__/__fixtures__/two.yml rename to src/core/server/config/__fixtures__/two.yml diff --git a/src/core/server/config/__tests__/__mocks__/env.ts b/src/core/server/config/__mocks__/env.ts similarity index 76% rename from src/core/server/config/__tests__/__mocks__/env.ts rename to src/core/server/config/__mocks__/env.ts index fe33fd32f4648..dec62978a292f 100644 --- a/src/core/server/config/__tests__/__mocks__/env.ts +++ b/src/core/server/config/__mocks__/env.ts @@ -19,13 +19,22 @@ // Test helpers to simplify mocking environment options. -import { EnvOptions } from '../../env'; +import { EnvOptions } from '../env'; -export function getEnvOptions(options: Partial = {}): EnvOptions { +type DeepPartial = { + [P in keyof T]?: T[P] extends Array ? Array> : DeepPartial +}; + +export function getEnvOptions(options: DeepPartial = {}): EnvOptions { return { configs: options.configs || [], cliArgs: { dev: true, + quiet: false, + silent: false, + watch: false, + repl: false, + basePath: false, ...(options.cliArgs || {}), }, isDevClusterMaster: diff --git a/src/core/server/config/__tests__/__snapshots__/config_service.test.ts.snap b/src/core/server/config/__snapshots__/config_service.test.ts.snap similarity index 100% rename from src/core/server/config/__tests__/__snapshots__/config_service.test.ts.snap rename to src/core/server/config/__snapshots__/config_service.test.ts.snap diff --git a/src/core/server/config/__tests__/__snapshots__/env.test.ts.snap b/src/core/server/config/__snapshots__/env.test.ts.snap similarity index 57% rename from src/core/server/config/__tests__/__snapshots__/env.test.ts.snap rename to src/core/server/config/__snapshots__/env.test.ts.snap index db2917da5406f..5931b0697d79c 100644 --- a/src/core/server/config/__tests__/__snapshots__/env.test.ts.snap +++ b/src/core/server/config/__snapshots__/env.test.ts.snap @@ -1,12 +1,85 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`correctly creates default environment if \`--env.name\` is supplied.: dev env properties 1`] = ` +Env { + "binDir": "/test/cwd/bin", + "cliArgs": Object { + "basePath": false, + "dev": true, + "envName": "development", + "quiet": false, + "repl": false, + "silent": false, + "watch": false, + }, + "configDir": "/test/cwd/config", + "configs": Array [ + "/some/other/path/some-kibana.yml", + ], + "corePluginsDir": "/test/cwd/core_plugins", + "homeDir": "/test/cwd", + "isDevClusterMaster": false, + "logDir": "/test/cwd/log", + "mode": Object { + "dev": true, + "name": "development", + "prod": false, + }, + "packageInfo": Object { + "branch": "feature-v1", + "buildNum": 9007199254740991, + "buildSha": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "version": "v1", + }, + "staticFilesDir": "/test/cwd/ui", +} +`; + +exports[`correctly creates default environment if \`--env.name\` is supplied.: prod env properties 1`] = ` +Env { + "binDir": "/test/cwd/bin", + "cliArgs": Object { + "basePath": false, + "dev": false, + "envName": "production", + "quiet": false, + "repl": false, + "silent": false, + "watch": false, + }, + "configDir": "/test/cwd/config", + "configs": Array [ + "/some/other/path/some-kibana.yml", + ], + "corePluginsDir": "/test/cwd/core_plugins", + "homeDir": "/test/cwd", + "isDevClusterMaster": false, + "logDir": "/test/cwd/log", + "mode": Object { + "dev": false, + "name": "production", + "prod": true, + }, + "packageInfo": Object { + "branch": "feature-v1", + "buildNum": 9007199254740991, + "buildSha": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "version": "v1", + }, + "staticFilesDir": "/test/cwd/ui", +} +`; + exports[`correctly creates default environment in dev mode.: env properties 1`] = ` Env { "binDir": "/test/cwd/bin", "cliArgs": Object { + "basePath": false, "dev": true, - "someArg": 1, - "someOtherArg": "2", + "quiet": false, + "repl": false, + "silent": false, + "watch": false, }, "configDir": "/test/cwd/config", "configs": Array [ @@ -15,12 +88,6 @@ Env { "corePluginsDir": "/test/cwd/core_plugins", "homeDir": "/test/cwd", "isDevClusterMaster": true, - "legacy": EventEmitter { - "_events": Object {}, - "_eventsCount": 0, - "_maxListeners": undefined, - "domain": null, - }, "logDir": "/test/cwd/log", "mode": Object { "dev": true, @@ -41,9 +108,12 @@ exports[`correctly creates default environment in prod distributable mode.: env Env { "binDir": "/test/cwd/bin", "cliArgs": Object { + "basePath": false, "dev": false, - "someArg": 1, - "someOtherArg": "2", + "quiet": false, + "repl": false, + "silent": false, + "watch": false, }, "configDir": "/test/cwd/config", "configs": Array [ @@ -52,12 +122,6 @@ Env { "corePluginsDir": "/test/cwd/core_plugins", "homeDir": "/test/cwd", "isDevClusterMaster": false, - "legacy": EventEmitter { - "_events": Object {}, - "_eventsCount": 0, - "_maxListeners": undefined, - "domain": null, - }, "logDir": "/test/cwd/log", "mode": Object { "dev": false, @@ -78,9 +142,12 @@ exports[`correctly creates default environment in prod non-distributable mode.: Env { "binDir": "/test/cwd/bin", "cliArgs": Object { + "basePath": false, "dev": false, - "someArg": 1, - "someOtherArg": "2", + "quiet": false, + "repl": false, + "silent": false, + "watch": false, }, "configDir": "/test/cwd/config", "configs": Array [ @@ -89,12 +156,6 @@ Env { "corePluginsDir": "/test/cwd/core_plugins", "homeDir": "/test/cwd", "isDevClusterMaster": false, - "legacy": EventEmitter { - "_events": Object {}, - "_eventsCount": 0, - "_maxListeners": undefined, - "domain": null, - }, "logDir": "/test/cwd/log", "mode": Object { "dev": false, @@ -115,9 +176,12 @@ exports[`correctly creates environment with constructor.: env properties 1`] = ` Env { "binDir": "/some/home/dir/bin", "cliArgs": Object { + "basePath": false, "dev": false, - "someArg": 1, - "someOtherArg": "2", + "quiet": false, + "repl": false, + "silent": false, + "watch": false, }, "configDir": "/some/home/dir/config", "configs": Array [ @@ -126,12 +190,6 @@ Env { "corePluginsDir": "/some/home/dir/core_plugins", "homeDir": "/some/home/dir", "isDevClusterMaster": false, - "legacy": EventEmitter { - "_events": Object {}, - "_eventsCount": 0, - "_maxListeners": undefined, - "domain": null, - }, "logDir": "/some/home/dir/log", "mode": Object { "dev": false, diff --git a/src/core/server/config/__tests__/__snapshots__/read_config.test.ts.snap b/src/core/server/config/__snapshots__/read_config.test.ts.snap similarity index 100% rename from src/core/server/config/__tests__/__snapshots__/read_config.test.ts.snap rename to src/core/server/config/__snapshots__/read_config.test.ts.snap diff --git a/src/core/server/config/__tests__/apply_argv.test.ts b/src/core/server/config/apply_argv.test.ts similarity index 94% rename from src/core/server/config/__tests__/apply_argv.test.ts rename to src/core/server/config/apply_argv.test.ts index 7908dd2468021..80aa3d9f74a40 100644 --- a/src/core/server/config/__tests__/apply_argv.test.ts +++ b/src/core/server/config/apply_argv.test.ts @@ -17,12 +17,12 @@ * under the License. */ -import { Config, ObjectToConfigAdapter } from '..'; +import { Config, ObjectToConfigAdapter } from '.'; /** * Overrides some config values with ones from argv. * - * @param config `RawConfig` instance to update config values for. + * @param config `Config` instance to update config values for. * @param argv Argv object with key/value pairs. */ export function overrideConfigWithArgv(config: Config, argv: { [key: string]: any }) { diff --git a/src/core/server/config/__tests__/config_service.test.ts b/src/core/server/config/config_service.test.ts similarity index 97% rename from src/core/server/config/__tests__/config_service.test.ts rename to src/core/server/config/config_service.test.ts index c41ae3ccd8b75..22598b2a971d0 100644 --- a/src/core/server/config/__tests__/config_service.test.ts +++ b/src/core/server/config/config_service.test.ts @@ -23,13 +23,12 @@ import { BehaviorSubject } from 'rxjs'; import { first } from 'rxjs/operators'; const mockPackage = new Proxy({ raw: {} as any }, { get: (obj, prop) => obj.raw[prop] }); -jest.mock('../../../../utils/package_json', () => ({ pkg: mockPackage })); +jest.mock('../../../utils/package_json', () => ({ pkg: mockPackage })); -import { schema, Type, TypeOf } from '../schema'; +import { schema, Type, TypeOf } from './schema'; -import { ConfigService, ObjectToConfigAdapter } from '..'; -import { logger } from '../../logging/__mocks__'; -import { Env } from '../env'; +import { ConfigService, Env, ObjectToConfigAdapter } from '.'; +import { logger } from '../logging/__mocks__'; import { getEnvOptions } from './__mocks__/env'; const emptyArgv = getEnvOptions(); diff --git a/src/core/server/config/__tests__/ensure_deep_object.test.ts b/src/core/server/config/ensure_deep_object.test.ts similarity index 98% rename from src/core/server/config/__tests__/ensure_deep_object.test.ts rename to src/core/server/config/ensure_deep_object.test.ts index 40c0732266073..5a520fbeef316 100644 --- a/src/core/server/config/__tests__/ensure_deep_object.test.ts +++ b/src/core/server/config/ensure_deep_object.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { ensureDeepObject } from '../ensure_deep_object'; +import { ensureDeepObject } from './ensure_deep_object'; test('flat object', () => { const obj = { diff --git a/src/core/server/config/__tests__/env.test.ts b/src/core/server/config/env.test.ts similarity index 59% rename from src/core/server/config/__tests__/env.test.ts rename to src/core/server/config/env.test.ts index 26163c82c8464..56ff576fd8f31 100644 --- a/src/core/server/config/__tests__/env.test.ts +++ b/src/core/server/config/env.test.ts @@ -30,9 +30,10 @@ jest.mock('path', () => ({ })); const mockPackage = new Proxy({ raw: {} as any }, { get: (obj, prop) => obj.raw[prop] }); -jest.mock('../../../../utils/package_json', () => ({ pkg: mockPackage })); +jest.mock('../../../utils/package_json', () => ({ pkg: mockPackage })); -import { Env } from '../env'; +import { Env } from '.'; +import { getEnvOptions } from './__mocks__/env'; test('correctly creates default environment in dev mode.', () => { mockPackage.raw = { @@ -40,11 +41,12 @@ test('correctly creates default environment in dev mode.', () => { version: 'some-version', }; - const defaultEnv = Env.createDefault({ - cliArgs: { dev: true, someArg: 1, someOtherArg: '2' }, - configs: ['/test/cwd/config/kibana.yml'], - isDevClusterMaster: true, - }); + const defaultEnv = Env.createDefault( + getEnvOptions({ + configs: ['/test/cwd/config/kibana.yml'], + isDevClusterMaster: true, + }) + ); expect(defaultEnv).toMatchSnapshot('env properties'); }); @@ -60,11 +62,12 @@ test('correctly creates default environment in prod distributable mode.', () => }, }; - const defaultEnv = Env.createDefault({ - cliArgs: { dev: false, someArg: 1, someOtherArg: '2' }, - configs: ['/some/other/path/some-kibana.yml'], - isDevClusterMaster: false, - }); + const defaultEnv = Env.createDefault( + getEnvOptions({ + cliArgs: { dev: false }, + configs: ['/some/other/path/some-kibana.yml'], + }) + ); expect(defaultEnv).toMatchSnapshot('env properties'); }); @@ -80,15 +83,45 @@ test('correctly creates default environment in prod non-distributable mode.', () }, }; - const defaultEnv = Env.createDefault({ - cliArgs: { dev: false, someArg: 1, someOtherArg: '2' }, - configs: ['/some/other/path/some-kibana.yml'], - isDevClusterMaster: false, - }); + const defaultEnv = Env.createDefault( + getEnvOptions({ + cliArgs: { dev: false }, + configs: ['/some/other/path/some-kibana.yml'], + }) + ); expect(defaultEnv).toMatchSnapshot('env properties'); }); +test('correctly creates default environment if `--env.name` is supplied.', () => { + mockPackage.raw = { + branch: 'feature-v1', + version: 'v1', + build: { + distributable: false, + number: 100, + sha: 'feature-v1-build-sha', + }, + }; + + const defaultDevEnv = Env.createDefault( + getEnvOptions({ + cliArgs: { envName: 'development' }, + configs: ['/some/other/path/some-kibana.yml'], + }) + ); + + const defaultProdEnv = Env.createDefault( + getEnvOptions({ + cliArgs: { dev: false, envName: 'production' }, + configs: ['/some/other/path/some-kibana.yml'], + }) + ); + + expect(defaultDevEnv).toMatchSnapshot('dev env properties'); + expect(defaultProdEnv).toMatchSnapshot('prod env properties'); +}); + test('correctly creates environment with constructor.', () => { mockPackage.raw = { branch: 'feature-v1', @@ -100,11 +133,13 @@ test('correctly creates environment with constructor.', () => { }, }; - const env = new Env('/some/home/dir', { - cliArgs: { dev: false, someArg: 1, someOtherArg: '2' }, - configs: ['/some/other/path/some-kibana.yml'], - isDevClusterMaster: false, - }); + const env = new Env( + '/some/home/dir', + getEnvOptions({ + cliArgs: { dev: false }, + configs: ['/some/other/path/some-kibana.yml'], + }) + ); expect(env).toMatchSnapshot('env properties'); }); diff --git a/src/core/server/config/env.ts b/src/core/server/config/env.ts index 56d6c1ae94a0c..f7b497403a28a 100644 --- a/src/core/server/config/env.ts +++ b/src/core/server/config/env.ts @@ -17,7 +17,6 @@ * under the License. */ -import { EventEmitter } from 'events'; import { resolve } from 'path'; import process from 'process'; @@ -38,10 +37,20 @@ interface EnvironmentMode { export interface EnvOptions { configs: string[]; - cliArgs: Record; + cliArgs: CliArgs; isDevClusterMaster: boolean; } +export interface CliArgs { + dev: boolean; + envName?: string; + quiet: boolean; + silent: boolean; + watch: boolean; + repl: boolean; + basePath: boolean; +} + export class Env { /** * @internal @@ -66,15 +75,10 @@ export class Env { */ public readonly mode: Readonly; - /** - * @internal - */ - public readonly legacy: EventEmitter; - /** * Arguments provided through command line. */ - public readonly cliArgs: Readonly>; + public readonly cliArgs: Readonly; /** * Paths to the configuration files. @@ -100,10 +104,11 @@ export class Env { this.configs = Object.freeze(options.configs); this.isDevClusterMaster = options.isDevClusterMaster; + const isDevMode = this.cliArgs.dev || this.cliArgs.envName === 'development'; this.mode = Object.freeze({ - dev: this.cliArgs.dev, - name: this.cliArgs.dev ? 'development' : 'production', - prod: !this.cliArgs.dev, + dev: isDevMode, + name: isDevMode ? 'development' : 'production', + prod: !isDevMode, }); const isKibanaDistributable = pkg.build && pkg.build.distributable === true; @@ -113,7 +118,5 @@ export class Env { buildSha: isKibanaDistributable ? pkg.build.sha : 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', version: pkg.version, }); - - this.legacy = new EventEmitter(); } } diff --git a/src/core/server/config/index.ts b/src/core/server/config/index.ts index a5f535d12db1e..bbddec03a0f41 100644 --- a/src/core/server/config/index.ts +++ b/src/core/server/config/index.ts @@ -22,6 +22,5 @@ export { RawConfigService } from './raw_config_service'; export { Config, ConfigPath } from './config'; /** @internal */ export { ObjectToConfigAdapter } from './object_to_config_adapter'; -export { Env } from './env'; +export { Env, CliArgs } from './env'; export { ConfigWithSchema } from './config_with_schema'; -export { getConfigFromFiles } from './read_config'; diff --git a/src/core/server/config/__tests__/raw_config_service.test.ts b/src/core/server/config/raw_config_service.test.ts similarity index 98% rename from src/core/server/config/__tests__/raw_config_service.test.ts rename to src/core/server/config/raw_config_service.test.ts index 66cc31bc77774..eb5c212a31eb9 100644 --- a/src/core/server/config/__tests__/raw_config_service.test.ts +++ b/src/core/server/config/raw_config_service.test.ts @@ -19,12 +19,12 @@ const mockGetConfigFromFiles = jest.fn(); -jest.mock('../read_config', () => ({ +jest.mock('./read_config', () => ({ getConfigFromFiles: mockGetConfigFromFiles, })); import { first } from 'rxjs/operators'; -import { RawConfigService } from '../raw_config_service'; +import { RawConfigService } from '.'; const configFile = '/config/kibana.yml'; const anotherConfigFile = '/config/kibana.dev.yml'; diff --git a/src/core/server/config/__tests__/read_config.test.ts b/src/core/server/config/read_config.test.ts similarity index 98% rename from src/core/server/config/__tests__/read_config.test.ts rename to src/core/server/config/read_config.test.ts index b9aa3871b1794..46b75f28eb987 100644 --- a/src/core/server/config/__tests__/read_config.test.ts +++ b/src/core/server/config/read_config.test.ts @@ -18,7 +18,7 @@ */ import { relative, resolve } from 'path'; -import { getConfigFromFiles } from '../read_config'; +import { getConfigFromFiles } from './read_config'; const fixtureFile = (name: string) => `${__dirname}/__fixtures__/${name}`; diff --git a/src/core/server/config/schema/byte_size_value/__tests__/__snapshots__/index.test.ts.snap b/src/core/server/config/schema/byte_size_value/__snapshots__/index.test.ts.snap similarity index 100% rename from src/core/server/config/schema/byte_size_value/__tests__/__snapshots__/index.test.ts.snap rename to src/core/server/config/schema/byte_size_value/__snapshots__/index.test.ts.snap diff --git a/src/core/server/config/schema/byte_size_value/__tests__/index.test.ts b/src/core/server/config/schema/byte_size_value/index.test.ts similarity index 99% rename from src/core/server/config/schema/byte_size_value/__tests__/index.test.ts rename to src/core/server/config/schema/byte_size_value/index.test.ts index ece8769248152..46ed96c83dd1f 100644 --- a/src/core/server/config/schema/byte_size_value/__tests__/index.test.ts +++ b/src/core/server/config/schema/byte_size_value/index.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { ByteSizeValue } from '../'; +import { ByteSizeValue } from '.'; describe('parsing units', () => { test('bytes', () => { diff --git a/src/core/server/config/schema/byte_size_value/index.ts b/src/core/server/config/schema/byte_size_value/index.ts index 61ba879a5c926..fb0105503a149 100644 --- a/src/core/server/config/schema/byte_size_value/index.ts +++ b/src/core/server/config/schema/byte_size_value/index.ts @@ -36,8 +36,7 @@ export class ByteSizeValue { const match = /([1-9][0-9]*)(b|kb|mb|gb)/.exec(text); if (!match) { throw new Error( - `could not parse byte size value [${text}]. value must start with a ` + - `number and end with bytes size unit, e.g. 10kb, 23mb, 3gb, 239493b` + `could not parse byte size value [${text}]. Value must be a safe positive integer.` ); } diff --git a/src/core/server/config/schema/errors/__tests__/schema_error.test.ts b/src/core/server/config/schema/errors/schema_error.test.ts similarity index 98% rename from src/core/server/config/schema/errors/__tests__/schema_error.test.ts rename to src/core/server/config/schema/errors/schema_error.test.ts index 15ce626621b58..0f632b781e9a6 100644 --- a/src/core/server/config/schema/errors/__tests__/schema_error.test.ts +++ b/src/core/server/config/schema/errors/schema_error.test.ts @@ -18,7 +18,7 @@ */ import { relative } from 'path'; -import { SchemaError } from '..'; +import { SchemaError } from '.'; /** * Make all paths in stacktrace relative. diff --git a/src/core/server/config/schema/types/__tests__/__snapshots__/any_type.test.ts.snap b/src/core/server/config/schema/types/__snapshots__/any_type.test.ts.snap similarity index 100% rename from src/core/server/config/schema/types/__tests__/__snapshots__/any_type.test.ts.snap rename to src/core/server/config/schema/types/__snapshots__/any_type.test.ts.snap diff --git a/src/core/server/config/schema/types/__tests__/__snapshots__/array_type.test.ts.snap b/src/core/server/config/schema/types/__snapshots__/array_type.test.ts.snap similarity index 100% rename from src/core/server/config/schema/types/__tests__/__snapshots__/array_type.test.ts.snap rename to src/core/server/config/schema/types/__snapshots__/array_type.test.ts.snap diff --git a/src/core/server/config/schema/types/__tests__/__snapshots__/boolean_type.test.ts.snap b/src/core/server/config/schema/types/__snapshots__/boolean_type.test.ts.snap similarity index 100% rename from src/core/server/config/schema/types/__tests__/__snapshots__/boolean_type.test.ts.snap rename to src/core/server/config/schema/types/__snapshots__/boolean_type.test.ts.snap diff --git a/src/core/server/config/schema/types/__tests__/__snapshots__/byte_size_type.test.ts.snap b/src/core/server/config/schema/types/__snapshots__/byte_size_type.test.ts.snap similarity index 100% rename from src/core/server/config/schema/types/__tests__/__snapshots__/byte_size_type.test.ts.snap rename to src/core/server/config/schema/types/__snapshots__/byte_size_type.test.ts.snap diff --git a/src/core/server/config/schema/types/__tests__/__snapshots__/conditional_type.test.ts.snap b/src/core/server/config/schema/types/__snapshots__/conditional_type.test.ts.snap similarity index 100% rename from src/core/server/config/schema/types/__tests__/__snapshots__/conditional_type.test.ts.snap rename to src/core/server/config/schema/types/__snapshots__/conditional_type.test.ts.snap diff --git a/src/core/server/config/schema/types/__tests__/__snapshots__/duration_type.test.ts.snap b/src/core/server/config/schema/types/__snapshots__/duration_type.test.ts.snap similarity index 100% rename from src/core/server/config/schema/types/__tests__/__snapshots__/duration_type.test.ts.snap rename to src/core/server/config/schema/types/__snapshots__/duration_type.test.ts.snap diff --git a/src/core/server/config/schema/types/__tests__/__snapshots__/literal_type.test.ts.snap b/src/core/server/config/schema/types/__snapshots__/literal_type.test.ts.snap similarity index 100% rename from src/core/server/config/schema/types/__tests__/__snapshots__/literal_type.test.ts.snap rename to src/core/server/config/schema/types/__snapshots__/literal_type.test.ts.snap diff --git a/src/core/server/config/schema/types/__tests__/__snapshots__/map_of_type.test.ts.snap b/src/core/server/config/schema/types/__snapshots__/map_of_type.test.ts.snap similarity index 100% rename from src/core/server/config/schema/types/__tests__/__snapshots__/map_of_type.test.ts.snap rename to src/core/server/config/schema/types/__snapshots__/map_of_type.test.ts.snap diff --git a/src/core/server/config/schema/types/__tests__/__snapshots__/maybe_type.test.ts.snap b/src/core/server/config/schema/types/__snapshots__/maybe_type.test.ts.snap similarity index 100% rename from src/core/server/config/schema/types/__tests__/__snapshots__/maybe_type.test.ts.snap rename to src/core/server/config/schema/types/__snapshots__/maybe_type.test.ts.snap diff --git a/src/core/server/config/schema/types/__tests__/__snapshots__/number_type.test.ts.snap b/src/core/server/config/schema/types/__snapshots__/number_type.test.ts.snap similarity index 100% rename from src/core/server/config/schema/types/__tests__/__snapshots__/number_type.test.ts.snap rename to src/core/server/config/schema/types/__snapshots__/number_type.test.ts.snap diff --git a/src/core/server/config/schema/types/__tests__/__snapshots__/object_type.test.ts.snap b/src/core/server/config/schema/types/__snapshots__/object_type.test.ts.snap similarity index 100% rename from src/core/server/config/schema/types/__tests__/__snapshots__/object_type.test.ts.snap rename to src/core/server/config/schema/types/__snapshots__/object_type.test.ts.snap diff --git a/src/core/server/config/schema/types/__tests__/__snapshots__/one_of_type.test.ts.snap b/src/core/server/config/schema/types/__snapshots__/one_of_type.test.ts.snap similarity index 100% rename from src/core/server/config/schema/types/__tests__/__snapshots__/one_of_type.test.ts.snap rename to src/core/server/config/schema/types/__snapshots__/one_of_type.test.ts.snap diff --git a/src/core/server/config/schema/types/__tests__/__snapshots__/string_type.test.ts.snap b/src/core/server/config/schema/types/__snapshots__/string_type.test.ts.snap similarity index 100% rename from src/core/server/config/schema/types/__tests__/__snapshots__/string_type.test.ts.snap rename to src/core/server/config/schema/types/__snapshots__/string_type.test.ts.snap diff --git a/src/core/server/config/schema/types/__tests__/any_type.test.ts b/src/core/server/config/schema/types/any_type.test.ts similarity index 98% rename from src/core/server/config/schema/types/__tests__/any_type.test.ts rename to src/core/server/config/schema/types/any_type.test.ts index 6f39f3deab5fd..4d68c860ba13d 100644 --- a/src/core/server/config/schema/types/__tests__/any_type.test.ts +++ b/src/core/server/config/schema/types/any_type.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { schema } from '../..'; +import { schema } from '..'; test('works for any value', () => { expect(schema.any().validate(true)).toBe(true); diff --git a/src/core/server/config/schema/types/__tests__/array_type.test.ts b/src/core/server/config/schema/types/array_type.test.ts similarity index 99% rename from src/core/server/config/schema/types/__tests__/array_type.test.ts rename to src/core/server/config/schema/types/array_type.test.ts index f1fb124a95ede..c6943e0d1b5f3 100644 --- a/src/core/server/config/schema/types/__tests__/array_type.test.ts +++ b/src/core/server/config/schema/types/array_type.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { schema } from '../..'; +import { schema } from '..'; test('returns value if it matches the type', () => { const type = schema.arrayOf(schema.string()); diff --git a/src/core/server/config/schema/types/__tests__/boolean_type.test.ts b/src/core/server/config/schema/types/boolean_type.test.ts similarity index 98% rename from src/core/server/config/schema/types/__tests__/boolean_type.test.ts rename to src/core/server/config/schema/types/boolean_type.test.ts index bfd4259af387e..d6e274f05e3ff 100644 --- a/src/core/server/config/schema/types/__tests__/boolean_type.test.ts +++ b/src/core/server/config/schema/types/boolean_type.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { schema } from '../..'; +import { schema } from '..'; test('returns value by default', () => { expect(schema.boolean().validate(true)).toBe(true); diff --git a/src/core/server/config/schema/types/__tests__/byte_size_type.test.ts b/src/core/server/config/schema/types/byte_size_type.test.ts similarity index 97% rename from src/core/server/config/schema/types/__tests__/byte_size_type.test.ts rename to src/core/server/config/schema/types/byte_size_type.test.ts index 786b996ae5687..67eae1e7c382a 100644 --- a/src/core/server/config/schema/types/__tests__/byte_size_type.test.ts +++ b/src/core/server/config/schema/types/byte_size_type.test.ts @@ -17,8 +17,8 @@ * under the License. */ -import { schema } from '../..'; -import { ByteSizeValue } from '../../byte_size_value'; +import { schema } from '..'; +import { ByteSizeValue } from '../byte_size_value'; const { byteSize } = schema; diff --git a/src/core/server/config/schema/types/__tests__/conditional_type.test.ts b/src/core/server/config/schema/types/conditional_type.test.ts similarity index 99% rename from src/core/server/config/schema/types/__tests__/conditional_type.test.ts rename to src/core/server/config/schema/types/conditional_type.test.ts index 112ee874afa7b..a72c3463e00cb 100644 --- a/src/core/server/config/schema/types/__tests__/conditional_type.test.ts +++ b/src/core/server/config/schema/types/conditional_type.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { schema } from '../..'; +import { schema } from '..'; test('required by default', () => { const type = schema.conditional( diff --git a/src/core/server/config/schema/types/__tests__/duration_type.test.ts b/src/core/server/config/schema/types/duration_type.test.ts similarity index 98% rename from src/core/server/config/schema/types/__tests__/duration_type.test.ts rename to src/core/server/config/schema/types/duration_type.test.ts index 0c1d7e4dd8e50..9a21afc6cf40f 100644 --- a/src/core/server/config/schema/types/__tests__/duration_type.test.ts +++ b/src/core/server/config/schema/types/duration_type.test.ts @@ -18,7 +18,7 @@ */ import { duration as momentDuration } from 'moment'; -import { schema } from '../..'; +import { schema } from '..'; const { duration } = schema; diff --git a/src/core/server/config/schema/types/__tests__/literal_type.test.ts b/src/core/server/config/schema/types/literal_type.test.ts similarity index 98% rename from src/core/server/config/schema/types/__tests__/literal_type.test.ts rename to src/core/server/config/schema/types/literal_type.test.ts index 4d590200c1ccf..5ee0ac4edff68 100644 --- a/src/core/server/config/schema/types/__tests__/literal_type.test.ts +++ b/src/core/server/config/schema/types/literal_type.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { schema } from '../..'; +import { schema } from '..'; const { literal } = schema; diff --git a/src/core/server/config/schema/types/__tests__/map_of_type.test.ts b/src/core/server/config/schema/types/map_of_type.test.ts similarity index 98% rename from src/core/server/config/schema/types/__tests__/map_of_type.test.ts rename to src/core/server/config/schema/types/map_of_type.test.ts index ed4e12f162c59..1b72d39fcec26 100644 --- a/src/core/server/config/schema/types/__tests__/map_of_type.test.ts +++ b/src/core/server/config/schema/types/map_of_type.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { schema } from '../..'; +import { schema } from '..'; test('handles object as input', () => { const type = schema.mapOf(schema.string(), schema.string()); diff --git a/src/core/server/config/schema/types/__tests__/maybe_type.test.ts b/src/core/server/config/schema/types/maybe_type.test.ts similarity index 98% rename from src/core/server/config/schema/types/__tests__/maybe_type.test.ts rename to src/core/server/config/schema/types/maybe_type.test.ts index 950987763baf1..b29f504c03b32 100644 --- a/src/core/server/config/schema/types/__tests__/maybe_type.test.ts +++ b/src/core/server/config/schema/types/maybe_type.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { schema } from '../..'; +import { schema } from '..'; test('returns value if specified', () => { const type = schema.maybe(schema.string()); diff --git a/src/core/server/config/schema/types/__tests__/number_type.test.ts b/src/core/server/config/schema/types/number_type.test.ts similarity index 98% rename from src/core/server/config/schema/types/__tests__/number_type.test.ts rename to src/core/server/config/schema/types/number_type.test.ts index dd6be2631d28c..b85d5113563eb 100644 --- a/src/core/server/config/schema/types/__tests__/number_type.test.ts +++ b/src/core/server/config/schema/types/number_type.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { schema } from '../..'; +import { schema } from '..'; test('returns value by default', () => { expect(schema.number().validate(4)).toBe(4); diff --git a/src/core/server/config/schema/types/__tests__/object_type.test.ts b/src/core/server/config/schema/types/object_type.test.ts similarity index 99% rename from src/core/server/config/schema/types/__tests__/object_type.test.ts rename to src/core/server/config/schema/types/object_type.test.ts index ec54528c292a0..e0eaabadb8ef5 100644 --- a/src/core/server/config/schema/types/__tests__/object_type.test.ts +++ b/src/core/server/config/schema/types/object_type.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { schema } from '../..'; +import { schema } from '..'; test('returns value by default', () => { const type = schema.object({ diff --git a/src/core/server/config/schema/types/__tests__/one_of_type.test.ts b/src/core/server/config/schema/types/one_of_type.test.ts similarity index 99% rename from src/core/server/config/schema/types/__tests__/one_of_type.test.ts rename to src/core/server/config/schema/types/one_of_type.test.ts index e2f0f9688544a..72119e761590b 100644 --- a/src/core/server/config/schema/types/__tests__/one_of_type.test.ts +++ b/src/core/server/config/schema/types/one_of_type.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { schema } from '../..'; +import { schema } from '..'; test('handles string', () => { expect(schema.oneOf([schema.string()]).validate('test')).toBe('test'); diff --git a/src/core/server/config/schema/types/__tests__/string_type.test.ts b/src/core/server/config/schema/types/string_type.test.ts similarity index 99% rename from src/core/server/config/schema/types/__tests__/string_type.test.ts rename to src/core/server/config/schema/types/string_type.test.ts index f9415a0ac2506..193d85d290731 100644 --- a/src/core/server/config/schema/types/__tests__/string_type.test.ts +++ b/src/core/server/config/schema/types/string_type.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { schema } from '../..'; +import { schema } from '..'; test('returns value is string and defined', () => { expect(schema.string().validate('test')).toBe('test'); diff --git a/src/core/server/http/__tests__/__snapshots__/http_config.test.ts.snap b/src/core/server/http/__snapshots__/http_config.test.ts.snap similarity index 99% rename from src/core/server/http/__tests__/__snapshots__/http_config.test.ts.snap rename to src/core/server/http/__snapshots__/http_config.test.ts.snap index 6c38ae7ecf5d6..d7fe10b1c417b 100644 --- a/src/core/server/http/__tests__/__snapshots__/http_config.test.ts.snap +++ b/src/core/server/http/__snapshots__/http_config.test.ts.snap @@ -11,6 +11,7 @@ Object { exports[`has defaults for config 1`] = ` Object { + "autoListen": true, "cors": false, "host": "localhost", "maxPayload": ByteSizeValue { diff --git a/src/core/server/http/__tests__/__snapshots__/http_server.test.ts.snap b/src/core/server/http/__snapshots__/http_server.test.ts.snap similarity index 81% rename from src/core/server/http/__tests__/__snapshots__/http_server.test.ts.snap rename to src/core/server/http/__snapshots__/http_server.test.ts.snap index 3060d7b468960..8e868e803602f 100644 --- a/src/core/server/http/__tests__/__snapshots__/http_server.test.ts.snap +++ b/src/core/server/http/__snapshots__/http_server.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`broadcasts server and connection options to the legacy "channel" 1`] = ` +exports[`returns server and connection options on start 1`] = ` Object { "host": "127.0.0.1", "port": 12345, diff --git a/src/core/server/http/__tests__/__snapshots__/http_service.test.ts.snap b/src/core/server/http/__snapshots__/http_service.test.ts.snap similarity index 100% rename from src/core/server/http/__tests__/__snapshots__/http_service.test.ts.snap rename to src/core/server/http/__snapshots__/http_service.test.ts.snap diff --git a/src/core/server/http/__tests__/__snapshots__/https_redirect_server.test.ts.snap b/src/core/server/http/__snapshots__/https_redirect_server.test.ts.snap similarity index 100% rename from src/core/server/http/__tests__/__snapshots__/https_redirect_server.test.ts.snap rename to src/core/server/http/__snapshots__/https_redirect_server.test.ts.snap diff --git a/src/core/server/http/base_path_proxy_server.ts b/src/core/server/http/base_path_proxy_server.ts index f4a9b59b77b10..b0c2144d7189a 100644 --- a/src/core/server/http/base_path_proxy_server.ts +++ b/src/core/server/http/base_path_proxy_server.ts @@ -29,8 +29,6 @@ import { createServer, getServerOptions } from './http_tools'; const alphabet = 'abcdefghijklmnopqrztuvwxyz'.split(''); export interface BasePathProxyServerOptions { - httpConfig: HttpConfig; - devConfig: DevConfig; shouldRedirectFromOldBasePath: (path: string) => boolean; blockUntil: () => Promise; } @@ -40,34 +38,38 @@ export class BasePathProxyServer { private httpsAgent?: HttpsAgent; get basePath() { - return this.options.httpConfig.basePath; + return this.httpConfig.basePath; } get targetPort() { - return this.options.devConfig.basePathProxyTargetPort; + return this.devConfig.basePathProxyTargetPort; } - constructor(private readonly log: Logger, private readonly options: BasePathProxyServerOptions) { + constructor( + private readonly log: Logger, + private readonly httpConfig: HttpConfig, + private readonly devConfig: DevConfig + ) { const ONE_GIGABYTE = 1024 * 1024 * 1024; - options.httpConfig.maxPayload = new ByteSizeValue(ONE_GIGABYTE); + httpConfig.maxPayload = new ByteSizeValue(ONE_GIGABYTE); - if (!options.httpConfig.basePath) { - options.httpConfig.basePath = `/${sample(alphabet, 3).join('')}`; + if (!httpConfig.basePath) { + httpConfig.basePath = `/${sample(alphabet, 3).join('')}`; } } - public async start() { - const { httpConfig } = this.options; + public async start(options: Readonly) { + this.log.debug('starting basepath proxy server'); - const options = getServerOptions(httpConfig); - this.server = createServer(options); + const serverOptions = getServerOptions(this.httpConfig); + this.server = createServer(serverOptions); // Register hapi plugin that adds proxying functionality. It can be configured // through the route configuration object (see { handler: { proxy: ... } }). await this.server.register({ plugin: require('h2o2-latest') }); - if (httpConfig.ssl.enabled) { - const tlsOptions = options.tls as TlsOptions; + if (this.httpConfig.ssl.enabled) { + const tlsOptions = serverOptions.tls as TlsOptions; this.httpsAgent = new HttpsAgent({ ca: tlsOptions.ca, cert: tlsOptions.cert, @@ -77,40 +79,42 @@ export class BasePathProxyServer { }); } - this.setupRoutes(); + this.setupRoutes(options); + + await this.server.start(); this.log.info( - `starting basepath proxy server at ${this.server.info.uri}${httpConfig.basePath}` + `basepath proxy server running at ${this.server.info.uri}${this.httpConfig.basePath}` ); - - await this.server.start(); } public async stop() { - this.log.info('stopping basepath proxy server'); - - if (this.server !== undefined) { - await this.server.stop(); - this.server = undefined; + if (this.server === undefined) { + return; } + this.log.debug('stopping basepath proxy server'); + await this.server.stop(); + this.server = undefined; + if (this.httpsAgent !== undefined) { this.httpsAgent.destroy(); this.httpsAgent = undefined; } } - private setupRoutes() { + private setupRoutes({ + blockUntil, + shouldRedirectFromOldBasePath, + }: Readonly) { if (this.server === undefined) { throw new Error(`Routes cannot be set up since server is not initialized.`); } - const { httpConfig, devConfig, blockUntil, shouldRedirectFromOldBasePath } = this.options; - // Always redirect from root URL to the URL with basepath. this.server.route({ handler: (request, responseToolkit) => { - return responseToolkit.redirect(httpConfig.basePath); + return responseToolkit.redirect(this.httpConfig.basePath); }, method: 'GET', path: '/', @@ -122,7 +126,7 @@ export class BasePathProxyServer { agent: this.httpsAgent, host: this.server.info.host, passThrough: true, - port: devConfig.basePathProxyTargetPort, + port: this.devConfig.basePathProxyTargetPort, protocol: this.server.info.protocol, xforward: true, }, @@ -138,7 +142,7 @@ export class BasePathProxyServer { }, ], }, - path: `${httpConfig.basePath}/{kbnPath*}`, + path: `${this.httpConfig.basePath}/{kbnPath*}`, }); // It may happen that basepath has changed, but user still uses the old one, @@ -152,7 +156,7 @@ export class BasePathProxyServer { const isBasepathLike = oldBasePath.length === 3; return isGet && isBasepathLike && shouldRedirectFromOldBasePath(kbnPath) - ? responseToolkit.redirect(`${httpConfig.basePath}/${kbnPath}`) + ? responseToolkit.redirect(`${this.httpConfig.basePath}/${kbnPath}`) : responseToolkit.response('Not Found').code(404); }, method: '*', diff --git a/src/core/server/http/__tests__/http_config.test.ts b/src/core/server/http/http_config.test.ts similarity index 99% rename from src/core/server/http/__tests__/http_config.test.ts rename to src/core/server/http/http_config.test.ts index 45bd8962fc0df..54d28ef921fcf 100644 --- a/src/core/server/http/__tests__/http_config.test.ts +++ b/src/core/server/http/http_config.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { HttpConfig } from '../http_config'; +import { HttpConfig } from '.'; test('has defaults for config', () => { const httpSchema = HttpConfig.schema; diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts index 5d1504008027b..67578ecc1559c 100644 --- a/src/core/server/http/http_config.ts +++ b/src/core/server/http/http_config.ts @@ -28,6 +28,7 @@ const match = (regex: RegExp, errorMsg: string) => (str: string) => const createHttpSchema = schema.object( { + autoListen: schema.boolean({ defaultValue: true }), basePath: schema.maybe( schema.string({ validate: match(validBasePathRegex, "must start with a slash, don't end with one"), @@ -90,6 +91,7 @@ export class HttpConfig { */ public static schema = createHttpSchema; + public autoListen: boolean; public host: string; public port: number; public cors: boolean | { origin: string[] }; @@ -103,6 +105,7 @@ export class HttpConfig { * @internal */ constructor(config: HttpConfigType, env: Env) { + this.autoListen = config.autoListen; this.host = config.host; this.port = config.port; this.cors = config.cors; diff --git a/src/core/server/http/__tests__/http_server.test.ts b/src/core/server/http/http_server.test.ts similarity index 80% rename from src/core/server/http/__tests__/http_server.test.ts rename to src/core/server/http/http_server.test.ts index 7f49d153163a9..704a6ddf97aba 100644 --- a/src/core/server/http/__tests__/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { getEnvOptions } from '../../config/__tests__/__mocks__/env'; +import { Server } from 'http'; jest.mock('fs', () => ({ readFileSync: jest.fn(), @@ -26,23 +26,16 @@ jest.mock('fs', () => ({ import Chance from 'chance'; import supertest from 'supertest'; -import { Env } from '../../config'; -import { ByteSizeValue } from '../../config/schema'; -import { logger } from '../../logging/__mocks__'; -import { HttpConfig } from '../http_config'; -import { HttpServer } from '../http_server'; -import { Router } from '../router'; +import { HttpConfig, Router } from '.'; +import { ByteSizeValue } from '../config/schema'; +import { logger } from '../logging/__mocks__'; +import { HttpServer } from './http_server'; const chance = new Chance(); -let env: Env; let server: HttpServer; let config: HttpConfig; -function getServerListener(httpServer: HttpServer) { - return (httpServer as any).server.listener; -} - beforeEach(() => { config = { host: '127.0.0.1', @@ -51,8 +44,7 @@ beforeEach(() => { ssl: {}, } as HttpConfig; - env = new Env('/kibana', getEnvOptions()); - server = new HttpServer(logger.get(), env); + server = new HttpServer(logger.get()); }); afterEach(async () => { @@ -77,9 +69,9 @@ test('200 OK with body', async () => { server.registerRouter(router); - await server.start(config); + const { server: innerServer } = await server.start(config); - await supertest(getServerListener(server)) + await supertest(innerServer.listener) .get('/foo/') .expect(200) .then(res => { @@ -96,9 +88,9 @@ test('202 Accepted with body', async () => { server.registerRouter(router); - await server.start(config); + const { server: innerServer } = await server.start(config); - await supertest(getServerListener(server)) + await supertest(innerServer.listener) .get('/foo/') .expect(202) .then(res => { @@ -115,9 +107,9 @@ test('204 No content', async () => { server.registerRouter(router); - await server.start(config); + const { server: innerServer } = await server.start(config); - await supertest(getServerListener(server)) + await supertest(innerServer.listener) .get('/foo/') .expect(204) .then(res => { @@ -136,9 +128,9 @@ test('400 Bad request with error', async () => { server.registerRouter(router); - await server.start(config); + const { server: innerServer } = await server.start(config); - await supertest(getServerListener(server)) + await supertest(innerServer.listener) .get('/foo/') .expect(400) .then(res => { @@ -165,9 +157,9 @@ test('valid params', async () => { server.registerRouter(router); - await server.start(config); + const { server: innerServer } = await server.start(config); - await supertest(getServerListener(server)) + await supertest(innerServer.listener) .get('/foo/some-string') .expect(200) .then(res => { @@ -194,9 +186,9 @@ test('invalid params', async () => { server.registerRouter(router); - await server.start(config); + const { server: innerServer } = await server.start(config); - await supertest(getServerListener(server)) + await supertest(innerServer.listener) .get('/foo/some-string') .expect(400) .then(res => { @@ -226,9 +218,9 @@ test('valid query', async () => { server.registerRouter(router); - await server.start(config); + const { server: innerServer } = await server.start(config); - await supertest(getServerListener(server)) + await supertest(innerServer.listener) .get('/foo/?bar=test&quux=123') .expect(200) .then(res => { @@ -255,9 +247,9 @@ test('invalid query', async () => { server.registerRouter(router); - await server.start(config); + const { server: innerServer } = await server.start(config); - await supertest(getServerListener(server)) + await supertest(innerServer.listener) .get('/foo/?bar=test') .expect(400) .then(res => { @@ -287,9 +279,9 @@ test('valid body', async () => { server.registerRouter(router); - await server.start(config); + const { server: innerServer } = await server.start(config); - await supertest(getServerListener(server)) + await supertest(innerServer.listener) .post('/foo/') .send({ bar: 'test', @@ -320,9 +312,9 @@ test('invalid body', async () => { server.registerRouter(router); - await server.start(config); + const { server: innerServer } = await server.start(config); - await supertest(getServerListener(server)) + await supertest(innerServer.listener) .post('/foo/') .send({ bar: 'test' }) .expect(400) @@ -352,9 +344,9 @@ test('handles putting', async () => { server.registerRouter(router); - await server.start(config); + const { server: innerServer } = await server.start(config); - await supertest(getServerListener(server)) + await supertest(innerServer.listener) .put('/foo/') .send({ key: 'new value' }) .expect(200) @@ -382,9 +374,9 @@ test('handles deleting', async () => { server.registerRouter(router); - await server.start(config); + const { server: innerServer } = await server.start(config); - await supertest(getServerListener(server)) + await supertest(innerServer.listener) .delete('/foo/3') .expect(200) .then(res => { @@ -407,9 +399,9 @@ test('filtered headers', async () => { server.registerRouter(router); - await server.start(config); + const { server: innerServer } = await server.start(config); - await supertest(getServerListener(server)) + await supertest(innerServer.listener) .get('/foo/?bar=quux') .set('x-kibana-foo', 'bar') .set('x-kibana-bar', 'quux'); @@ -422,6 +414,7 @@ test('filtered headers', async () => { describe('with `basepath: /bar` and `rewriteBasePath: false`', () => { let configWithBasePath: HttpConfig; + let innerServerListener: Server; beforeEach(async () => { configWithBasePath = { @@ -438,29 +431,30 @@ describe('with `basepath: /bar` and `rewriteBasePath: false`', () => { server.registerRouter(router); - await server.start(configWithBasePath); + const { server: innerServer } = await server.start(configWithBasePath); + innerServerListener = innerServer.listener; }); test('/bar => 404', async () => { - await supertest(getServerListener(server)) + await supertest(innerServerListener) .get('/bar') .expect(404); }); test('/bar/ => 404', async () => { - await supertest(getServerListener(server)) + await supertest(innerServerListener) .get('/bar/') .expect(404); }); test('/bar/foo => 404', async () => { - await supertest(getServerListener(server)) + await supertest(innerServerListener) .get('/bar/foo') .expect(404); }); test('/ => /', async () => { - await supertest(getServerListener(server)) + await supertest(innerServerListener) .get('/') .expect(200) .then(res => { @@ -469,7 +463,7 @@ describe('with `basepath: /bar` and `rewriteBasePath: false`', () => { }); test('/foo => /foo', async () => { - await supertest(getServerListener(server)) + await supertest(innerServerListener) .get('/foo') .expect(200) .then(res => { @@ -480,6 +474,7 @@ describe('with `basepath: /bar` and `rewriteBasePath: false`', () => { describe('with `basepath: /bar` and `rewriteBasePath: true`', () => { let configWithBasePath: HttpConfig; + let innerServerListener: Server; beforeEach(async () => { configWithBasePath = { @@ -496,11 +491,12 @@ describe('with `basepath: /bar` and `rewriteBasePath: true`', () => { server.registerRouter(router); - await server.start(configWithBasePath); + const { server: innerServer } = await server.start(configWithBasePath); + innerServerListener = innerServer.listener; }); test('/bar => /', async () => { - await supertest(getServerListener(server)) + await supertest(innerServerListener) .get('/bar') .expect(200) .then(res => { @@ -509,7 +505,7 @@ describe('with `basepath: /bar` and `rewriteBasePath: true`', () => { }); test('/bar/ => /', async () => { - await supertest(getServerListener(server)) + await supertest(innerServerListener) .get('/bar/') .expect(200) .then(res => { @@ -518,7 +514,7 @@ describe('with `basepath: /bar` and `rewriteBasePath: true`', () => { }); test('/bar/foo => /foo', async () => { - await supertest(getServerListener(server)) + await supertest(innerServerListener) .get('/bar/foo') .expect(200) .then(res => { @@ -527,13 +523,13 @@ describe('with `basepath: /bar` and `rewriteBasePath: true`', () => { }); test('/ => 404', async () => { - await supertest(getServerListener(server)) + await supertest(innerServerListener) .get('/') .expect(404); }); test('/foo => 404', async () => { - await supertest(getServerListener(server)) + await supertest(innerServerListener) .get('/foo') .expect(404); }); @@ -564,21 +560,13 @@ describe('with defined `redirectHttpFromPort`', () => { }); }); -test('broadcasts server and connection options to the legacy "channel"', async () => { - const onConnectionListener = jest.fn(); - env.legacy.on('connection', onConnectionListener); - - expect(onConnectionListener).not.toHaveBeenCalled(); - - await server.start({ +test('returns server and connection options on start', async () => { + const { server: innerServer, options } = await server.start({ ...config, port: 12345, }); - expect(onConnectionListener).toHaveBeenCalledTimes(1); - - const [[{ options, server: rawServer }]] = onConnectionListener.mock.calls; - expect(rawServer).toBeDefined(); - expect(rawServer).toBe((server as any).server); + expect(innerServer).toBeDefined(); + expect(innerServer).toBe((server as any).server); expect(options).toMatchSnapshot(); }); diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 21cde147b8ea2..c828ff4df5408 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -17,20 +17,24 @@ * under the License. */ -import { Server } from 'hapi-latest'; +import { Server, ServerOptions } from 'hapi-latest'; import { modifyUrl } from '../../utils'; -import { Env } from '../config'; import { Logger } from '../logging'; import { HttpConfig } from './http_config'; import { createServer, getServerOptions } from './http_tools'; import { Router } from './router'; +export interface HttpServerInfo { + server: Server; + options: ServerOptions; +} + export class HttpServer { private server?: Server; private registeredRouters: Set = new Set(); - constructor(private readonly log: Logger, private readonly env: Env) {} + constructor(private readonly log: Logger) {} public isListening() { return this.server !== undefined && this.server.listener.listening; @@ -62,21 +66,18 @@ export class HttpServer { } } - // Notify legacy compatibility layer about HTTP(S) connection providing server - // instance with connection options so that we can properly bridge core and - // the "legacy" Kibana internally. - this.env.legacy.emit('connection', { - options: serverOptions, - server: this.server, - }); - await this.server.start(); - this.log.info( - `Server running at ${this.server.info.uri}${config.rewriteBasePath ? config.basePath : ''}`, - // The "legacy" Kibana will output log records with `listening` tag even if `quiet` logging mode is enabled. - { tags: ['listening'] } + this.log.debug( + `http server running at ${this.server.info.uri}${ + config.rewriteBasePath ? config.basePath : '' + }` ); + + // Return server instance with the connection options so that we can properly + // bridge core and the "legacy" Kibana internally. Once this bridge isn't + // needed anymore we shouldn't return anything from this method. + return { server: this.server, options: serverOptions }; } public async stop() { diff --git a/src/core/server/http/__tests__/http_service.test.ts b/src/core/server/http/http_service.test.ts similarity index 78% rename from src/core/server/http/__tests__/http_service.test.ts rename to src/core/server/http/http_service.test.ts index 0cacad8817468..a42ac26745c60 100644 --- a/src/core/server/http/__tests__/http_service.test.ts +++ b/src/core/server/http/http_service.test.ts @@ -17,22 +17,16 @@ * under the License. */ -import { getEnvOptions } from '../../config/__tests__/__mocks__/env'; - const mockHttpServer = jest.fn(); -jest.mock('../http_server', () => ({ +jest.mock('./http_server', () => ({ HttpServer: mockHttpServer, })); import { noop } from 'lodash'; import { BehaviorSubject } from 'rxjs'; - -import { Env } from '../../config'; -import { logger } from '../../logging/__mocks__'; -import { HttpConfig } from '../http_config'; -import { HttpService } from '../http_service'; -import { Router } from '../router'; +import { HttpConfig, HttpService, Router } from '.'; +import { logger } from '../logging/__mocks__'; beforeEach(() => { logger.mockClear(); @@ -55,11 +49,7 @@ test('creates and starts http server', async () => { }; mockHttpServer.mockImplementation(() => httpServer); - const service = new HttpService( - config$.asObservable(), - logger, - new Env('/kibana', getEnvOptions()) - ); + const service = new HttpService(config$.asObservable(), logger); expect(mockHttpServer.mock.instances.length).toBe(1); expect(httpServer.start).not.toHaveBeenCalled(); @@ -81,11 +71,7 @@ test('logs error if already started', async () => { }; mockHttpServer.mockImplementation(() => httpServer); - const service = new HttpService( - config$.asObservable(), - logger, - new Env('/kibana', getEnvOptions()) - ); + const service = new HttpService(config$.asObservable(), logger); await service.start(); @@ -104,11 +90,7 @@ test('stops http server', async () => { }; mockHttpServer.mockImplementation(() => httpServer); - const service = new HttpService( - config$.asObservable(), - logger, - new Env('/kibana', getEnvOptions()) - ); + const service = new HttpService(config$.asObservable(), logger); await service.start(); @@ -132,11 +114,7 @@ test('register route handler', () => { }; mockHttpServer.mockImplementation(() => httpServer); - const service = new HttpService( - config$.asObservable(), - logger, - new Env('/kibana', getEnvOptions()) - ); + const service = new HttpService(config$.asObservable(), logger); const router = new Router('/foo'); service.registerRouter(router); @@ -159,11 +137,7 @@ test('throws if registering route handler after http server is started', () => { }; mockHttpServer.mockImplementation(() => httpServer); - const service = new HttpService( - config$.asObservable(), - logger, - new Env('/kibana', getEnvOptions()) - ); + const service = new HttpService(config$.asObservable(), logger); const router = new Router('/foo'); service.registerRouter(router); @@ -171,3 +145,20 @@ test('throws if registering route handler after http server is started', () => { expect(httpServer.registerRouter).toHaveBeenCalledTimes(0); expect(logger.mockCollect()).toMatchSnapshot(); }); + +test('returns http server contract on start', async () => { + const httpServerContract = { + server: {}, + options: { someOption: true }, + }; + + mockHttpServer.mockImplementation(() => ({ + isListening: () => false, + start: jest.fn().mockReturnValue(httpServerContract), + stop: noop, + })); + + const service = new HttpService(new BehaviorSubject({ ssl: {} } as HttpConfig), logger); + + expect(await service.start()).toBe(httpServerContract); +}); diff --git a/src/core/server/http/http_service.ts b/src/core/server/http/http_service.ts index 3caae18e857b3..6972dfffbb1dd 100644 --- a/src/core/server/http/http_service.ts +++ b/src/core/server/http/http_service.ts @@ -21,24 +21,23 @@ import { Observable, Subscription } from 'rxjs'; import { first } from 'rxjs/operators'; import { CoreService } from '../../types/core_service'; -import { Env } from '../config'; import { Logger, LoggerFactory } from '../logging'; import { HttpConfig } from './http_config'; -import { HttpServer } from './http_server'; +import { HttpServer, HttpServerInfo } from './http_server'; import { HttpsRedirectServer } from './https_redirect_server'; import { Router } from './router'; -export class HttpService implements CoreService { +export class HttpService implements CoreService { private readonly httpServer: HttpServer; private readonly httpsRedirectServer: HttpsRedirectServer; private configSubscription?: Subscription; private readonly log: Logger; - constructor(private readonly config$: Observable, logger: LoggerFactory, env: Env) { + constructor(private readonly config$: Observable, logger: LoggerFactory) { this.log = logger.get('http'); - this.httpServer = new HttpServer(logger.get('http', 'server'), env); + this.httpServer = new HttpServer(logger.get('http', 'server')); this.httpsRedirectServer = new HttpsRedirectServer(logger.get('http', 'redirect', 'server')); } @@ -61,7 +60,7 @@ export class HttpService implements CoreService { await this.httpsRedirectServer.start(config); } - await this.httpServer.start(config); + return await this.httpServer.start(config); } public async stop() { diff --git a/src/core/server/http/__tests__/https_redirect_server.test.ts b/src/core/server/http/https_redirect_server.test.ts similarity index 92% rename from src/core/server/http/__tests__/https_redirect_server.test.ts rename to src/core/server/http/https_redirect_server.test.ts index c92691a679ef0..6d9443335a62b 100644 --- a/src/core/server/http/__tests__/https_redirect_server.test.ts +++ b/src/core/server/http/https_redirect_server.test.ts @@ -25,10 +25,10 @@ import Chance from 'chance'; import { Server } from 'http'; import supertest from 'supertest'; -import { ByteSizeValue } from '../../config/schema'; -import { logger } from '../../logging/__mocks__'; -import { HttpConfig } from '../http_config'; -import { HttpsRedirectServer } from '../https_redirect_server'; +import { HttpConfig } from '.'; +import { ByteSizeValue } from '../config/schema'; +import { logger } from '../logging/__mocks__'; +import { HttpsRedirectServer } from './https_redirect_server'; const chance = new Chance(); diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index e636fcd801eb5..3fd3715083416 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -19,20 +19,26 @@ import { Observable } from 'rxjs'; -import { Env } from '../config'; import { LoggerFactory } from '../logging'; import { HttpConfig } from './http_config'; import { HttpService } from './http_service'; +import { Router } from './router'; export { Router, KibanaRequest } from './router'; export { HttpService }; +export { HttpServerInfo } from './http_server'; +export { BasePathProxyServer } from './base_path_proxy_server'; export { HttpConfig }; export class HttpModule { public readonly service: HttpService; - constructor(readonly config$: Observable, logger: LoggerFactory, env: Env) { - this.service = new HttpService(this.config$, logger, env); + constructor(readonly config$: Observable, logger: LoggerFactory) { + this.service = new HttpService(this.config$, logger); + + const router = new Router('/core'); + router.get({ path: '/', validate: false }, async (req, res) => res.ok({ version: '0.0.1' })); + this.service.registerRouter(router); } } diff --git a/src/core/server/index.test.ts b/src/core/server/index.test.ts new file mode 100644 index 0000000000000..d6bd19d36ce3c --- /dev/null +++ b/src/core/server/index.test.ts @@ -0,0 +1,121 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const mockHttpService = { start: jest.fn(), stop: jest.fn(), registerRouter: jest.fn() }; +jest.mock('./http/http_service', () => ({ + HttpService: jest.fn(() => mockHttpService), +})); + +const mockLegacyService = { start: jest.fn(), stop: jest.fn() }; +jest.mock('./legacy_compat/legacy_service', () => ({ + LegacyService: jest.fn(() => mockLegacyService), +})); + +import { BehaviorSubject } from 'rxjs'; +import { Server } from '.'; +import { Env } from './config'; +import { getEnvOptions } from './config/__mocks__/env'; +import { logger } from './logging/__mocks__'; + +const mockConfigService = { atPath: jest.fn(), getUnusedPaths: jest.fn().mockReturnValue([]) }; +const env = new Env('.', getEnvOptions()); + +beforeEach(() => { + mockConfigService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); +}); + +afterEach(() => { + logger.mockClear(); + mockConfigService.atPath.mockReset(); + mockHttpService.start.mockReset(); + mockHttpService.stop.mockReset(); + mockLegacyService.start.mockReset(); + mockLegacyService.stop.mockReset(); +}); + +test('starts services on "start"', async () => { + const mockHttpServiceStartContract = { something: true }; + mockHttpService.start.mockReturnValue(Promise.resolve(mockHttpServiceStartContract)); + + const server = new Server(mockConfigService as any, logger, env); + + expect(mockHttpService.start).not.toHaveBeenCalled(); + expect(mockLegacyService.start).not.toHaveBeenCalled(); + + await server.start(); + + expect(mockHttpService.start).toHaveBeenCalledTimes(1); + expect(mockLegacyService.start).toHaveBeenCalledTimes(1); + expect(mockLegacyService.start).toHaveBeenCalledWith(mockHttpServiceStartContract); +}); + +test('does not fail on "start" if there are unused paths detected', async () => { + mockConfigService.getUnusedPaths.mockReturnValue(['some.path', 'another.path']); + + const server = new Server(mockConfigService as any, logger, env); + await expect(server.start()).resolves.toBeUndefined(); + expect(logger.mockCollect()).toMatchSnapshot('unused paths logs'); +}); + +test('does not start http service is `autoListen:false`', async () => { + mockConfigService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: false })); + + const server = new Server(mockConfigService as any, logger, env); + + expect(mockLegacyService.start).not.toHaveBeenCalled(); + + await server.start(); + + expect(mockHttpService.start).not.toHaveBeenCalled(); + expect(mockLegacyService.start).toHaveBeenCalledTimes(1); + expect(mockLegacyService.start).toHaveBeenCalledWith(undefined); +}); + +test('does not start http service if process is dev cluster master', async () => { + const server = new Server( + mockConfigService as any, + logger, + new Env('.', getEnvOptions({ isDevClusterMaster: true })) + ); + + expect(mockLegacyService.start).not.toHaveBeenCalled(); + + await server.start(); + + expect(mockHttpService.start).not.toHaveBeenCalled(); + expect(mockLegacyService.start).toHaveBeenCalledTimes(1); + expect(mockLegacyService.start).toHaveBeenCalledWith(undefined); +}); + +test('stops services on "stop"', async () => { + const mockHttpServiceStartContract = { something: true }; + mockHttpService.start.mockReturnValue(Promise.resolve(mockHttpServiceStartContract)); + + const server = new Server(mockConfigService as any, logger, env); + + await server.start(); + + expect(mockHttpService.stop).not.toHaveBeenCalled(); + expect(mockLegacyService.stop).not.toHaveBeenCalled(); + + await server.stop(); + + expect(mockHttpService.stop).toHaveBeenCalledTimes(1); + expect(mockLegacyService.stop).toHaveBeenCalledTimes(1); +}); diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 7d55670239f5e..ac645b2280041 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -17,29 +17,44 @@ * under the License. */ +export { bootstrap } from './bootstrap'; + +import { first } from 'rxjs/operators'; import { ConfigService, Env } from './config'; -import { HttpConfig, HttpModule, Router } from './http'; +import { HttpConfig, HttpModule, HttpServerInfo } from './http'; +import { LegacyCompatModule } from './legacy_compat'; import { Logger, LoggerFactory } from './logging'; export class Server { private readonly http: HttpModule; + private readonly legacy: LegacyCompatModule; private readonly log: Logger; - constructor(private readonly configService: ConfigService, logger: LoggerFactory, env: Env) { + constructor( + private readonly configService: ConfigService, + logger: LoggerFactory, + private readonly env: Env + ) { this.log = logger.get('server'); - const httpConfig$ = configService.atPath('server', HttpConfig); - this.http = new HttpModule(httpConfig$, logger, env); + this.http = new HttpModule(configService.atPath('server', HttpConfig), logger); + this.legacy = new LegacyCompatModule(configService, logger, env); } public async start() { - this.log.debug('starting server :tada:'); + this.log.debug('starting server'); - const router = new Router('/core'); - router.get({ path: '/', validate: false }, async (req, res) => res.ok({ version: '0.0.1' })); - this.http.service.registerRouter(router); + // We shouldn't start http service in two cases: + // 1. If `server.autoListen` is explicitly set to `false`. + // 2. When the process is run as dev cluster master in which case cluster manager + // will fork a dedicated process where http service will be started instead. + let httpServerInfo: HttpServerInfo | undefined; + const httpConfig = await this.http.config$.pipe(first()).toPromise(); + if (!this.env.isDevClusterMaster && httpConfig.autoListen) { + httpServerInfo = await this.http.service.start(); + } - await this.http.service.start(); + await this.legacy.service.start(httpServerInfo); const unhandledConfigPaths = await this.configService.getUnusedPaths(); if (unhandledConfigPaths.length > 0) { @@ -54,6 +69,7 @@ export class Server { public async stop() { this.log.debug('stopping server'); + await this.legacy.service.stop(); await this.http.service.stop(); } } diff --git a/src/core/server/legacy_compat/__snapshots__/legacy_service.test.ts.snap b/src/core/server/legacy_compat/__snapshots__/legacy_service.test.ts.snap new file mode 100644 index 0000000000000..99e6a29a8c5ee --- /dev/null +++ b/src/core/server/legacy_compat/__snapshots__/legacy_service.test.ts.snap @@ -0,0 +1,143 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`once LegacyService is started in \`devClusterMaster\` mode creates ClusterManager with base path proxy.: cluster manager with base path proxy 1`] = ` +Array [ + Array [ + Object { + "basePath": true, + "dev": true, + "quiet": true, + "repl": false, + "silent": false, + "watch": false, + }, + Object { + "server": Object { + "autoListen": true, + }, + }, + BasePathProxyServer { + "devConfig": Object { + "basePathProxyTargetPort": 100500, + }, + "httpConfig": Object { + "basePath": "/abc", + "maxPayload": ByteSizeValue { + "valueInBytes": 1073741824, + }, + }, + "log": Object { + "debug": [MockFunction] { + "calls": Array [ + Array [ + "starting legacy service", + ], + ], + "results": Array [ + Object { + "isThrow": false, + "value": undefined, + }, + ], + }, + "error": [MockFunction], + "fatal": [MockFunction], + "info": [MockFunction], + "log": [MockFunction], + "trace": [MockFunction], + "warn": [MockFunction], + }, + }, + ], +] +`; + +exports[`once LegacyService is started in \`devClusterMaster\` mode creates ClusterManager without base path proxy.: cluster manager without base path proxy 1`] = ` +Array [ + Array [ + Object { + "basePath": false, + "dev": true, + "quiet": false, + "repl": false, + "silent": true, + "watch": false, + }, + Object { + "server": Object { + "autoListen": true, + }, + }, + undefined, + ], +] +`; + +exports[`once LegacyService is started with connection info creates legacy kbnServer and closes it if \`listen\` fails. 1`] = `"something failed"`; + +exports[`once LegacyService is started with connection info proxy route responds with \`503\` if \`kbnServer\` is not ready yet.: 503 response 1`] = ` +Object { + "body": Array [ + Array [ + "Kibana server is not ready yet", + ], + ], + "code": Array [ + Array [ + 503, + ], + ], + "header": Array [ + Array [ + "Retry-After", + "30", + ], + ], +} +`; + +exports[`once LegacyService is started with connection info reconfigures logging configuration if new config is received.: applyLoggingConfiguration params 1`] = ` +Array [ + Array [ + Object { + "logging": Object { + "verbose": true, + }, + }, + ], +] +`; + +exports[`once LegacyService is started with connection info register proxy route.: proxy route options 1`] = ` +Array [ + Array [ + Object { + "handler": [Function], + "method": "*", + "options": Object { + "payload": Object { + "maxBytes": 9007199254740991, + "output": "stream", + "parse": false, + "timeout": false, + }, + }, + "path": "/{p*}", + }, + ], +] +`; + +exports[`once LegacyService is started with connection info throws if fails to retrieve initial config. 1`] = `"something failed"`; + +exports[`once LegacyService is started without connection info reconfigures logging configuration if new config is received.: applyLoggingConfiguration params 1`] = ` +Array [ + Array [ + Object { + "logging": Object { + "verbose": true, + }, + }, + ], +] +`; diff --git a/src/core/server/legacy_compat/__tests__/__snapshots__/legacy_platform_proxifier.test.ts.snap b/src/core/server/legacy_compat/__tests__/__snapshots__/legacy_platform_proxifier.test.ts.snap deleted file mode 100644 index eb58ca8cbc5fd..0000000000000 --- a/src/core/server/legacy_compat/__tests__/__snapshots__/legacy_platform_proxifier.test.ts.snap +++ /dev/null @@ -1,21 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`correctly binds to the server.: proxy route options 1`] = ` -Array [ - Array [ - Object { - "handler": [Function], - "method": "*", - "options": Object { - "payload": Object { - "maxBytes": 9007199254740991, - "output": "stream", - "parse": false, - "timeout": false, - }, - }, - "path": "/{p*}", - }, - ], -] -`; diff --git a/src/core/server/legacy_compat/config/__tests__/__snapshots__/legacy_object_to_config_adapter.test.ts.snap b/src/core/server/legacy_compat/config/__snapshots__/legacy_object_to_config_adapter.test.ts.snap similarity index 98% rename from src/core/server/legacy_compat/config/__tests__/__snapshots__/legacy_object_to_config_adapter.test.ts.snap rename to src/core/server/legacy_compat/config/__snapshots__/legacy_object_to_config_adapter.test.ts.snap index d03398e173e40..af2bfff0abfe3 100644 --- a/src/core/server/legacy_compat/config/__tests__/__snapshots__/legacy_object_to_config_adapter.test.ts.snap +++ b/src/core/server/legacy_compat/config/__snapshots__/legacy_object_to_config_adapter.test.ts.snap @@ -2,6 +2,7 @@ exports[`#get correctly handles server config. 1`] = ` Object { + "autoListen": true, "basePath": "/abc", "cors": false, "host": "host", diff --git a/src/core/server/legacy_compat/config/__tests__/legacy_object_to_config_adapter.test.ts b/src/core/server/legacy_compat/config/legacy_object_to_config_adapter.test.ts similarity index 98% rename from src/core/server/legacy_compat/config/__tests__/legacy_object_to_config_adapter.test.ts rename to src/core/server/legacy_compat/config/legacy_object_to_config_adapter.test.ts index b465a55be4243..afa3cf03fe9a9 100644 --- a/src/core/server/legacy_compat/config/__tests__/legacy_object_to_config_adapter.test.ts +++ b/src/core/server/legacy_compat/config/legacy_object_to_config_adapter.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { LegacyObjectToConfigAdapter } from '../legacy_object_to_config_adapter'; +import { LegacyObjectToConfigAdapter } from './legacy_object_to_config_adapter'; describe('#get', () => { test('correctly handles paths that do not exist.', () => { diff --git a/src/core/server/legacy_compat/config/legacy_object_to_config_adapter.ts b/src/core/server/legacy_compat/config/legacy_object_to_config_adapter.ts index ef07e86e5fefc..483e156f4697d 100644 --- a/src/core/server/legacy_compat/config/legacy_object_to_config_adapter.ts +++ b/src/core/server/legacy_compat/config/legacy_object_to_config_adapter.ts @@ -32,7 +32,7 @@ interface LegacyLoggingConfig { } /** - * Represents adapter between config provided by legacy platform and `RawConfig` + * Represents adapter between config provided by legacy platform and `Config` * supported by the current platform. */ export class LegacyObjectToConfigAdapter extends ObjectToConfigAdapter { @@ -59,6 +59,7 @@ export class LegacyObjectToConfigAdapter extends ObjectToConfigAdapter { // TODO: New platform uses just a subset of `server` config from the legacy platform, // new values will be exposed once we need them (eg. customResponseHeaders or xsrf). return { + autoListen: configValue.autoListen, basePath: configValue.basePath, cors: configValue.cors, host: configValue.host, diff --git a/src/core/server/legacy_compat/index.ts b/src/core/server/legacy_compat/index.ts index dc5db0ab1fb7a..3e10928aa3456 100644 --- a/src/core/server/legacy_compat/index.ts +++ b/src/core/server/legacy_compat/index.ts @@ -17,54 +17,17 @@ * under the License. */ -import { BehaviorSubject } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { ConfigService, Env } from '../config'; +import { LoggerFactory } from '../logging'; +import { LegacyService } from './legacy_service'; -/** @internal */ -export { LegacyPlatformProxifier } from './legacy_platform_proxifier'; -/** @internal */ export { LegacyObjectToConfigAdapter } from './config/legacy_object_to_config_adapter'; +export { LegacyService } from './legacy_service'; -import { LegacyObjectToConfigAdapter, LegacyPlatformProxifier } from '.'; -import { Env } from '../config'; -import { Root } from '../root'; -import { BasePathProxyRoot } from '../root/base_path_proxy_root'; +export class LegacyCompatModule { + public readonly service: LegacyService; -function initEnvironment(rawKbnServer: any, isDevClusterMaster = false) { - const env = Env.createDefault({ - // The core doesn't work with configs yet, everything is provided by the - // "legacy" Kibana, so we can have empty array here. - configs: [], - // `dev` is the only CLI argument we currently use. - cliArgs: { dev: rawKbnServer.config.get('env.dev') }, - isDevClusterMaster, - }); - - const legacyConfig$ = new BehaviorSubject>(rawKbnServer.config.get()); - return { - config$: legacyConfig$.pipe(map(legacyConfig => new LegacyObjectToConfigAdapter(legacyConfig))), - env, - // Propagates legacy config updates to the new platform. - updateConfig(legacyConfig: Record) { - legacyConfig$.next(legacyConfig); - }, - }; + constructor(private readonly configService: ConfigService, logger: LoggerFactory, env: Env) { + this.service = new LegacyService(env, logger, this.configService); + } } - -/** - * @internal - */ -export const injectIntoKbnServer = (rawKbnServer: any) => { - const { env, config$, updateConfig } = initEnvironment(rawKbnServer); - - rawKbnServer.newPlatform = { - // Custom HTTP Listener that will be used within legacy platform by HapiJS server. - proxyListener: new LegacyPlatformProxifier(new Root(config$, env), env), - updateConfig, - }; -}; - -export const createBasePathProxy = (rawKbnServer: any) => { - const { env, config$ } = initEnvironment(rawKbnServer, true /*isDevClusterMaster*/); - return new BasePathProxyRoot(config$, env); -}; diff --git a/src/core/server/legacy_compat/legacy_platform_proxifier.ts b/src/core/server/legacy_compat/legacy_platform_proxifier.ts deleted file mode 100644 index 8baa156266ef0..0000000000000 --- a/src/core/server/legacy_compat/legacy_platform_proxifier.ts +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { EventEmitter } from 'events'; -import { Server } from 'net'; - -import { Server as HapiServer, ServerOptions as HapiServerOptions } from 'hapi-latest'; -import { Env } from '../config'; -import { Logger } from '../logging'; -import { Root } from '../root'; - -interface ConnectionInfo { - server: HapiServer; - options: HapiServerOptions; -} - -/** - * List of the server events to be forwarded to the legacy platform. - */ -const ServerEventsToForward = [ - 'clientError', - 'close', - 'connection', - 'error', - 'listening', - 'upgrade', -]; - -/** - * Represents "proxy" between legacy and current platform. - * @internal - */ -export class LegacyPlatformProxifier extends EventEmitter { - private readonly eventHandlers: Map void>; - private readonly log: Logger; - private server?: Server; - - constructor(private readonly root: Root, private readonly env: Env) { - super(); - - this.log = root.logger.get('legacy-platform-proxifier'); - - // HapiJS expects that the following events will be generated by `listener`, see: - // https://github.com/hapijs/hapi/blob/v14.2.0/lib/connection.js. - this.eventHandlers = new Map( - ServerEventsToForward.map(eventName => { - return [ - eventName, - (...args: any[]) => { - this.log.debug(`Event is being forwarded: ${eventName}`); - this.emit(eventName, ...args); - }, - ] as [string, (...args: any[]) => void]; - }) - ); - - // Once core HTTP service is ready it broadcasts the internal server it relies on - // and server options that were used to create that server so that we can properly - // bridge with the "legacy" Kibana. If server isn't run (e.g. if process is managed - // by ClusterManager or optimizer) then this event will never fire. - this.env.legacy.once('connection', (connectionInfo: ConnectionInfo) => - this.onConnection(connectionInfo) - ); - } - - /** - * Neither new nor legacy platform should use this method directly. - */ - public address() { - return this.server && this.server.address(); - } - - /** - * Neither new nor legacy platform should use this method directly. - */ - public async listen(port: number, host: string, callback?: (error?: Error) => void) { - this.log.debug(`"listen" has been called (${host}:${port}).`); - - let error: Error | undefined; - try { - await this.root.start(); - } catch (err) { - error = err; - this.emit('error', err); - } - - if (callback !== undefined) { - callback(error); - } - } - - /** - * Neither new nor legacy platform should use this method directly. - */ - public async close(callback?: (error?: Error) => void) { - this.log.debug('"close" has been called.'); - - let error: Error | undefined; - try { - await this.root.shutdown(); - } catch (err) { - error = err; - this.emit('error', err); - } - - if (callback !== undefined) { - callback(error); - } - } - - /** - * Neither new nor legacy platform should use this method directly. - */ - public getConnections(callback: (error: Error | null, count?: number) => void) { - // This method is used by `even-better` (before we start platform). - // It seems that the latest version of parent `good` doesn't use this anymore. - if (this.server) { - this.server.getConnections(callback); - } else { - callback(null, 0); - } - } - - private onConnection({ server }: ConnectionInfo) { - this.server = server.listener; - - for (const [eventName, eventHandler] of this.eventHandlers) { - this.server.addListener(eventName, eventHandler); - } - - // We register Kibana proxy middleware right before we start server to allow - // all new platform plugins register their routes, so that `legacyProxy` - // handles only requests that aren't handled by the new platform. - server.route({ - path: '/{p*}', - method: '*', - options: { - payload: { - output: 'stream', - parse: false, - timeout: false, - // Having such a large value here will allow legacy routes to override - // maximum allowed payload size set in the core http server if needed. - maxBytes: Number.MAX_SAFE_INTEGER, - }, - }, - handler: async ({ raw: { req, res } }, responseToolkit) => { - this.log.trace(`Request will be handled by proxy ${req.method}:${req.url}.`); - // Forward request and response objects to the legacy platform. This method - // is used whenever new platform doesn't know how to handle the request. - this.emit('request', req, res); - return responseToolkit.abandon; - }, - }); - } -} diff --git a/src/core/server/legacy_compat/__tests__/legacy_platform_proxifier.test.ts b/src/core/server/legacy_compat/legacy_platform_proxy.test.ts similarity index 51% rename from src/core/server/legacy_compat/__tests__/legacy_platform_proxifier.test.ts rename to src/core/server/legacy_compat/legacy_platform_proxy.test.ts index 27db835a0ecf3..cc7436ce32170 100644 --- a/src/core/server/legacy_compat/__tests__/legacy_platform_proxifier.test.ts +++ b/src/core/server/legacy_compat/legacy_platform_proxy.test.ts @@ -17,17 +17,12 @@ * under the License. */ -import { Server as HapiServer } from 'hapi-latest'; import { Server } from 'net'; -import { LegacyPlatformProxifier } from '..'; -import { Env } from '../../config'; -import { getEnvOptions } from '../../config/__tests__/__mocks__/env'; -import { logger } from '../../logging/__mocks__'; + +import { LegacyPlatformProxy } from './legacy_platform_proxy'; let server: jest.Mocked; -let mockHapiServer: jest.Mocked; -let root: any; -let proxifier: LegacyPlatformProxifier; +let proxy: LegacyPlatformProxy; beforeEach(() => { server = { addListener: jest.fn(), @@ -36,29 +31,7 @@ beforeEach(() => { .mockReturnValue({ port: 1234, family: 'test-family', address: 'test-address' }), getConnections: jest.fn(), } as any; - - mockHapiServer = { listener: server, route: jest.fn() } as any; - - root = { - logger, - shutdown: jest.fn(), - start: jest.fn(), - } as any; - - const env = new Env('/kibana', getEnvOptions()); - proxifier = new LegacyPlatformProxifier(root, env); - env.legacy.emit('connection', { - server: mockHapiServer, - options: { someOption: 'foo', someAnotherOption: 'bar' }, - }); -}); - -test('correctly binds to the server.', () => { - expect(mockHapiServer.route.mock.calls).toMatchSnapshot('proxy route options'); - expect(server.addListener).toHaveBeenCalledTimes(6); - for (const eventName of ['clientError', 'close', 'connection', 'error', 'listening', 'upgrade']) { - expect(server.addListener).toHaveBeenCalledWith(eventName, expect.any(Function)); - } + proxy = new LegacyPlatformProxy({ debug: jest.fn() } as any, server); }); test('correctly redirects server events.', () => { @@ -66,7 +39,7 @@ test('correctly redirects server events.', () => { expect(server.addListener).toHaveBeenCalledWith(eventName, expect.any(Function)); const listener = jest.fn(); - proxifier.addListener(eventName, listener); + proxy.addListener(eventName, listener); // Emit several events, to make sure that server is not being listened with `once`. const [, serverListener] = server.addListener.mock.calls.find( @@ -78,68 +51,47 @@ test('correctly redirects server events.', () => { expect(listener).toHaveBeenCalledTimes(2); expect(listener).toHaveBeenCalledWith(1, 2, 3, 4); - expect(listener).toHaveBeenCalledWith(5, 6, 7, 8); - proxifier.removeListener(eventName, listener); + proxy.removeListener(eventName, listener); } }); test('returns `address` from the underlying server.', () => { - expect(proxifier.address()).toEqual({ + expect(proxy.address()).toEqual({ address: 'test-address', family: 'test-family', port: 1234, }); }); -test('`listen` starts the `root`.', async () => { +test('`listen` calls callback immediately.', async () => { const onListenComplete = jest.fn(); - await proxifier.listen(1234, 'host-1', onListenComplete); + await proxy.listen(1234, 'host-1', onListenComplete); - expect(root.start).toHaveBeenCalledTimes(1); expect(onListenComplete).toHaveBeenCalledTimes(1); }); -test('`close` shuts down the `root`.', async () => { +test('`close` calls callback immediately.', async () => { const onCloseComplete = jest.fn(); - await proxifier.close(onCloseComplete); + await proxy.close(onCloseComplete); - expect(root.shutdown).toHaveBeenCalledTimes(1); expect(onCloseComplete).toHaveBeenCalledTimes(1); }); test('returns connection count from the underlying server.', () => { server.getConnections.mockImplementation(callback => callback(null, 0)); const onGetConnectionsComplete = jest.fn(); - proxifier.getConnections(onGetConnectionsComplete); + proxy.getConnections(onGetConnectionsComplete); expect(onGetConnectionsComplete).toHaveBeenCalledTimes(1); expect(onGetConnectionsComplete).toHaveBeenCalledWith(null, 0); onGetConnectionsComplete.mockReset(); server.getConnections.mockImplementation(callback => callback(null, 100500)); - proxifier.getConnections(onGetConnectionsComplete); + proxy.getConnections(onGetConnectionsComplete); expect(onGetConnectionsComplete).toHaveBeenCalledTimes(1); expect(onGetConnectionsComplete).toHaveBeenCalledWith(null, 100500); }); - -test('proxy route abandons request processing and forwards it to the legacy Kibana', async () => { - const mockResponseToolkit = { response: jest.fn(), abandon: Symbol('abandon') }; - const mockRequest = { raw: { req: { a: 1 }, res: { b: 2 } } }; - - const onRequest = jest.fn(); - proxifier.addListener('request', onRequest); - - const [[{ handler }]] = mockHapiServer.route.mock.calls; - const response = await handler(mockRequest, mockResponseToolkit); - - expect(response).toBe(mockResponseToolkit.abandon); - expect(mockResponseToolkit.response).not.toHaveBeenCalled(); - - // Make sure request hasn't been passed to the legacy platform. - expect(onRequest).toHaveBeenCalledTimes(1); - expect(onRequest).toHaveBeenCalledWith(mockRequest.raw.req, mockRequest.raw.res); -}); diff --git a/src/core/server/legacy_compat/legacy_platform_proxy.ts b/src/core/server/legacy_compat/legacy_platform_proxy.ts new file mode 100644 index 0000000000000..e91d661e30238 --- /dev/null +++ b/src/core/server/legacy_compat/legacy_platform_proxy.ts @@ -0,0 +1,107 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { EventEmitter } from 'events'; +import { Server } from 'net'; + +import { Logger } from '../logging'; + +/** + * List of the server events to be forwarded to the legacy platform. + */ +const ServerEventsToForward = [ + 'clientError', + 'close', + 'connection', + 'error', + 'listening', + 'upgrade', +]; + +/** + * Represents "proxy" between legacy and current platform. + * @internal + */ +export class LegacyPlatformProxy extends EventEmitter { + private readonly eventHandlers: Map void>; + + constructor(private readonly log: Logger, private readonly server: Server) { + super(); + + // HapiJS expects that the following events will be generated by `listener`, see: + // https://github.com/hapijs/hapi/blob/v14.2.0/lib/connection.js. + this.eventHandlers = new Map( + ServerEventsToForward.map(eventName => { + return [ + eventName, + (...args: any[]) => { + this.log.debug(`Event is being forwarded: ${eventName}`); + this.emit(eventName, ...args); + }, + ] as [string, (...args: any[]) => void]; + }) + ); + + for (const [eventName, eventHandler] of this.eventHandlers) { + this.server.addListener(eventName, eventHandler); + } + } + + /** + * Neither new nor legacy platform should use this method directly. + */ + public address() { + this.log.debug('"address" has been called.'); + + return this.server.address(); + } + + /** + * Neither new nor legacy platform should use this method directly. + */ + public listen(port: number, host: string, callback?: (error?: Error) => void) { + this.log.debug(`"listen" has been called (${host}:${port}).`); + + if (callback !== undefined) { + callback(); + } + } + + /** + * Neither new nor legacy platform should use this method directly. + */ + public close(callback?: (error?: Error) => void) { + this.log.debug('"close" has been called.'); + + if (callback !== undefined) { + callback(); + } + } + + /** + * Neither new nor legacy platform should use this method directly. + */ + public getConnections(callback: (error: Error | null, count?: number) => void) { + this.log.debug('"getConnections" has been called.'); + + // This method is used by `even-better` (before we start platform). + // It seems that the latest version of parent `good` doesn't use this anymore. + this.server.getConnections(callback); + } +} diff --git a/src/core/server/legacy_compat/legacy_service.test.ts b/src/core/server/legacy_compat/legacy_service.test.ts new file mode 100644 index 0000000000000..70c71e3b4b0b8 --- /dev/null +++ b/src/core/server/legacy_compat/legacy_service.test.ts @@ -0,0 +1,339 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { BehaviorSubject, Subject, throwError } from 'rxjs'; + +jest.mock('./legacy_platform_proxy'); +jest.mock('../../../server/kbn_server'); +jest.mock('../../../cli/cluster/cluster_manager'); + +import { first } from 'rxjs/operators'; +import { LegacyService } from '.'; +// @ts-ignore: implicit any for JS file +import MockClusterManager from '../../../cli/cluster/cluster_manager'; +// @ts-ignore: implicit any for JS file +import MockKbnServer from '../../../server/kbn_server'; +import { Config, ConfigService, Env, ObjectToConfigAdapter } from '../config'; +import { getEnvOptions } from '../config/__mocks__/env'; +import { logger } from '../logging/__mocks__'; +import { LegacyPlatformProxy } from './legacy_platform_proxy'; + +const MockLegacyPlatformProxy: jest.Mock = LegacyPlatformProxy as any; + +let legacyService: LegacyService; +let configService: jest.Mocked; +let env: Env; +let mockHttpServerInfo: any; +let config$: BehaviorSubject; +beforeEach(() => { + env = Env.createDefault(getEnvOptions()); + + MockKbnServer.prototype.ready = jest.fn().mockReturnValue(Promise.resolve()); + + mockHttpServerInfo = { + server: { listener: { addListener: jest.fn() }, route: jest.fn() }, + options: { someOption: 'foo', someAnotherOption: 'bar' }, + }; + + config$ = new BehaviorSubject( + new ObjectToConfigAdapter({ + server: { autoListen: true }, + }) + ); + + configService = { + getConfig$: jest.fn().mockReturnValue(config$), + atPath: jest.fn().mockReturnValue(new BehaviorSubject({})), + } as any; + legacyService = new LegacyService(env, logger, configService); +}); + +afterEach(() => { + MockLegacyPlatformProxy.mockClear(); + MockKbnServer.mockClear(); + MockClusterManager.create.mockClear(); + logger.mockClear(); +}); + +describe('once LegacyService is started with connection info', () => { + test('register proxy route.', async () => { + await legacyService.start(mockHttpServerInfo); + + expect(mockHttpServerInfo.server.route.mock.calls).toMatchSnapshot('proxy route options'); + }); + + test('proxy route responds with `503` if `kbnServer` is not ready yet.', async () => { + configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); + + const kbnServerListen$ = new Subject(); + MockKbnServer.prototype.listen = jest.fn(() => { + kbnServerListen$.next(); + return kbnServerListen$.toPromise(); + }); + + // Wait until listen is called and proxy route is registered, but don't allow + // listen to complete and make kbnServer available. + const legacyStartPromise = legacyService.start(mockHttpServerInfo); + await kbnServerListen$.pipe(first()).toPromise(); + + const mockResponse: any = { + code: jest.fn().mockImplementation(() => mockResponse), + header: jest.fn().mockImplementation(() => mockResponse), + }; + const mockResponseToolkit = { + response: jest.fn().mockReturnValue(mockResponse), + abandon: Symbol('abandon'), + }; + const mockRequest = { raw: { req: { a: 1 }, res: { b: 2 } } }; + + const [[{ handler }]] = mockHttpServerInfo.server.route.mock.calls; + const response503 = await handler(mockRequest, mockResponseToolkit); + + expect(response503).toBe(mockResponse); + expect({ + body: mockResponseToolkit.response.mock.calls, + code: mockResponse.code.mock.calls, + header: mockResponse.header.mock.calls, + }).toMatchSnapshot('503 response'); + + // Make sure request hasn't been passed to the legacy platform. + const [mockedLegacyPlatformProxy] = MockLegacyPlatformProxy.mock.instances; + expect(mockedLegacyPlatformProxy.emit).not.toHaveBeenCalled(); + + // Now wait until kibana is ready and try to request once again. + kbnServerListen$.complete(); + await legacyStartPromise; + mockResponseToolkit.response.mockClear(); + + const responseProxy = await handler(mockRequest, mockResponseToolkit); + expect(responseProxy).toBe(mockResponseToolkit.abandon); + expect(mockResponseToolkit.response).not.toHaveBeenCalled(); + + // Make sure request has been passed to the legacy platform. + expect(mockedLegacyPlatformProxy.emit).toHaveBeenCalledTimes(1); + expect(mockedLegacyPlatformProxy.emit).toHaveBeenCalledWith( + 'request', + mockRequest.raw.req, + mockRequest.raw.res + ); + }); + + test('creates legacy kbnServer and calls `listen`.', async () => { + configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); + + await legacyService.start(mockHttpServerInfo); + + expect(MockKbnServer).toHaveBeenCalledTimes(1); + expect(MockKbnServer).toHaveBeenCalledWith( + { server: { autoListen: true } }, + { + serverOptions: { + listener: expect.any(LegacyPlatformProxy), + someAnotherOption: 'bar', + someOption: 'foo', + }, + } + ); + + const [mockKbnServer] = MockKbnServer.mock.instances; + expect(mockKbnServer.listen).toHaveBeenCalledTimes(1); + expect(mockKbnServer.close).not.toHaveBeenCalled(); + }); + + test('creates legacy kbnServer but does not call `listen` if `autoListen: false`.', async () => { + configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: false })); + + await legacyService.start(mockHttpServerInfo); + + expect(MockKbnServer).toHaveBeenCalledTimes(1); + expect(MockKbnServer).toHaveBeenCalledWith( + { server: { autoListen: true } }, + { + serverOptions: { + listener: expect.any(LegacyPlatformProxy), + someAnotherOption: 'bar', + someOption: 'foo', + }, + } + ); + + const [mockKbnServer] = MockKbnServer.mock.instances; + expect(mockKbnServer.ready).toHaveBeenCalledTimes(1); + expect(mockKbnServer.listen).not.toHaveBeenCalled(); + expect(mockKbnServer.close).not.toHaveBeenCalled(); + }); + + test('creates legacy kbnServer and closes it if `listen` fails.', async () => { + configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); + MockKbnServer.prototype.listen.mockRejectedValue(new Error('something failed')); + + await expect(legacyService.start(mockHttpServerInfo)).rejects.toThrowErrorMatchingSnapshot(); + + const [mockKbnServer] = MockKbnServer.mock.instances; + expect(mockKbnServer.listen).toHaveBeenCalled(); + expect(mockKbnServer.close).toHaveBeenCalled(); + }); + + test('throws if fails to retrieve initial config.', async () => { + configService.getConfig$.mockReturnValue(throwError(new Error('something failed'))); + + await expect(legacyService.start(mockHttpServerInfo)).rejects.toThrowErrorMatchingSnapshot(); + + expect(MockKbnServer).not.toHaveBeenCalled(); + expect(MockClusterManager).not.toHaveBeenCalled(); + }); + + test('reconfigures logging configuration if new config is received.', async () => { + await legacyService.start(mockHttpServerInfo); + + const [mockKbnServer] = MockKbnServer.mock.instances; + expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled(); + + config$.next(new ObjectToConfigAdapter({ logging: { verbose: true } })); + + expect(mockKbnServer.applyLoggingConfiguration.mock.calls).toMatchSnapshot( + `applyLoggingConfiguration params` + ); + }); + + test('logs error if re-configuring fails.', async () => { + await legacyService.start(mockHttpServerInfo); + + const [mockKbnServer] = MockKbnServer.mock.instances; + expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled(); + expect(logger.mockCollect().error).toEqual([]); + + const configError = new Error('something went wrong'); + mockKbnServer.applyLoggingConfiguration.mockImplementation(() => { + throw configError; + }); + + config$.next(new ObjectToConfigAdapter({ logging: { verbose: true } })); + + expect(logger.mockCollect().error).toEqual([[configError]]); + }); + + test('logs error if config service fails.', async () => { + await legacyService.start(mockHttpServerInfo); + + const [mockKbnServer] = MockKbnServer.mock.instances; + expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled(); + expect(logger.mockCollect().error).toEqual([]); + + const configError = new Error('something went wrong'); + config$.error(configError); + + expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled(); + expect(logger.mockCollect().error).toEqual([[configError]]); + }); + + test('proxy route abandons request processing and forwards it to the legacy Kibana', async () => { + const mockResponseToolkit = { response: jest.fn(), abandon: Symbol('abandon') }; + const mockRequest = { raw: { req: { a: 1 }, res: { b: 2 } } }; + + await legacyService.start(mockHttpServerInfo); + + const [[{ handler }]] = mockHttpServerInfo.server.route.mock.calls; + const response = await handler(mockRequest, mockResponseToolkit); + + expect(response).toBe(mockResponseToolkit.abandon); + expect(mockResponseToolkit.response).not.toHaveBeenCalled(); + + // Make sure request has been passed to the legacy platform. + const [mockedLegacyPlatformProxy] = MockLegacyPlatformProxy.mock.instances; + expect(mockedLegacyPlatformProxy.emit).toHaveBeenCalledTimes(1); + expect(mockedLegacyPlatformProxy.emit).toHaveBeenCalledWith( + 'request', + mockRequest.raw.req, + mockRequest.raw.res + ); + }); +}); + +describe('once LegacyService is started without connection info', () => { + beforeEach(async () => await legacyService.start()); + + test('creates legacy kbnServer with `autoListen: false`.', () => { + expect(mockHttpServerInfo.server.route).not.toHaveBeenCalled(); + expect(MockKbnServer).toHaveBeenCalledTimes(1); + expect(MockKbnServer).toHaveBeenCalledWith( + { server: { autoListen: true } }, + { serverOptions: { autoListen: false } } + ); + }); + + test('reconfigures logging configuration if new config is received.', async () => { + const [mockKbnServer] = MockKbnServer.mock.instances; + expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled(); + + config$.next(new ObjectToConfigAdapter({ logging: { verbose: true } })); + + expect(mockKbnServer.applyLoggingConfiguration.mock.calls).toMatchSnapshot( + `applyLoggingConfiguration params` + ); + }); +}); + +describe('once LegacyService is started in `devClusterMaster` mode', () => { + beforeEach(() => { + configService.atPath.mockImplementation(path => { + return new BehaviorSubject( + path === 'dev' ? { basePathProxyTargetPort: 100500 } : { basePath: '/abc' } + ); + }); + }); + + test('creates ClusterManager without base path proxy.', async () => { + const devClusterLegacyService = new LegacyService( + Env.createDefault( + getEnvOptions({ + cliArgs: { silent: true, basePath: false }, + isDevClusterMaster: true, + }) + ), + logger, + configService + ); + + await devClusterLegacyService.start(); + + expect(MockClusterManager.create.mock.calls).toMatchSnapshot( + 'cluster manager without base path proxy' + ); + }); + + test('creates ClusterManager with base path proxy.', async () => { + const devClusterLegacyService = new LegacyService( + Env.createDefault( + getEnvOptions({ + cliArgs: { quiet: true, basePath: true }, + isDevClusterMaster: true, + }) + ), + logger, + configService + ); + + await devClusterLegacyService.start(); + + expect(MockClusterManager.create.mock.calls).toMatchSnapshot( + 'cluster manager with base path proxy' + ); + }); +}); diff --git a/src/core/server/legacy_compat/legacy_service.ts b/src/core/server/legacy_compat/legacy_service.ts new file mode 100644 index 0000000000000..092057874fa73 --- /dev/null +++ b/src/core/server/legacy_compat/legacy_service.ts @@ -0,0 +1,204 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Server as HapiServer } from 'hapi-latest'; +import { combineLatest, ConnectableObservable, EMPTY, Subscription } from 'rxjs'; +import { first, map, mergeMap, publishReplay, tap } from 'rxjs/operators'; +import { CoreService } from '../../types/core_service'; +import { Config, ConfigService, Env } from '../config'; +import { DevConfig } from '../dev'; +import { BasePathProxyServer, HttpConfig, HttpServerInfo } from '../http'; +import { Logger, LoggerFactory } from '../logging'; +import { LegacyPlatformProxy } from './legacy_platform_proxy'; + +interface LegacyKbnServer { + applyLoggingConfiguration: (settings: Readonly>) => void; + listen: () => Promise; + ready: () => Promise; + close: () => Promise; +} + +export class LegacyService implements CoreService { + private readonly log: Logger; + private kbnServer?: LegacyKbnServer; + private configSubscription?: Subscription; + + constructor( + private readonly env: Env, + private readonly logger: LoggerFactory, + private readonly configService: ConfigService + ) { + this.log = logger.get('legacy', 'service'); + } + + public async start(httpServerInfo?: HttpServerInfo) { + this.log.debug('starting legacy service'); + + const update$ = this.configService.getConfig$().pipe( + tap(config => { + if (this.kbnServer !== undefined) { + this.kbnServer.applyLoggingConfiguration(config.toRaw()); + } + }), + tap({ error: err => this.log.error(err) }), + publishReplay(1) + ) as ConnectableObservable; + + this.configSubscription = update$.connect(); + + // Receive initial config and create kbnServer/ClusterManager. + this.kbnServer = await update$ + .pipe( + first(), + mergeMap(async config => { + if (this.env.isDevClusterMaster) { + await this.createClusterManager(config); + return; + } + + return await this.createKbnServer(config, httpServerInfo); + }) + ) + .toPromise(); + } + + public async stop() { + this.log.debug('stopping legacy service'); + + if (this.configSubscription !== undefined) { + this.configSubscription.unsubscribe(); + this.configSubscription = undefined; + } + + if (this.kbnServer !== undefined) { + await this.kbnServer.close(); + this.kbnServer = undefined; + } + } + + private async createClusterManager(config: Config) { + const basePathProxy$ = this.env.cliArgs.basePath + ? combineLatest( + this.configService.atPath('dev', DevConfig), + this.configService.atPath('server', HttpConfig) + ).pipe( + first(), + map(([devConfig, httpConfig]) => { + return new BasePathProxyServer(this.logger.get('server'), httpConfig, devConfig); + }) + ) + : EMPTY; + + require('../../../cli/cluster/cluster_manager').create( + this.env.cliArgs, + config.toRaw(), + await basePathProxy$.toPromise() + ); + } + + private async createKbnServer(config: Config, httpServerInfo?: HttpServerInfo) { + const KbnServer = require('../../../server/kbn_server'); + const kbnServer: LegacyKbnServer = new KbnServer(config.toRaw(), { + // If core HTTP service is run we'll receive internal server reference and + // options that were used to create that server so that we can properly + // bridge with the "legacy" Kibana. If server isn't run (e.g. if process is + // managed by ClusterManager or optimizer) then we won't have that info, + // so we can't start "legacy" server either. + serverOptions: + httpServerInfo !== undefined + ? { + ...httpServerInfo.options, + listener: this.setupProxyListener(httpServerInfo.server), + } + : { autoListen: false }, + }); + + // The kbnWorkerType check is necessary to prevent the repl + // from being started multiple times in different processes. + // We only want one REPL. + if (this.env.cliArgs.repl && process.env.kbnWorkerType === 'server') { + require('../../../cli/repl').startRepl(kbnServer); + } + + const httpConfig = await this.configService + .atPath('server', HttpConfig) + .pipe(first()) + .toPromise(); + + if (httpConfig.autoListen) { + try { + await kbnServer.listen(); + } catch (err) { + await kbnServer.close(); + throw err; + } + } else { + await kbnServer.ready(); + } + + return kbnServer; + } + + private setupProxyListener(server: HapiServer) { + const legacyProxy = new LegacyPlatformProxy( + this.logger.get('legacy', 'proxy'), + server.listener + ); + + // We register Kibana proxy middleware right before we start server to allow + // all new platform plugins register their routes, so that `legacyProxy` + // handles only requests that aren't handled by the new platform. + server.route({ + path: '/{p*}', + method: '*', + options: { + payload: { + output: 'stream', + parse: false, + timeout: false, + // Having such a large value here will allow legacy routes to override + // maximum allowed payload size set in the core http server if needed. + maxBytes: Number.MAX_SAFE_INTEGER, + }, + }, + handler: async ({ raw: { req, res } }, responseToolkit) => { + if (this.kbnServer === undefined) { + this.log.debug(`Kibana server is not ready yet ${req.method}:${req.url}.`); + + // If legacy server is not ready yet (e.g. it's still in optimization phase), + // we should let client know that and ask to retry after 30 seconds. + return responseToolkit + .response('Kibana server is not ready yet') + .code(503) + .header('Retry-After', '30'); + } + + this.log.trace(`Request will be handled by proxy ${req.method}:${req.url}.`); + + // Forward request and response objects to the legacy platform. This method + // is used whenever new platform doesn't know how to handle the request. + legacyProxy.emit('request', req, res); + + return responseToolkit.abandon; + }, + }); + + return legacyProxy; + } +} diff --git a/src/core/server/legacy_compat/logging/appenders/__tests__/__snapshots__/legacy_appender.test.ts.snap b/src/core/server/legacy_compat/logging/appenders/__snapshots__/legacy_appender.test.ts.snap similarity index 100% rename from src/core/server/legacy_compat/logging/appenders/__tests__/__snapshots__/legacy_appender.test.ts.snap rename to src/core/server/legacy_compat/logging/appenders/__snapshots__/legacy_appender.test.ts.snap diff --git a/src/core/server/legacy_compat/logging/appenders/__tests__/legacy_appender.test.ts b/src/core/server/legacy_compat/logging/appenders/legacy_appender.test.ts similarity index 93% rename from src/core/server/legacy_compat/logging/appenders/__tests__/legacy_appender.test.ts rename to src/core/server/legacy_compat/logging/appenders/legacy_appender.test.ts index 5f62a853666d9..adc5dcae3ec9d 100644 --- a/src/core/server/legacy_compat/logging/appenders/__tests__/legacy_appender.test.ts +++ b/src/core/server/legacy_compat/logging/appenders/legacy_appender.test.ts @@ -17,12 +17,12 @@ * under the License. */ -jest.mock('../../legacy_logging_server'); +jest.mock('../legacy_logging_server'); -import { LogLevel } from '../../../../logging/log_level'; -import { LogRecord } from '../../../../logging/log_record'; -import { LegacyLoggingServer } from '../../legacy_logging_server'; -import { LegacyAppender } from '../legacy_appender'; +import { LogLevel } from '../../../logging/log_level'; +import { LogRecord } from '../../../logging/log_record'; +import { LegacyLoggingServer } from '../legacy_logging_server'; +import { LegacyAppender } from './legacy_appender'; afterEach(() => (LegacyLoggingServer as any).mockClear()); diff --git a/src/core/server/logging/__tests__/__snapshots__/logging_config.test.ts.snap b/src/core/server/logging/__snapshots__/logging_config.test.ts.snap similarity index 100% rename from src/core/server/logging/__tests__/__snapshots__/logging_config.test.ts.snap rename to src/core/server/logging/__snapshots__/logging_config.test.ts.snap diff --git a/src/core/server/logging/__tests__/__snapshots__/logging_service.test.ts.snap b/src/core/server/logging/__snapshots__/logging_service.test.ts.snap similarity index 100% rename from src/core/server/logging/__tests__/__snapshots__/logging_service.test.ts.snap rename to src/core/server/logging/__snapshots__/logging_service.test.ts.snap diff --git a/src/core/server/logging/appenders/__tests__/appenders.test.ts b/src/core/server/logging/appenders/appenders.test.ts similarity index 88% rename from src/core/server/logging/appenders/__tests__/appenders.test.ts rename to src/core/server/logging/appenders/appenders.test.ts index f141de991f453..2103f9d8187b2 100644 --- a/src/core/server/logging/appenders/__tests__/appenders.test.ts +++ b/src/core/server/logging/appenders/appenders.test.ts @@ -18,8 +18,8 @@ */ const mockCreateLayout = jest.fn(); -jest.mock('../../layouts/layouts', () => { - const { schema } = require('../../../config/schema'); +jest.mock('../layouts/layouts', () => { + const { schema } = require('../../config/schema'); return { Layouts: { configSchema: schema.object({ kind: schema.literal('mock') }), @@ -28,10 +28,10 @@ jest.mock('../../layouts/layouts', () => { }; }); -import { LegacyAppender } from '../../../legacy_compat/logging/appenders/legacy_appender'; -import { Appenders } from '../appenders'; -import { ConsoleAppender } from '../console/console_appender'; -import { FileAppender } from '../file/file_appender'; +import { LegacyAppender } from '../../legacy_compat/logging/appenders/legacy_appender'; +import { Appenders } from './appenders'; +import { ConsoleAppender } from './console/console_appender'; +import { FileAppender } from './file/file_appender'; beforeEach(() => { mockCreateLayout.mockReset(); diff --git a/src/core/server/logging/appenders/__tests__/buffer_appender.test.ts b/src/core/server/logging/appenders/buffer/buffer_appender.test.ts similarity index 97% rename from src/core/server/logging/appenders/__tests__/buffer_appender.test.ts rename to src/core/server/logging/appenders/buffer/buffer_appender.test.ts index cdf2714f44e29..453a29271c582 100644 --- a/src/core/server/logging/appenders/__tests__/buffer_appender.test.ts +++ b/src/core/server/logging/appenders/buffer/buffer_appender.test.ts @@ -19,7 +19,7 @@ import { LogLevel } from '../../log_level'; import { LogRecord } from '../../log_record'; -import { BufferAppender } from '../buffer/buffer_appender'; +import { BufferAppender } from './buffer_appender'; test('`flush()` does not return any record buffered at the beginning.', () => { const appender = new BufferAppender(); diff --git a/src/core/server/logging/appenders/__tests__/console_appender.test.ts b/src/core/server/logging/appenders/console/console_appender.test.ts similarity index 97% rename from src/core/server/logging/appenders/__tests__/console_appender.test.ts rename to src/core/server/logging/appenders/console/console_appender.test.ts index 35128bd6ba1fd..fc885e5f58a11 100644 --- a/src/core/server/logging/appenders/__tests__/console_appender.test.ts +++ b/src/core/server/logging/appenders/console/console_appender.test.ts @@ -30,7 +30,7 @@ jest.mock('../../layouts/layouts', () => { import { LogLevel } from '../../log_level'; import { LogRecord } from '../../log_record'; -import { ConsoleAppender } from '../console/console_appender'; +import { ConsoleAppender } from './console_appender'; test('`configSchema` creates correct schema.', () => { const appenderSchema = ConsoleAppender.configSchema; diff --git a/src/core/server/logging/appenders/__tests__/file_appender.test.ts b/src/core/server/logging/appenders/file/file_appender.test.ts similarity index 98% rename from src/core/server/logging/appenders/__tests__/file_appender.test.ts rename to src/core/server/logging/appenders/file/file_appender.test.ts index 69b4980dff1f0..cc8f0196bff7c 100644 --- a/src/core/server/logging/appenders/__tests__/file_appender.test.ts +++ b/src/core/server/logging/appenders/file/file_appender.test.ts @@ -33,7 +33,7 @@ jest.mock('fs', () => ({ createWriteStream: mockCreateWriteStream })); import { LogLevel } from '../../log_level'; import { LogRecord } from '../../log_record'; -import { FileAppender } from '../file/file_appender'; +import { FileAppender } from './file_appender'; const tickMs = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); diff --git a/src/core/server/logging/layouts/__tests__/__snapshots__/json_layout.test.ts.snap b/src/core/server/logging/layouts/__snapshots__/json_layout.test.ts.snap similarity index 100% rename from src/core/server/logging/layouts/__tests__/__snapshots__/json_layout.test.ts.snap rename to src/core/server/logging/layouts/__snapshots__/json_layout.test.ts.snap diff --git a/src/core/server/logging/layouts/__tests__/__snapshots__/pattern_layout.test.ts.snap b/src/core/server/logging/layouts/__snapshots__/pattern_layout.test.ts.snap similarity index 100% rename from src/core/server/logging/layouts/__tests__/__snapshots__/pattern_layout.test.ts.snap rename to src/core/server/logging/layouts/__snapshots__/pattern_layout.test.ts.snap diff --git a/src/core/server/logging/layouts/__tests__/json_layout.test.ts b/src/core/server/logging/layouts/json_layout.test.ts similarity index 94% rename from src/core/server/logging/layouts/__tests__/json_layout.test.ts rename to src/core/server/logging/layouts/json_layout.test.ts index ec94d023b2d64..49b8ddef07a63 100644 --- a/src/core/server/logging/layouts/__tests__/json_layout.test.ts +++ b/src/core/server/logging/layouts/json_layout.test.ts @@ -17,9 +17,9 @@ * under the License. */ -import { LogLevel } from '../../log_level'; -import { LogRecord } from '../../log_record'; -import { JsonLayout } from '../json_layout'; +import { LogLevel } from '../log_level'; +import { LogRecord } from '../log_record'; +import { JsonLayout } from './json_layout'; const records: LogRecord[] = [ { diff --git a/src/core/server/logging/layouts/__tests__/layouts.test.ts b/src/core/server/logging/layouts/layouts.test.ts similarity index 93% rename from src/core/server/logging/layouts/__tests__/layouts.test.ts rename to src/core/server/logging/layouts/layouts.test.ts index ca70710233fee..aa1c54c846bc6 100644 --- a/src/core/server/logging/layouts/__tests__/layouts.test.ts +++ b/src/core/server/logging/layouts/layouts.test.ts @@ -17,9 +17,9 @@ * under the License. */ -import { JsonLayout } from '../json_layout'; -import { Layouts } from '../layouts'; -import { PatternLayout } from '../pattern_layout'; +import { JsonLayout } from './json_layout'; +import { Layouts } from './layouts'; +import { PatternLayout } from './pattern_layout'; test('`configSchema` creates correct schema for `pattern` layout.', () => { const layoutsSchema = Layouts.configSchema; diff --git a/src/core/server/logging/layouts/__tests__/pattern_layout.test.ts b/src/core/server/logging/layouts/pattern_layout.test.ts similarity index 93% rename from src/core/server/logging/layouts/__tests__/pattern_layout.test.ts rename to src/core/server/logging/layouts/pattern_layout.test.ts index 4e6ddf2c097ed..ae8b39b9cc99a 100644 --- a/src/core/server/logging/layouts/__tests__/pattern_layout.test.ts +++ b/src/core/server/logging/layouts/pattern_layout.test.ts @@ -17,10 +17,10 @@ * under the License. */ -import { stripAnsiSnapshotSerializer } from '../../../../test_helpers/strip_ansi_snapshot_serializer'; -import { LogLevel } from '../../log_level'; -import { LogRecord } from '../../log_record'; -import { PatternLayout } from '../pattern_layout'; +import { stripAnsiSnapshotSerializer } from '../../../test_helpers/strip_ansi_snapshot_serializer'; +import { LogLevel } from '../log_level'; +import { LogRecord } from '../log_record'; +import { PatternLayout } from './pattern_layout'; const records: LogRecord[] = [ { diff --git a/src/core/server/logging/__tests__/log_level.test.ts b/src/core/server/logging/log_level.test.ts similarity index 98% rename from src/core/server/logging/__tests__/log_level.test.ts rename to src/core/server/logging/log_level.test.ts index 43de344b34cff..1f86cf21037a6 100644 --- a/src/core/server/logging/__tests__/log_level.test.ts +++ b/src/core/server/logging/log_level.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { LogLevel } from '../log_level'; +import { LogLevel } from './log_level'; const allLogLevels = [ LogLevel.Off, diff --git a/src/core/server/logging/__tests__/logger.test.ts b/src/core/server/logging/logger.test.ts similarity index 98% rename from src/core/server/logging/__tests__/logger.test.ts rename to src/core/server/logging/logger.test.ts index 2dc16178fb47b..61eaa4912185b 100644 --- a/src/core/server/logging/__tests__/logger.test.ts +++ b/src/core/server/logging/logger.test.ts @@ -17,10 +17,10 @@ * under the License. */ -import { Appender } from '../appenders/appenders'; -import { LogLevel } from '../log_level'; -import { BaseLogger } from '../logger'; -import { LoggingConfig } from '../logging_config'; +import { LoggingConfig } from '.'; +import { Appender } from './appenders/appenders'; +import { LogLevel } from './log_level'; +import { BaseLogger } from './logger'; const context = LoggingConfig.getLoggerContext(['context', 'parent', 'child']); let appenderMocks: Appender[]; diff --git a/src/core/server/logging/__tests__/logger_adapter.test.ts b/src/core/server/logging/logger_adapter.test.ts similarity index 97% rename from src/core/server/logging/__tests__/logger_adapter.test.ts rename to src/core/server/logging/logger_adapter.test.ts index 25a9c01b108d6..075e8f4d47ffe 100644 --- a/src/core/server/logging/__tests__/logger_adapter.test.ts +++ b/src/core/server/logging/logger_adapter.test.ts @@ -17,8 +17,8 @@ * under the License. */ -import { Logger } from '../logger'; -import { LoggerAdapter } from '../logger_adapter'; +import { Logger } from '.'; +import { LoggerAdapter } from './logger_adapter'; test('proxies all method calls to the internal logger.', () => { const internalLogger: Logger = { diff --git a/src/core/server/logging/__tests__/logging_config.test.ts b/src/core/server/logging/logging_config.test.ts similarity index 98% rename from src/core/server/logging/__tests__/logging_config.test.ts rename to src/core/server/logging/logging_config.test.ts index 2f1f1d9f2f7c0..f21b5aaf3c1a7 100644 --- a/src/core/server/logging/__tests__/logging_config.test.ts +++ b/src/core/server/logging/logging_config.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { LoggingConfig } from '../logging_config'; +import { LoggingConfig } from '.'; test('`schema` creates correct schema with defaults.', () => { const loggingConfigSchema = LoggingConfig.schema; diff --git a/src/core/server/logging/__tests__/logging_service.test.ts b/src/core/server/logging/logging_service.test.ts similarity index 98% rename from src/core/server/logging/__tests__/logging_service.test.ts rename to src/core/server/logging/logging_service.test.ts index eb452376d6ccc..b6aeb88b50052 100644 --- a/src/core/server/logging/__tests__/logging_service.test.ts +++ b/src/core/server/logging/logging_service.test.ts @@ -32,8 +32,7 @@ jest.spyOn(global, 'Date').mockImplementation(() => timestamp); import { createWriteStream } from 'fs'; const mockCreateWriteStream = createWriteStream as jest.Mock; -import { LoggingConfig } from '../logging_config'; -import { LoggingService } from '../logging_service'; +import { LoggingConfig, LoggingService } from '.'; let service: LoggingService; beforeEach(() => (service = new LoggingService())); diff --git a/src/core/server/logging/logging_service.ts b/src/core/server/logging/logging_service.ts index 90ee9524381de..966bd74a0df41 100644 --- a/src/core/server/logging/logging_service.ts +++ b/src/core/server/logging/logging_service.ts @@ -71,7 +71,7 @@ export class LoggingService implements LoggerFactory { this.appenders.set(appenderKey, Appenders.create(appenderConfig)); } - for (const [loggerKey, loggerAdapter] of this.loggers.entries()) { + for (const [loggerKey, loggerAdapter] of this.loggers) { loggerAdapter.updateLogger(this.createLogger(loggerKey, config)); } diff --git a/src/core/server/root/__tests__/__snapshots__/index.test.ts.snap b/src/core/server/root/__snapshots__/index.test.ts.snap similarity index 100% rename from src/core/server/root/__tests__/__snapshots__/index.test.ts.snap rename to src/core/server/root/__snapshots__/index.test.ts.snap diff --git a/src/core/server/root/base_path_proxy_root.ts b/src/core/server/root/base_path_proxy_root.ts deleted file mode 100644 index 80ab7d1c60677..0000000000000 --- a/src/core/server/root/base_path_proxy_root.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { first } from 'rxjs/operators'; - -import { Root } from '.'; -import { DevConfig } from '../dev'; -import { HttpConfig } from '../http'; -import { BasePathProxyServer, BasePathProxyServerOptions } from '../http/base_path_proxy_server'; - -/** - * Top-level entry point to start BasePathProxy server. - */ -export class BasePathProxyRoot extends Root { - private basePathProxy?: BasePathProxyServer; - - public async configure({ - blockUntil, - shouldRedirectFromOldBasePath, - }: Pick) { - const [devConfig, httpConfig] = await Promise.all([ - this.configService - .atPath('dev', DevConfig) - .pipe(first()) - .toPromise(), - this.configService - .atPath('server', HttpConfig) - .pipe(first()) - .toPromise(), - ]); - - this.basePathProxy = new BasePathProxyServer(this.logger.get('server'), { - blockUntil, - devConfig, - httpConfig, - shouldRedirectFromOldBasePath, - }); - } - - public getBasePath() { - return this.getBasePathProxy().basePath; - } - - public getTargetPort() { - return this.getBasePathProxy().targetPort; - } - - protected async startServer() { - return this.getBasePathProxy().start(); - } - - protected async stopServer() { - await this.getBasePathProxy().stop(); - this.basePathProxy = undefined; - } - - private getBasePathProxy() { - if (this.basePathProxy === undefined) { - throw new Error('BasePathProxyRoot is not configured!'); - } - - return this.basePathProxy; - } -} diff --git a/src/core/server/root/__tests__/index.test.ts b/src/core/server/root/index.test.ts similarity index 95% rename from src/core/server/root/__tests__/index.test.ts rename to src/core/server/root/index.test.ts index 851e7c9dca85e..97308ef484f4f 100644 --- a/src/core/server/root/__tests__/index.test.ts +++ b/src/core/server/root/index.test.ts @@ -18,24 +18,24 @@ */ const mockLoggingService = { asLoggerFactory: jest.fn(), upgrade: jest.fn(), stop: jest.fn() }; -jest.mock('../../logging', () => ({ +jest.mock('../logging', () => ({ LoggingService: jest.fn(() => mockLoggingService), })); const mockConfigService = { atPath: jest.fn() }; -jest.mock('../../config/config_service', () => ({ +jest.mock('../config/config_service', () => ({ ConfigService: jest.fn(() => mockConfigService), })); const mockServer = { start: jest.fn(), stop: jest.fn() }; -jest.mock('../../', () => ({ Server: jest.fn(() => mockServer) })); +jest.mock('../', () => ({ Server: jest.fn(() => mockServer) })); import { BehaviorSubject } from 'rxjs'; import { filter, first } from 'rxjs/operators'; -import { Root } from '../'; -import { Config, Env } from '../../config'; -import { getEnvOptions } from '../../config/__tests__/__mocks__/env'; -import { logger } from '../../logging/__mocks__'; +import { Root } from '.'; +import { Config, Env } from '../config'; +import { getEnvOptions } from '../config/__mocks__/env'; +import { logger } from '../logging/__mocks__'; const env = new Env('.', getEnvOptions()); const config$ = new BehaviorSubject({} as Config); diff --git a/src/core/server/root/index.ts b/src/core/server/root/index.ts index 935d11a83e963..02a5c9e559544 100644 --- a/src/core/server/root/index.ts +++ b/src/core/server/root/index.ts @@ -18,38 +18,34 @@ */ import { ConnectableObservable, Observable, Subscription } from 'rxjs'; -import { catchError, first, map, publishReplay } from 'rxjs/operators'; +import { first, map, publishReplay, tap } from 'rxjs/operators'; import { Server } from '..'; import { Config, ConfigService, Env } from '../config'; - import { Logger, LoggerFactory, LoggingConfig, LoggingService } from '../logging'; -export type OnShutdown = (reason?: Error) => void; - /** * Top-level entry point to kick off the app and start the Kibana server. */ export class Root { public readonly logger: LoggerFactory; - protected readonly configService: ConfigService; + private readonly configService: ConfigService; private readonly log: Logger; - private server?: Server; + private readonly server: Server; private readonly loggingService: LoggingService; private loggingConfigSubscription?: Subscription; constructor( config$: Observable, private readonly env: Env, - private readonly onShutdown: OnShutdown = () => { - // noop - } + private readonly onShutdown?: (reason?: Error | string) => void ) { this.loggingService = new LoggingService(); this.logger = this.loggingService.asLoggerFactory(); - this.log = this.logger.get('root'); + this.configService = new ConfigService(config$, env, this.logger); + this.server = new Server(this.configService, this.logger, this.env); } public async start() { @@ -57,53 +53,46 @@ export class Root { try { await this.setupLogging(); - await this.startServer(); + await this.server.start(); } catch (e) { await this.shutdown(e); throw e; } } - public async shutdown(reason?: Error) { + public async shutdown(reason?: any) { this.log.debug('shutting root down'); - await this.stopServer(); + if (reason) { + if (reason.code === 'EADDRINUSE' && Number.isInteger(reason.port)) { + reason = new Error( + `Port ${reason.port} is already in use. Another instance of Kibana may be running!` + ); + } + + this.log.fatal(reason); + } + + await this.server.stop(); if (this.loggingConfigSubscription !== undefined) { this.loggingConfigSubscription.unsubscribe(); this.loggingConfigSubscription = undefined; } - await this.loggingService.stop(); - this.onShutdown(reason); - } - - protected async startServer() { - this.server = new Server(this.configService, this.logger, this.env); - return this.server.start(); - } - - protected async stopServer() { - if (this.server === undefined) { - return; + if (this.onShutdown !== undefined) { + this.onShutdown(reason); } - - await this.server.stop(); - this.server = undefined; } private async setupLogging() { // Stream that maps config updates to logger updates, including update failures. const update$ = this.configService.atPath('logging', LoggingConfig).pipe( map(config => this.loggingService.upgrade(config)), - catchError(err => { - // This specifically console.logs because we were not able to configure the logger. - // tslint:disable-next-line no-console - console.error('Configuring logger failed:', err); - - throw err; - }), + // This specifically console.logs because we were not able to configure the logger. + // tslint:disable-next-line no-console + tap({ error: err => console.error('Configuring logger failed:', err) }), publishReplay(1) ) as ConnectableObservable; diff --git a/src/core/types/core_service.ts b/src/core/types/core_service.ts index b6031e0deb7ba..8a8ac92b93ccc 100644 --- a/src/core/types/core_service.ts +++ b/src/core/types/core_service.ts @@ -17,7 +17,7 @@ * under the License. */ -export interface CoreService { - start(): Promise; +export interface CoreService { + start(): Promise; stop(): Promise; } diff --git a/src/core/utils/__tests__/__snapshots__/get.test.ts.snap b/src/core/utils/__snapshots__/get.test.ts.snap similarity index 100% rename from src/core/utils/__tests__/__snapshots__/get.test.ts.snap rename to src/core/utils/__snapshots__/get.test.ts.snap diff --git a/src/core/utils/__tests__/get.test.ts b/src/core/utils/get.test.ts similarity index 97% rename from src/core/utils/__tests__/get.test.ts rename to src/core/utils/get.test.ts index a93ad6f6d708e..f409638b5d491 100644 --- a/src/core/utils/__tests__/get.test.ts +++ b/src/core/utils/get.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { get } from '../get'; +import { get } from './get'; const obj = { bar: { diff --git a/src/core/utils/__tests__/url.test.ts b/src/core/utils/url.test.ts similarity index 98% rename from src/core/utils/__tests__/url.test.ts rename to src/core/utils/url.test.ts index 6ff3a75d6e725..f890b2ec13db3 100644 --- a/src/core/utils/__tests__/url.test.ts +++ b/src/core/utils/url.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { modifyUrl } from '../url'; +import { modifyUrl } from './url'; describe('modifyUrl()', () => { test('throws an error with invalid input', () => { diff --git a/src/core_plugins/inspector_views/public/data/__snapshots__/data_view.test.js.snap b/src/core_plugins/inspector_views/public/data/__snapshots__/data_view.test.js.snap index 65dbad218a28a..86ba5ce7cd2bf 100644 --- a/src/core_plugins/inspector_views/public/data/__snapshots__/data_view.test.js.snap +++ b/src/core_plugins/inspector_views/public/data/__snapshots__/data_view.test.js.snap @@ -29,11 +29,11 @@ exports[`Inspector Data View component should render empty state 1`] = ` > +

The element did not provide any data.

- +
} iconColor="subdued" title={ diff --git a/src/core_plugins/kibana/common/field_formats/types/source.js b/src/core_plugins/kibana/common/field_formats/types/source.js index 81a6eb51eb20d..61509a746c163 100644 --- a/src/core_plugins/kibana/common/field_formats/types/source.js +++ b/src/core_plugins/kibana/common/field_formats/types/source.js @@ -48,7 +48,7 @@ export function createSourceFormat(FieldFormat) { SourceFormat.prototype._convert = { text: (value) => toJson(value), html: function sourceToHtml(source, field, hit) { - if (!field) return this.getConverterFor('text')(source, field, hit); + if (!field) return _.escape(this.getConverterFor('text')(source)); const highlights = (hit && hit.highlight) || {}; const formatted = field.indexPattern.formatHit(hit); diff --git a/src/core_plugins/kibana/common/tutorials/filebeat_instructions.js b/src/core_plugins/kibana/common/tutorials/filebeat_instructions.js index 692074d2ce897..b96f1f6b1b7d0 100644 --- a/src/core_plugins/kibana/common/tutorials/filebeat_instructions.js +++ b/src/core_plugins/kibana/common/tutorials/filebeat_instructions.js @@ -17,16 +17,22 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { INSTRUCTION_VARIANT } from './instruction_variant'; import { createTrycloudOption1, createTrycloudOption2 } from './onprem_cloud_instructions'; export const createFilebeatInstructions = () => ({ INSTALL: { OSX: { - title: 'Download and install Filebeat', - textPre: - 'First time using Filebeat? See the [Getting Started Guide]' + - '({config.docs.beats.filebeat}/filebeat-getting-started.html).', + title: i18n.translate('kbn.common.tutorials.filebeatInstructions.install.osxTitle', { + defaultMessage: 'Download and install Filebeat', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatInstructions.install.osxTextPre', { + defaultMessage: 'First time using Filebeat? See the [Getting Started Guide]({linkUrl}).', + values: { + linkUrl: '{config.docs.beats.filebeat}/filebeat-getting-started.html', + }, + }), commands: [ 'curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-{config.kibana.version}-darwin-x86_64.tar.gz', 'tar xzvf filebeat-{config.kibana.version}-darwin-x86_64.tar.gz', @@ -34,76 +40,123 @@ export const createFilebeatInstructions = () => ({ ], }, DEB: { - title: 'Download and install Filebeat', - textPre: - 'First time using Filebeat? See the [Getting Started Guide]' + - '({config.docs.beats.filebeat}/filebeat-getting-started.html).', + title: i18n.translate('kbn.common.tutorials.filebeatInstructions.install.debTitle', { + defaultMessage: 'Download and install Filebeat', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatInstructions.install.debTextPre', { + defaultMessage: 'First time using Filebeat? See the [Getting Started Guide]({linkUrl}).', + values: { + linkUrl: '{config.docs.beats.filebeat}/filebeat-getting-started.html', + }, + }), commands: [ 'curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-{config.kibana.version}-amd64.deb', 'sudo dpkg -i filebeat-{config.kibana.version}-amd64.deb', ], - textPost: - 'Looking for the 32-bit packages? See the [Download page](https://www.elastic.co/downloads/beats/filebeat).', + textPost: i18n.translate('kbn.common.tutorials.filebeatInstructions.install.debTextPost', { + defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({linkUrl}).', + values: { + linkUrl: 'https://www.elastic.co/downloads/beats/filebeat', + }, + }), }, RPM: { - title: 'Download and install Filebeat', - textPre: - 'First time using Filebeat? See the [Getting Started Guide]' + - '({config.docs.beats.filebeat}/filebeat-getting-started.html).', + title: i18n.translate('kbn.common.tutorials.filebeatInstructions.install.rpmTitle', { + defaultMessage: 'Download and install Filebeat', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatInstructions.install.rpmTextPre', { + defaultMessage: 'First time using Filebeat? See the [Getting Started Guide]({linkUrl}).', + values: { + linkUrl: '{config.docs.beats.filebeat}/filebeat-getting-started.html', + }, + }), commands: [ 'curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-{config.kibana.version}-x86_64.rpm', 'sudo rpm -vi filebeat-{config.kibana.version}-x86_64.rpm', ], - textPost: - 'Looking for the 32-bit packages? See the [Download page](https://www.elastic.co/downloads/beats/filebeat).', + textPost: i18n.translate('kbn.common.tutorials.filebeatInstructions.install.rpmTextPost', { + defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({linkUrl}).', + values: { + linkUrl: 'https://www.elastic.co/downloads/beats/filebeat', + }, + }), }, WINDOWS: { - title: 'Download and install Filebeat', - textPre: - 'First time using Filebeat? See the [Getting Started Guide]' + - '({config.docs.beats.filebeat}/filebeat-getting-started.html).\n' + - '1. Download the Filebeat Windows zip file from the [Download](https://www.elastic.co/downloads/beats/filebeat) page.\n' + - '2. Extract the contents of the zip file into `C:\\Program Files`.\n' + - '3. Rename the `filebeat-{config.kibana.version}-windows` directory to `Filebeat`.\n' + - '4. Open a PowerShell prompt as an Administrator (right-click the PowerShell icon and select' + - ' **Run As Administrator**). If you are running Windows XP, you might need to download and install PowerShell.\n' + - '5. From the PowerShell prompt, run the following commands to install Filebeat as a Windows service.', + title: i18n.translate('kbn.common.tutorials.filebeatInstructions.install.windowsTitle', { + defaultMessage: 'Download and install Filebeat', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatInstructions.install.windowsTextPre', { + defaultMessage: 'First time using Filebeat? See the [Getting Started Guide]({guideLinkUrl}).\n\ + 1. Download the Filebeat Windows zip file from the [Download]({filebeatLinkUrl}) page.\n\ + 2. Extract the contents of the zip file into {folderPath}.\n\ + 3. Rename the `{directoryName}` directory to `Filebeat`.\n\ + 4. Open a PowerShell prompt as an Administrator (right-click the PowerShell icon and select \ +**Run As Administrator**). If you are running Windows XP, you might need to download and install PowerShell.\n\ + 5. From the PowerShell prompt, run the following commands to install Filebeat as a Windows service.', + values: { + folderPath: '`C:\\Program Files`', + guideLinkUrl: '{config.docs.beats.filebeat}/filebeat-getting-started.html', + filebeatLinkUrl: 'https://www.elastic.co/downloads/beats/filebeat', + directoryName: 'filebeat-{config.kibana.version}-windows', + } + }), commands: [ 'PS > cd C:\\Program Files\\Filebeat', 'PS C:\\Program Files\\Filebeat> .\\install-service-filebeat.ps1', ], - textPost: - 'Modify the settings under `output.elasticsearch` in the ' + - '`C:\\Program Files\\Filebeat\\filebeat.yml` file to point to your Elasticsearch installation.', - }, + textPost: i18n.translate('kbn.common.tutorials.filebeatInstructions.install.windowsTextPost', { + defaultMessage: 'Modify the settings under {propertyName} in the {filebeatPath} file to point to your Elasticsearch installation.', + values: { + propertyName: '`output.elasticsearch`', + filebeatPath: '`C:\\Program Files\\Filebeat\\filebeat.yml`', + } + }), + } }, START: { OSX: { - title: 'Start Filebeat', - textPre: - 'The `setup` command loads the Kibana dashboards.' + - ' If the dashboards are already set up, omit this command.', - commands: ['./filebeat setup', './filebeat -e'], + title: i18n.translate('kbn.common.tutorials.filebeatInstructions.start.osxTitle', { + defaultMessage: 'Start Filebeat', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatInstructions.start.osxTextPre', { + defaultMessage: 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: [ + './filebeat setup', + './filebeat -e', + ] }, DEB: { - title: 'Start Filebeat', - textPre: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, ' + - 'omit this command.', - commands: ['sudo filebeat setup', 'sudo service filebeat start'], + title: i18n.translate('kbn.common.tutorials.filebeatInstructions.start.debTitle', { + defaultMessage: 'Start Filebeat', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatInstructions.start.debTextPre', { + defaultMessage: 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: [ + 'sudo filebeat setup', + 'sudo service filebeat start', + ] }, RPM: { - title: 'Start Filebeat', - textPre: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, ' + - 'omit this command.', - commands: ['sudo filebeat setup', 'sudo service filebeat start'], + title: i18n.translate('kbn.common.tutorials.filebeatInstructions.start.rpmTitle', { + defaultMessage: 'Start Filebeat', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatInstructions.start.rpmTextPre', { + defaultMessage: 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: [ + 'sudo filebeat setup', + 'sudo service filebeat start', + ], }, WINDOWS: { - title: 'Start Filebeat', - textPre: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, ' + - 'omit this command.', + title: i18n.translate('kbn.common.tutorials.filebeatInstructions.start.windowsTitle', { + defaultMessage: 'Start Filebeat', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatInstructions.start.windowsTextPre', { + defaultMessage: 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), commands: [ 'PS C:\\Program Files\\Filebeat> filebeat.exe setup', 'PS C:\\Program Files\\Filebeat> Start-Service filebeat', @@ -112,8 +165,15 @@ export const createFilebeatInstructions = () => ({ }, CONFIG: { OSX: { - title: 'Edit the configuration', - textPre: 'Modify `filebeat.yml` to set the connection information:', + title: i18n.translate('kbn.common.tutorials.filebeatInstructions.config.osxTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatInstructions.config.osxTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`filebeat.yml`', + }, + }), commands: [ 'output.elasticsearch:', ' hosts: [""]', @@ -122,13 +182,26 @@ export const createFilebeatInstructions = () => ({ 'setup.kibana:', ' host: ""', ], - textPost: - 'Where `` is the password of the `elastic` user, ' + - '`` is the URL of Elasticsearch, and `` is the URL of Kibana.', + textPost: i18n.translate('kbn.common.tutorials.filebeatInstructions.config.osxTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ +and {kibanaUrlTemplate} is the URL of Kibana.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + }, + }), }, DEB: { - title: 'Edit the configuration', - textPre: 'Modify `/etc/filebeat/filebeat.yml` to set the connection information:', + title: i18n.translate('kbn.common.tutorials.filebeatInstructions.config.debTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatInstructions.config.debTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`/etc/filebeat/filebeat.yml`', + }, + }), commands: [ 'output.elasticsearch:', ' hosts: [""]', @@ -137,13 +210,26 @@ export const createFilebeatInstructions = () => ({ 'setup.kibana:', ' host: ""', ], - textPost: - 'Where `` is the password of the `elastic` user, ' + - '`` is the URL of Elasticsearch, and `` is the URL of Kibana.', + textPost: i18n.translate('kbn.common.tutorials.filebeatInstructions.config.debTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ +and {kibanaUrlTemplate} is the URL of Kibana.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + }, + }), }, RPM: { - title: 'Edit the configuration', - textPre: 'Modify `/etc/filebeat/filebeat.yml` to set the connection information:', + title: i18n.translate('kbn.common.tutorials.filebeatInstructions.config.rpmTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatInstructions.config.rpmTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`/etc/filebeat/filebeat.yml`', + }, + }), commands: [ 'output.elasticsearch:', ' hosts: [""]', @@ -152,14 +238,26 @@ export const createFilebeatInstructions = () => ({ 'setup.kibana:', ' host: ""', ], - textPost: - 'Where `` is the password of the `elastic` user, ' + - '`` is the URL of Elasticsearch, and `` is the URL of Kibana.', + textPost: i18n.translate('kbn.common.tutorials.filebeatInstructions.config.rpmTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ +and {kibanaUrlTemplate} is the URL of Kibana.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + }, + }), }, WINDOWS: { - title: 'Edit the configuration', - textPre: - 'Modify `C:\\Program Files\\Filebeat\\filebeat.yml` to set the connection information:', + title: i18n.translate('kbn.common.tutorials.filebeatInstructions.config.windowsTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatInstructions.config.windowsTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`C:\\Program Files\\Filebeat\\filebeat.yml`', + }, + }), commands: [ 'output.elasticsearch:', ' hosts: [""]', @@ -168,98 +266,209 @@ export const createFilebeatInstructions = () => ({ 'setup.kibana:', ' host: ""', ], - textPost: - 'Where `` is the password of the `elastic` user, ' + - '`` is the URL of Elasticsearch, and `` is the URL of Kibana.', - }, + textPost: i18n.translate('kbn.common.tutorials.filebeatInstructions.config.windowsTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ +and {kibanaUrlTemplate} is the URL of Kibana.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + }, + }), + } }, PLUGINS: { GEOIP_AND_UA: { - title: 'Install Elasticsearch GeoIP and user agent plugins', - textPre: - 'This module requires two Elasticsearch plugins that are not ' + - 'installed by default.\n\nFrom the Elasticsearch installation folder, run:', + title: i18n.translate('kbn.common.tutorials.filebeatInstructions.plugins.geoipUaTitle', { + defaultMessage: 'Install Elasticsearch GeoIP and user agent plugins', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatInstructions.plugins.geoipUaTextPre', { + defaultMessage: 'This module requires two Elasticsearch plugins that are not installed by default.\n\n\ +From the Elasticsearch installation folder, run:', + }), commands: [ 'bin/elasticsearch-plugin install ingest-geoip', 'bin/elasticsearch-plugin install ingest-user-agent', ], }, GEOIP: { - title: 'Install Elasticsearch GeoIP plugin', - textPre: - 'This module requires an Elasticsearch plugin that is not ' + - 'installed by default.\n\nFrom the Elasticsearch installation folder, run:', - commands: ['bin/elasticsearch-plugin install ingest-geoip'], - }, - }, + title: i18n.translate('kbn.common.tutorials.filebeatInstructions.plugins.geoipTitle', { + defaultMessage: 'Install Elasticsearch GeoIP plugin', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatInstructions.plugins.geoipTextPre', { + defaultMessage: 'This module requires two Elasticsearch plugins that are not installed by default.\n\n\ +From the Elasticsearch installation folder, run:', + }), + commands: [ + 'bin/elasticsearch-plugin install ingest-geoip' + ] + } + } }); export const createFilebeatCloudInstructions = () => ({ CONFIG: { OSX: { - title: 'Edit the configuration', - textPre: 'Modify `filebeat.yml` to set the connection information for Elastic Cloud:', - commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: 'Where `` is the password of the `elastic` user.', + title: i18n.translate('kbn.common.tutorials.filebeatCloudInstructions.config.osxTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatCloudInstructions.config.osxTextPre', { + defaultMessage: 'Modify {path} to set the connection information for Elastic Cloud:', + values: { + path: '`filebeat.yml`', + }, + }), + commands: [ + 'cloud.id: "{config.cloud.id}"', + 'cloud.auth: "elastic:"' + ], + textPost: i18n.translate('kbn.common.tutorials.filebeatCloudInstructions.config.osxTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', + values: { passwordTemplate: '``' }, + }), }, DEB: { - title: 'Edit the configuration', - textPre: - 'Modify `/etc/filebeat/filebeat.yml` to set the connection information for Elastic Cloud:', - commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: 'Where `` is the password of the `elastic` user.', + title: i18n.translate('kbn.common.tutorials.filebeatCloudInstructions.config.debTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatCloudInstructions.config.debTextPre', { + defaultMessage: 'Modify {path} to set the connection information for Elastic Cloud:', + values: { + path: '`/etc/filebeat/filebeat.yml`', + }, + }), + commands: [ + 'cloud.id: "{config.cloud.id}"', + 'cloud.auth: "elastic:"' + ], + textPost: i18n.translate('kbn.common.tutorials.filebeatCloudInstructions.config.debTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', + values: { passwordTemplate: '``' }, + }), }, RPM: { - title: 'Edit the configuration', - textPre: - 'Modify `/etc/filebeat/filebeat.yml` to set the connection information for Elastic Cloud:', - commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: 'Where `` is the password of the `elastic` user.', + title: i18n.translate('kbn.common.tutorials.filebeatCloudInstructions.config.rpmTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatCloudInstructions.config.rpmTextPre', { + defaultMessage: 'Modify {path} to set the connection information for Elastic Cloud:', + values: { + path: '`/etc/filebeat/filebeat.yml`', + }, + }), + commands: [ + 'cloud.id: "{config.cloud.id}"', + 'cloud.auth: "elastic:"' + ], + textPost: i18n.translate('kbn.common.tutorials.filebeatCloudInstructions.config.rpmTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', + values: { passwordTemplate: '``' }, + }), }, WINDOWS: { - title: 'Edit the configuration', - textPre: - 'Modify `C:\\Program Files\\Filebeat\\filebeat.yml` to set the connection information for Elastic Cloud:', - commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: 'Where `` is the password of the `elastic` user.', - }, - }, + title: i18n.translate('kbn.common.tutorials.filebeatCloudInstructions.config.windowsTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatCloudInstructions.config.windowsTextPre', { + defaultMessage: 'Modify {path} to set the connection information for Elastic Cloud:', + values: { + path: '`C:\\Program Files\\Filebeat\\filebeat.yml`', + }, + }), + commands: [ + 'cloud.id: "{config.cloud.id}"', + 'cloud.auth: "elastic:"' + ], + textPost: i18n.translate('kbn.common.tutorials.filebeatCloudInstructions.config.windowsTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', + values: { passwordTemplate: '``' }, + }), + } + } }); export function filebeatEnableInstructions(moduleName) { return { OSX: { - title: 'Enable and configure the ' + moduleName + ' module', - textPre: 'From the installation directory, run:', - commands: ['./filebeat modules enable ' + moduleName], - textPost: 'Modify the settings in the `modules.d/' + moduleName + '.yml` file.', + title: i18n.translate('kbn.common.tutorials.filebeatEnableInstructions.osxTitle', { + defaultMessage: 'Enable and configure the {moduleName} module', + values: { moduleName }, + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatEnableInstructions.osxTextPre', { + defaultMessage: 'From the installation directory, run:', + }), + commands: [ + './filebeat modules enable ' + moduleName, + ], + textPost: i18n.translate('kbn.common.tutorials.filebeatEnableInstructions.osxTextPost', { + defaultMessage: 'Modify the settings in the `modules.d/{moduleName}.yml` file.', + values: { moduleName }, + }), }, DEB: { - title: 'Enable and configure the ' + moduleName + ' module', - commands: ['sudo filebeat modules enable ' + moduleName], - textPost: 'Modify the settings in the `/etc/filebeat/modules.d/' + moduleName + '.yml` file.', + title: i18n.translate('kbn.common.tutorials.filebeatEnableInstructions.debTitle', { + defaultMessage: 'Enable and configure the {moduleName} module', + values: { moduleName }, + }), + commands: [ + 'sudo filebeat modules enable ' + moduleName, + ], + textPost: i18n.translate('kbn.common.tutorials.filebeatEnableInstructions.debTextPost', { + defaultMessage: 'Modify the settings in the `/etc/filebeat/modules.d/{moduleName}.yml` file.', + values: { moduleName }, + }), }, RPM: { - title: 'Enable and configure the ' + moduleName + ' module', - commands: ['sudo filebeat modules enable ' + moduleName], - textPost: 'Modify the settings in the `/etc/filebeat/modules.d/' + moduleName + '.yml` file.', + title: i18n.translate('kbn.common.tutorials.filebeatEnableInstructions.rpmTitle', { + defaultMessage: 'Enable and configure the {moduleName} module', + values: { moduleName }, + }), + commands: [ + 'sudo filebeat modules enable ' + moduleName, + ], + textPost: i18n.translate('kbn.common.tutorials.filebeatEnableInstructions.rpmTextPost', { + defaultMessage: 'Modify the settings in the `/etc/filebeat/modules.d/{moduleName}.yml` file.', + values: { moduleName }, + }), }, WINDOWS: { - title: 'Enable and configure the ' + moduleName + ' module', - textPre: 'From the `C:\\Program Files\\Filebeat` folder, run:', - commands: ['PS C:\\Program Files\\Filebeat> filebeat.exe modules enable ' + moduleName], - textPost: 'Modify the settings in the `modules.d/' + moduleName + '.yml` file.', - }, + title: i18n.translate('kbn.common.tutorials.filebeatEnableInstructions.windowsTitle', { + defaultMessage: 'Enable and configure the {moduleName} module', + values: { moduleName }, + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatEnableInstructions.windowsTextPre', { + defaultMessage: 'From the {path} folder, run:', + values: { path: `C:\\Program Files\\Filebeat` }, + }), + commands: [ + 'PS C:\\Program Files\\Filebeat> filebeat.exe modules enable ' + moduleName, + ], + textPost: i18n.translate('kbn.common.tutorials.filebeatEnableInstructions.windowsTextPost', { + defaultMessage: 'Modify the settings in the `modules.d/{moduleName}.yml` file.', + values: { moduleName }, + }), + } }; } export function filebeatStatusCheck(moduleName) { return { - title: 'Module status', - text: 'Check that data is received from the Filebeat `' + moduleName + '` module', - btnLabel: 'Check data', - success: 'Data successfully received from this module', - error: 'No data has been received from this module yet', + title: i18n.translate('kbn.common.tutorials.filebeatStatusCheck.title', { + defaultMessage: 'Module status', + }), + text: i18n.translate('kbn.common.tutorials.filebeatStatusCheck.text', { + defaultMessage: 'Check that data is received from the Filebeat `{moduleName}` module', + values: { moduleName }, + }), + btnLabel: i18n.translate('kbn.common.tutorials.filebeatStatusCheck.buttonLabel', { + defaultMessage: 'Check data', + }), + success: i18n.translate('kbn.common.tutorials.filebeatStatusCheck.successText', { + defaultMessage: 'Data successfully received from this module', + }), + error: i18n.translate('kbn.common.tutorials.filebeatStatusCheck.errorText', { + defaultMessage: 'No data has been received from this module yet', + }), esHitsCheck: { index: 'filebeat-*', query: { @@ -299,7 +508,9 @@ export function onPremInstructions(moduleName, platforms, geoipRequired, uaRequi return { instructionSets: [ { - title: 'Getting Started', + title: i18n.translate('kbn.common.tutorials.filebeat.premInstructions.gettingStarted.title', { + defaultMessage: 'Getting Started', + }), instructionVariants: variants, statusCheck: filebeatStatusCheck(moduleName), }, @@ -331,7 +542,9 @@ export function onPremCloudInstructions(moduleName, platforms) { return { instructionSets: [ { - title: 'Getting Started', + title: i18n.translate('kbn.common.tutorials.filebeat.premCloudInstructions.gettingStarted.title', { + defaultMessage: 'Getting Started', + }), instructionVariants: variants, statusCheck: filebeatStatusCheck(moduleName), }, @@ -360,7 +573,9 @@ export function cloudInstructions(moduleName, platforms) { return { instructionSets: [ { - title: 'Getting Started', + title: i18n.translate('kbn.common.tutorials.filebeat.cloudInstructions.gettingStarted.title', { + defaultMessage: 'Getting Started', + }), instructionVariants: variants, statusCheck: filebeatStatusCheck(moduleName), }, diff --git a/src/core_plugins/kibana/common/tutorials/logstash_instructions.js b/src/core_plugins/kibana/common/tutorials/logstash_instructions.js index 4c52314236d10..5203f44ef9c8d 100644 --- a/src/core_plugins/kibana/common/tutorials/logstash_instructions.js +++ b/src/core_plugins/kibana/common/tutorials/logstash_instructions.js @@ -17,19 +17,28 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; + export const createLogstashInstructions = () => ({ INSTALL: { OSX: [ { - title: 'Download and install the Java Runtime Environment', - textPre: - 'Follow the installation instructions [here](https://docs.oracle.com/javase/8/docs/technotes/guides/install/mac_jre.html).', + title: i18n.translate('kbn.common.tutorials.logstashInstructions.install.java.osxTitle', { + defaultMessage: 'Download and install the Java Runtime Environment', + }), + textPre: i18n.translate('kbn.common.tutorials.logstashInstructions.install.java.osxTextPre', { + defaultMessage: 'Follow the installation instructions [here]({link}).', + values: { link: 'https://docs.oracle.com/javase/8/docs/technotes/guides/install/mac_jre.html' }, + }), }, { - title: 'Download and install Logstash', - textPre: - 'First time using Logstash? See the ' + - '[Getting Started Guide]({config.docs.base_url}guide/en/logstash/current/getting-started-with-logstash.html).', + title: i18n.translate('kbn.common.tutorials.logstashInstructions.install.logstash.osxTitle', { + defaultMessage: 'Download and install Logstash', + }), + textPre: i18n.translate('kbn.common.tutorials.logstashInstructions.install.logstash.osxTextPre', { + defaultMessage: 'First time using Logstash? See the [Getting Started Guide]({link}).', + values: { link: '{config.docs.base_url}guide/en/logstash/current/getting-started-with-logstash.html' }, + }), commands: [ 'curl -L -O https://artifacts.elastic.co/downloads/logstash/logstash-{config.kibana.version}.tar.gz', 'tar xzvf logstash-{config.kibana.version}.tar.gz', @@ -38,18 +47,28 @@ export const createLogstashInstructions = () => ({ ], WINDOWS: [ { - title: 'Download and install the Java Runtime Environment', - textPre: - 'Follow the installation instructions [here](https://docs.oracle.com/javase/8/docs/technotes/guides/install/windows_jre_install.html).', + title: i18n.translate('kbn.common.tutorials.logstashInstructions.install.java.windowsTitle', { + defaultMessage: 'Download and install the Java Runtime Environment', + }), + textPre: i18n.translate('kbn.common.tutorials.logstashInstructions.install.java.windowsTextPre', { + defaultMessage: 'Follow the installation instructions [here]({link}).', + values: { link: 'https://docs.oracle.com/javase/8/docs/technotes/guides/install/windows_jre_install.html' }, + }), }, { - title: 'Download and install Logstash', - textPre: - 'First time using Logstash? See the ' + - '[Getting Started Guide]({config.docs.base_url}guide/en/logstash/current/getting-started-with-logstash.html).\n' + - ' 1. [Download](https://artifacts.elastic.co/downloads/logstash/logstash-{config.kibana.version}.zip) the Logstash Windows zip file.\n' + - ' 2. Extract the contents of the zip file.', - }, + title: i18n.translate('kbn.common.tutorials.logstashInstructions.install.logstash.windowsTitle', { + defaultMessage: 'Download and install Logstash', + }), + textPre: i18n.translate('kbn.common.tutorials.logstashInstructions.install.logstash.windowsTextPre', { + defaultMessage: 'First time using Logstash? See the [Getting Started Guide]({logstashLink}).\n\ + 1. [Download]({elasticLink}) the Logstash Windows zip file.\n\ + 2. Extract the contents of the zip file.', + values: { + logstashLink: '{config.docs.base_url}guide/en/logstash/current/getting-started-with-logstash.html', + elasticLink: 'https://artifacts.elastic.co/downloads/logstash/logstash-{config.kibana.version}.zip' + }, + }), + } ], }, }); diff --git a/src/core_plugins/kibana/common/tutorials/metricbeat_instructions.js b/src/core_plugins/kibana/common/tutorials/metricbeat_instructions.js index 8e1b9b95bcb47..b15d3aaab1e7e 100644 --- a/src/core_plugins/kibana/common/tutorials/metricbeat_instructions.js +++ b/src/core_plugins/kibana/common/tutorials/metricbeat_instructions.js @@ -17,16 +17,20 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { INSTRUCTION_VARIANT } from './instruction_variant'; import { createTrycloudOption1, createTrycloudOption2 } from './onprem_cloud_instructions'; export const createMetricbeatInstructions = () => ({ INSTALL: { OSX: { - title: 'Download and install Metricbeat', - textPre: - 'First time using Metricbeat? See the [Getting Started Guide]' + - '({config.docs.beats.metricbeat}/metricbeat-getting-started.html).', + title: i18n.translate('kbn.common.tutorials.metricbeatInstructions.install.osxTitle', { + defaultMessage: 'Download and install Metricbeat', + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatInstructions.install.osxTextPre', { + defaultMessage: 'First time using Metricbeat? See the [Getting Started Guide]({link}).', + values: { link: '{config.docs.beats.metricbeat}/metricbeat-getting-started.html' }, + }), commands: [ 'curl -L -O https://artifacts.elastic.co/downloads/beats/metricbeat/metricbeat-{config.kibana.version}-darwin-x86_64.tar.gz', 'tar xzvf metricbeat-{config.kibana.version}-darwin-x86_64.tar.gz', @@ -34,76 +38,112 @@ export const createMetricbeatInstructions = () => ({ ], }, DEB: { - title: 'Download and install Metricbeat', - textPre: - 'First time using Metricbeat? See the [Getting Started Guide]' + - '({config.docs.beats.metricbeat}/metricbeat-getting-started.html).', + title: i18n.translate('kbn.common.tutorials.metricbeatInstructions.install.debTitle', { + defaultMessage: 'Download and install Metricbeat', + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatInstructions.install.debTextPre', { + defaultMessage: 'First time using Metricbeat? See the [Getting Started Guide]({link}).', + values: { link: '{config.docs.beats.metricbeat}/metricbeat-getting-started.html' }, + }), commands: [ 'curl -L -O https://artifacts.elastic.co/downloads/beats/metricbeat/metricbeat-{config.kibana.version}-amd64.deb', 'sudo dpkg -i metricbeat-{config.kibana.version}-amd64.deb', ], - textPost: - 'Looking for the 32-bit packages? See the [Download page](https://www.elastic.co/downloads/beats/metricbeat).', + textPost: i18n.translate('kbn.common.tutorials.metricbeatInstructions.install.debTextPost', { + defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({link}).', + values: { link: 'https://www.elastic.co/downloads/beats/metricbeat' }, + }), }, RPM: { - title: 'Download and install Metricbeat', - textPre: - 'First time using Metricbeat? See the [Getting Started Guide]' + - '({config.docs.beats.metricbeat}/metricbeat-getting-started.html).', + title: i18n.translate('kbn.common.tutorials.metricbeatInstructions.install.rpmTitle', { + defaultMessage: 'Download and install Metricbeat', + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatInstructions.install.rpmTextPre', { + defaultMessage: 'First time using Metricbeat? See the [Getting Started Guide]({link}).', + values: { link: '{config.docs.beats.metricbeat}/metricbeat-getting-started.html' }, + }), commands: [ 'curl -L -O https://artifacts.elastic.co/downloads/beats/metricbeat/metricbeat-{config.kibana.version}-x86_64.rpm', 'sudo rpm -vi metricbeat-{config.kibana.version}-x86_64.rpm', ], - textPost: - 'Looking for the 32-bit packages? See the [Download page](https://www.elastic.co/downloads/beats/metricbeat).', + textPost: i18n.translate('kbn.common.tutorials.metricbeatInstructions.install.debTextPost', { + defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({link}).', + values: { link: 'https://www.elastic.co/downloads/beats/metricbeat' }, + }), }, WINDOWS: { - title: 'Download and install Metricbeat', - textPre: - 'First time using Metricbeat? See the [Getting Started Guide]' + - '({config.docs.beats.metricbeat}/metricbeat-getting-started.html).\n' + - '1. Download the Metricbeat Windows zip file from the [Download](https://www.elastic.co/downloads/beats/metricbeat) page.\n' + - '2. Extract the contents of the zip file into `C:\\Program Files`.\n' + - '3. Rename the `metricbeat-{config.kibana.version}-windows` directory to `Metricbeat`.\n' + - '4. Open a PowerShell prompt as an Administrator (right-click the PowerShell icon and select' + - ' **Run As Administrator**). If you are running Windows XP, you might need to download and install PowerShell.\n' + - '5. From the PowerShell prompt, run the following commands to install Metricbeat as a Windows service.', + title: i18n.translate('kbn.common.tutorials.metricbeatInstructions.install.windowsTitle', { + defaultMessage: 'Download and install Metricbeat', + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatInstructions.install.windowsTextPre', { + defaultMessage: 'First time using Metricbeat? See the [Getting Started Guide]({metricbeatLink}).\n\ + 1. Download the Metricbeat Windows zip file from the [Download]({elasticLink}) page.\n\ + 2. Extract the contents of the zip file into {folderPath}.\n\ + 3. Rename the {directoryName} directory to `Metricbeat`.\n\ + 4. Open a PowerShell prompt as an Administrator (right-click the PowerShell icon and select \ +**Run As Administrator**). If you are running Windows XP, you might need to download and install PowerShell.\n\ + 5. From the PowerShell prompt, run the following commands to install Metricbeat as a Windows service.', + values: { + directoryName: '`metricbeat-{config.kibana.version}-windows`', + folderPath: '`C:\\Program Files`', + metricbeatLink: '{config.docs.beats.metricbeat}/metricbeat-getting-started.html', + elasticLink: 'https://www.elastic.co/downloads/beats/metricbeat', + }, + }), commands: [ 'PS > cd C:\\Program Files\\Metricbeat', 'PS C:\\Program Files\\Metricbeat> .\\install-service-metricbeat.ps1', ], - textPost: - 'Modify the settings under `output.elasticsearch` in the ' + - '`C:\\Program Files\\Metricbeat\\metricbeat.yml` file to point to your Elasticsearch installation.', - }, + textPost: i18n.translate('kbn.common.tutorials.metricbeatInstructions.install.windowsTextPost', { + defaultMessage: 'Modify the settings under `output.elasticsearch` in the {path} file to point to your Elasticsearch installation.', + values: { path: '`C:\\Program Files\\Metricbeat\\metricbeat.yml`' }, + }), + } }, START: { OSX: { - title: 'Start Metricbeat', - textPre: - 'The `setup` command loads the Kibana dashboards.' + - ' If the dashboards are already set up, omit this command.', - commands: ['./metricbeat setup', './metricbeat -e'], + title: i18n.translate('kbn.common.tutorials.metricbeatInstructions.start.osxTitle', { + defaultMessage: 'Start Metricbeat', + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatInstructions.start.osxTextPre', { + defaultMessage: 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: [ + './metricbeat setup', + './metricbeat -e', + ] }, DEB: { - title: 'Start Metricbeat', - textPre: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, ' + - 'omit this command.', - commands: ['sudo metricbeat setup', 'sudo service metricbeat start'], + title: i18n.translate('kbn.common.tutorials.metricbeatInstructions.start.debTitle', { + defaultMessage: 'Start Metricbeat', + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatInstructions.start.debTextPre', { + defaultMessage: 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: [ + 'sudo metricbeat setup', + 'sudo service metricbeat start', + ] }, RPM: { - title: 'Start Metricbeat', - textPre: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, ' + - 'omit this command.', - commands: ['sudo metricbeat setup', 'sudo service metricbeat start'], + title: i18n.translate('kbn.common.tutorials.metricbeatInstructions.start.rpmTitle', { + defaultMessage: 'Start Metricbeat', + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatInstructions.start.rpmTextPre', { + defaultMessage: 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: [ + 'sudo metricbeat setup', + 'sudo service metricbeat start', + ], }, WINDOWS: { - title: 'Start Metricbeat', - textPre: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, ' + - 'omit this command.', + title: i18n.translate('kbn.common.tutorials.metricbeatInstructions.start.windowsTitle', { + defaultMessage: 'Start Metricbeat', + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatInstructions.start.windowsTextPre', { + defaultMessage: 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), commands: [ 'PS C:\\Program Files\\Metricbeat> metricbeat.exe setup', 'PS C:\\Program Files\\Metricbeat> Start-Service metricbeat', @@ -112,8 +152,15 @@ export const createMetricbeatInstructions = () => ({ }, CONFIG: { OSX: { - title: 'Edit the configuration', - textPre: 'Modify `metricbeat.yml` to set the connection information:', + title: i18n.translate('kbn.common.tutorials.metricbeatInstructions.config.osxTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatInstructions.config.osxTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`metricbeat.yml`', + }, + }), commands: [ 'output.elasticsearch:', ' hosts: [""]', @@ -122,13 +169,26 @@ export const createMetricbeatInstructions = () => ({ 'setup.kibana:', ' host: ""', ], - textPost: - 'Where `` is the password of the `elastic` user, ' + - '`` is the URL of Elasticsearch, and `` is the URL of Kibana.', + textPost: i18n.translate('kbn.common.tutorials.metricbeatInstructions.config.osxTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ +and {kibanaUrlTemplate} is the URL of Kibana.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + }, + }), }, DEB: { - title: 'Edit the configuration', - textPre: 'Modify `/etc/metricbeat/metricbeat.yml` to set the connection information:', + title: i18n.translate('kbn.common.tutorials.metricbeatInstructions.config.debTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatInstructions.config.debTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`/etc/metricbeat/metricbeat.yml`', + }, + }), commands: [ 'output.elasticsearch:', ' hosts: [""]', @@ -137,13 +197,26 @@ export const createMetricbeatInstructions = () => ({ 'setup.kibana:', ' host: ""', ], - textPost: - 'Where `` is the password of the `elastic` user, ' + - '`` is the URL of Elasticsearch, and `` is the URL of Kibana.', + textPost: i18n.translate('kbn.common.tutorials.metricbeatInstructions.config.debTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ +and {kibanaUrlTemplate} is the URL of Kibana.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + }, + }), }, RPM: { - title: 'Edit the configuration', - textPre: 'Modify `/etc/metricbeat/metricbeat.yml` to set the connection information:', + title: i18n.translate('kbn.common.tutorials.metricbeatInstructions.config.rpmTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatInstructions.config.rpmTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`/etc/metricbeat/metricbeat.yml`', + }, + }), commands: [ 'output.elasticsearch:', ' hosts: [""]', @@ -152,14 +225,26 @@ export const createMetricbeatInstructions = () => ({ 'setup.kibana:', ' host: ""', ], - textPost: - 'Where `` is the password of the `elastic` user, ' + - '`` is the URL of Elasticsearch, and `` is the URL of Kibana.', + textPost: i18n.translate('kbn.common.tutorials.metricbeatInstructions.config.rpmTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ +and {kibanaUrlTemplate} is the URL of Kibana.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + }, + }), }, WINDOWS: { - title: 'Edit the configuration', - textPre: - 'Modify `C:\\Program Files\\Metricbeat\\metricbeat.yml` to set the connection information:', + title: i18n.translate('kbn.common.tutorials.metricbeatInstructions.config.windowsTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatInstructions.config.windowsTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`C:\\Program Files\\Metricbeat\\metricbeat.yml`', + }, + }), commands: [ 'output.elasticsearch:', ' hosts: [""]', @@ -168,81 +253,182 @@ export const createMetricbeatInstructions = () => ({ 'setup.kibana:', ' host: ""', ], - textPost: - 'Where `` is the password of the `elastic` user, ' + - '`` is the URL of Elasticsearch, and `` is the URL of Kibana.', - }, - }, + textPost: i18n.translate('kbn.common.tutorials.metricbeatInstructions.config.windowsTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ +and {kibanaUrlTemplate} is the URL of Kibana.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + }, + }), + } + } }); export const createMetricbeatCloudInstructions = () => ({ CONFIG: { OSX: { - title: 'Edit the configuration', - textPre: 'Modify `metricbeat.yml` to set the connection information for Elastic Cloud:', - commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: 'Where `` is the password of the `elastic` user.', + title: i18n.translate('kbn.common.tutorials.metricbeatCloudInstructions.config.osxTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatCloudInstructions.config.osxTextPre', { + defaultMessage: 'Modify {path} to set the connection information for Elastic Cloud:', + values: { + path: '`metricbeat.yml`', + }, + }), + commands: [ + 'cloud.id: "{config.cloud.id}"', + 'cloud.auth: "elastic:"' + ], + textPost: i18n.translate('kbn.common.tutorials.metricbeatCloudInstructions.config.osxTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', + values: { passwordTemplate: '``' }, + }), }, DEB: { - title: 'Edit the configuration', - textPre: - 'Modify `/etc/metricbeat/metricbeat.yml` to set the connection information for Elastic Cloud:', - commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: 'Where `` is the password of the `elastic` user.', + title: i18n.translate('kbn.common.tutorials.metricbeatCloudInstructions.config.debTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatCloudInstructions.config.debTextPre', { + defaultMessage: 'Modify {path} to set the connection information for Elastic Cloud:', + values: { + path: '`/etc/metricbeat/metricbeat.yml`', + }, + }), + commands: [ + 'cloud.id: "{config.cloud.id}"', + 'cloud.auth: "elastic:"' + ], + textPost: i18n.translate('kbn.common.tutorials.metricbeatCloudInstructions.config.debTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', + values: { passwordTemplate: '``' }, + }), }, RPM: { - title: 'Edit the configuration', - textPre: - 'Modify `/etc/metricbeat/metricbeat.yml` to set the connection information for Elastic Cloud:', - commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: 'Where `` is the password of the `elastic` user.', + title: i18n.translate('kbn.common.tutorials.metricbeatCloudInstructions.config.rpmTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatCloudInstructions.config.rpmTextPre', { + defaultMessage: 'Modify {path} to set the connection information for Elastic Cloud:', + values: { + path: '`/etc/metricbeat/metricbeat.yml`', + }, + }), + commands: [ + 'cloud.id: "{config.cloud.id}"', + 'cloud.auth: "elastic:"' + ], + textPost: i18n.translate('kbn.common.tutorials.metricbeatCloudInstructions.config.rpmTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', + values: { passwordTemplate: '``' }, + }), }, WINDOWS: { - title: 'Edit the configuration', - textPre: - 'Modify `C:\\Program Files\\Filebeat\\metricbeat.yml` to set the connection information for Elastic Cloud:', - commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: 'Where `` is the password of the `elastic` user.', - }, - }, + title: i18n.translate('kbn.common.tutorials.metricbeatCloudInstructions.config.windowsTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatCloudInstructions.config.windowsTextPre', { + defaultMessage: 'Modify {path} to set the connection information for Elastic Cloud:', + values: { + path: '`C:\\Program Files\\Metricbeat\\metricbeat.yml`', + }, + }), + commands: [ + 'cloud.id: "{config.cloud.id}"', + 'cloud.auth: "elastic:"' + ], + textPost: i18n.translate('kbn.common.tutorials.metricbeatCloudInstructions.config.windowsTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', + values: { passwordTemplate: '``' }, + }), + } + } }); export function metricbeatEnableInstructions(moduleName) { return { OSX: { - title: 'Enable and configure the ' + moduleName + ' module', - textPre: 'From the installation directory, run:', - commands: ['./metricbeat modules enable ' + moduleName], - textPost: 'Modify the settings in the `modules.d/' + moduleName + '.yml` file.', + title: i18n.translate('kbn.common.tutorials.metricbeatEnableInstructions.osxTitle', { + defaultMessage: 'Enable and configure the {moduleName} module', + values: { moduleName }, + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatEnableInstructions.osxTextPre', { + defaultMessage: 'From the installation directory, run:', + }), + commands: [ + './metricbeat modules enable ' + moduleName, + ], + textPost: i18n.translate('kbn.common.tutorials.metricbeatEnableInstructions.osxTextPost', { + defaultMessage: 'Modify the settings in the `modules.d/{moduleName}.yml` file.', + values: { moduleName }, + }), }, DEB: { - title: 'Enable and configure the ' + moduleName + ' module', - commands: ['sudo metricbeat modules enable ' + moduleName], - textPost: - 'Modify the settings in the `/etc/metricbeat/modules.d/' + moduleName + '.yml` file.', + title: i18n.translate('kbn.common.tutorials.metricbeatEnableInstructions.debTitle', { + defaultMessage: 'Enable and configure the {moduleName} module', + values: { moduleName }, + }), + commands: [ + 'sudo metricbeat modules enable ' + moduleName, + ], + textPost: i18n.translate('kbn.common.tutorials.metricbeatEnableInstructions.debTextPost', { + defaultMessage: 'Modify the settings in the `/etc/metricbeat/modules.d/{moduleName}.yml` file.', + values: { moduleName }, + }), }, RPM: { - title: 'Enable and configure the ' + moduleName + ' module', - commands: ['sudo metricbeat modules enable ' + moduleName], - textPost: - 'Modify the settings in the `/etc/metricbeat/modules.d/' + moduleName + '.yml` file.', + title: i18n.translate('kbn.common.tutorials.metricbeatEnableInstructions.rpmTitle', { + defaultMessage: 'Enable and configure the {moduleName} module', + values: { moduleName }, + }), + commands: [ + 'sudo metricbeat modules enable ' + moduleName, + ], + textPost: i18n.translate('kbn.common.tutorials.metricbeatEnableInstructions.rpmTextPost', { + defaultMessage: 'Modify the settings in the `/etc/metricbeat/modules.d/{moduleName}.yml` file.', + values: { moduleName }, + }), }, WINDOWS: { - title: 'Enable and configure the ' + moduleName + ' module', - textPre: 'From the `C:\\Program Files\\Metricbeat` folder, run:', - commands: ['PS C:\\Program Files\\Metricbeat> metricbeat.exe modules enable ' + moduleName], - textPost: 'Modify the settings in the `modules.d/' + moduleName + '.yml` file.', - }, + title: i18n.translate('kbn.common.tutorials.metricbeatEnableInstructions.windowsTitle', { + defaultMessage: 'Enable and configure the {moduleName} module', + values: { moduleName }, + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatEnableInstructions.windowsTextPre', { + defaultMessage: 'From the {path} folder, run:', + values: { path: `C:\\Program Files\\Metricbeat` }, + }), + commands: [ + 'PS C:\\Program Files\\Metricbeat> metricbeat.exe modules enable ' + moduleName, + ], + textPost: i18n.translate('kbn.common.tutorials.metricbeatEnableInstructions.windowsTextPost', { + defaultMessage: 'Modify the settings in the `modules.d/{moduleName}.yml` file.', + values: { moduleName }, + }), + } }; } export function metricbeatStatusCheck(moduleName) { return { - title: 'Module status', - text: 'Check that data is received from the Metricbeat `' + moduleName + '` module', - btnLabel: 'Check data', - success: 'Data successfully received from this module', - error: 'No data has been received from this module yet', + title: i18n.translate('kbn.common.tutorials.metricbeatStatusCheck.title', { + defaultMessage: 'Module status', + }), + text: i18n.translate('kbn.common.tutorials.metricbeatStatusCheck.text', { + defaultMessage: 'Check that data is received from the Metricbeat `{moduleName}` module', + values: { moduleName }, + }), + btnLabel: i18n.translate('kbn.common.tutorials.metricbeatStatusCheck.buttonLabel', { + defaultMessage: 'Check data', + }), + success: i18n.translate('kbn.common.tutorials.metricbeatStatusCheck.successText', { + defaultMessage: 'Data successfully received from this module', + }), + error: i18n.translate('kbn.common.tutorials.metricbeatStatusCheck.errorText', { + defaultMessage: 'No data has been received from this module yet', + }), esHitsCheck: { index: 'metricbeat-*', query: { @@ -264,7 +450,9 @@ export function onPremInstructions(moduleName) { return { instructionSets: [ { - title: 'Getting Started', + title: i18n.translate('kbn.common.tutorials.metricbeat.premInstructions.gettingStarted.title', { + defaultMessage: 'Getting Started', + }), instructionVariants: [ { id: INSTRUCTION_VARIANT.OSX, @@ -317,7 +505,9 @@ export function onPremCloudInstructions(moduleName) { return { instructionSets: [ { - title: 'Getting Started', + title: i18n.translate('kbn.common.tutorials.metricbeat.premCloudInstructions.gettingStarted.title', { + defaultMessage: 'Getting Started', + }), instructionVariants: [ { id: INSTRUCTION_VARIANT.OSX, @@ -377,7 +567,9 @@ export function cloudInstructions(moduleName) { return { instructionSets: [ { - title: 'Getting Started', + title: i18n.translate('kbn.common.tutorials.metricbeat.cloudInstructions.gettingStarted.title', { + defaultMessage: 'Getting Started', + }), instructionVariants: [ { id: INSTRUCTION_VARIANT.OSX, diff --git a/src/core_plugins/kibana/common/tutorials/onprem_cloud_instructions.js b/src/core_plugins/kibana/common/tutorials/onprem_cloud_instructions.js index eec1848c06a93..088038ab5beb1 100644 --- a/src/core_plugins/kibana/common/tutorials/onprem_cloud_instructions.js +++ b/src/core_plugins/kibana/common/tutorials/onprem_cloud_instructions.js @@ -17,24 +17,39 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; + export const createTrycloudOption1 = () => ({ - title: 'Option 1: Try module in Elastic Cloud', - textPre: - 'Go to [Elastic Cloud](https://www.elastic.co/cloud/as-a-service/signup?blade=kib). Register if you ' + - 'do not already have an account. Free 14-day trial available.\n\n' + - 'Log into the Elastic Cloud console\n\n' + - 'To create a cluster, in Elastic Cloud console:\n' + - ' 1. Select **Create Deployment** and specify the **Deployment Name**\n' + - ' 2. Modify the other deployment options as needed (or not, the defaults are great to get started)\n' + - ' 3. Click **Create Deployment**\n' + - ' 4. Wait until deployment creation completes\n' + - ' 5. Go to the new Cloud Kibana instance and follow the Kibana Home instructions', + title: i18n.translate('kbn.common.tutorials.premCloudInstructions.option1.title', { + defaultMessage: 'Option 1: Try module in Elastic Cloud', + }), + textPre: i18n.translate('kbn.common.tutorials.premCloudInstructions.option1.textPre', { + defaultMessage: 'Go to [Elastic Cloud]({link}). Register if you \ +do not already have an account. Free 14-day trial available.\n\n\ +Log into the Elastic Cloud console\n\n\ +To create a cluster, in Elastic Cloud console:\n\ + 1. Select **Create Deployment** and specify the **Deployment Name**\n\ + 2. Modify the other deployment options as needed (or not, the defaults are great to get started)\n\ + 3. Click **Create Deployment**\n\ + 4. Wait until deployment creation completes\n\ + 5. Go to the new Cloud Kibana instance and follow the Kibana Home instructions', + values: { + link: 'https://www.elastic.co/cloud/as-a-service/signup?blade=kib', + } + }), }); export const createTrycloudOption2 = () => ({ - title: 'Option 2: Connect local Kibana to a Cloud instance', - textPre: - 'If you are running this Kibana instance against a hosted Elasticsearch instance,' + - ' proceed with manual setup.\n\n' + - 'Save the **Elasticsearch** endpoint as `` and the cluster **Password** as `` for your records', + title: i18n.translate('kbn.common.tutorials.premCloudInstructions.option2.title', { + defaultMessage: 'Option 2: Connect local Kibana to a Cloud instance', + }), + textPre: i18n.translate('kbn.common.tutorials.premCloudInstructions.option2.textPre', { + defaultMessage: 'If you are running this Kibana instance against a hosted Elasticsearch instance, \ +proceed with manual setup.\n\n\ +Save the **Elasticsearch** endpoint as {urlTemplate} and the cluster **Password** as {passwordTemplate} for your records', + values: { + urlTemplate: '``', + passwordTemplate: '``', + } + }), }); diff --git a/src/core_plugins/kibana/public/context/api/__tests__/predecessors.js b/src/core_plugins/kibana/public/context/api/__tests__/predecessors.js index 2e461ff836fe6..7caeec57f4cf2 100644 --- a/src/core_plugins/kibana/public/context/api/__tests__/predecessors.js +++ b/src/core_plugins/kibana/public/context/api/__tests__/predecessors.js @@ -103,7 +103,7 @@ describe('context app', function () { // should have started at the given time expect(intervals[0].gte).to.eql(MS_PER_DAY * 3000); // should have ended with a half-open interval - expect(_.last(intervals)).to.only.have.key('gte'); + expect(_.last(intervals)).to.only.have.keys('gte', 'format'); expect(intervals.length).to.be.greaterThan(1); expect(hits).to.eql(searchSourceStub._stubHits.slice(0, 3)); diff --git a/src/core_plugins/kibana/public/context/api/__tests__/successors.js b/src/core_plugins/kibana/public/context/api/__tests__/successors.js index 1974d55655e25..e9c3e94829b71 100644 --- a/src/core_plugins/kibana/public/context/api/__tests__/successors.js +++ b/src/core_plugins/kibana/public/context/api/__tests__/successors.js @@ -103,7 +103,7 @@ describe('context app', function () { // should have started at the given time expect(intervals[0].lte).to.eql(MS_PER_DAY * 3000); // should have ended with a half-open interval - expect(_.last(intervals)).to.only.have.key('lte'); + expect(_.last(intervals)).to.only.have.keys('lte', 'format'); expect(intervals.length).to.be.greaterThan(1); expect(hits).to.eql(searchSourceStub._stubHits.slice(-3)); diff --git a/src/core_plugins/kibana/public/context/api/context.js b/src/core_plugins/kibana/public/context/api/context.js index 68ca81e83a20e..6b60eea7b7675 100644 --- a/src/core_plugins/kibana/public/context/api/context.js +++ b/src/core_plugins/kibana/public/context/api/context.js @@ -216,6 +216,7 @@ function fetchContextProvider(indexPatterns, Private) { filter: { range: { [timeField]: { + format: 'epoch_millis', ...startRange, ...endRange, } diff --git a/src/core_plugins/kibana/public/dashboard/dashboard_app.js b/src/core_plugins/kibana/public/dashboard/dashboard_app.js index 43b29ab5a877a..9f65e52997440 100644 --- a/src/core_plugins/kibana/public/dashboard/dashboard_app.js +++ b/src/core_plugins/kibana/public/dashboard/dashboard_app.js @@ -48,7 +48,7 @@ import { migrateLegacyQuery } from 'ui/utils/migrateLegacyQuery'; import * as filterActions from 'ui/doc_table/actions/filter'; import { FilterManagerProvider } from 'ui/filter_manager'; import { EmbeddableFactoriesRegistryProvider } from 'ui/embeddable/embeddable_factories_registry'; -import { DashboardPanelActionsRegistryProvider } from 'ui/dashboard_panel_actions/dashboard_panel_actions_registry'; +import { ContextMenuActionsRegistryProvider } from 'ui/embeddable'; import { VisTypesRegistryProvider } from 'ui/registry/vis_types'; import { timefilter } from 'ui/timefilter'; import { getUnhashableStatesProvider } from 'ui/state_management/state_hashing'; @@ -84,7 +84,7 @@ app.directive('dashboardApp', function ($injector) { const filterBar = Private(FilterBarQueryFilterProvider); const docTitle = Private(DocTitleProvider); const embeddableFactories = Private(EmbeddableFactoriesRegistryProvider); - const panelActionsRegistry = Private(DashboardPanelActionsRegistryProvider); + const panelActionsRegistry = Private(ContextMenuActionsRegistryProvider); const getUnhashableStates = Private(getUnhashableStatesProvider); panelActionsStore.initializeFromRegistry(panelActionsRegistry); diff --git a/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap b/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap index f8dd924d09f41..45517f756370f 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap +++ b/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap @@ -212,7 +212,7 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = ` } body={ - +

You can combine data views from any Kibana app into one dashboard and see everything in one place.

@@ -227,7 +227,7 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = ` to take a test drive.

-
+ } iconColor="subdued" iconType="dashboardApp" diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_customize_panel_action.tsx b/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_customize_panel_action.tsx index ce11c1b1820b0..f7cd97666b5c3 100644 --- a/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_customize_panel_action.tsx +++ b/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_customize_panel_action.tsx @@ -19,7 +19,7 @@ import { EuiIcon } from '@elastic/eui'; import React from 'react'; -import { DashboardContextMenuPanel, DashboardPanelAction } from 'ui/dashboard_panel_actions'; +import { ContextMenuAction, ContextMenuPanel } from 'ui/embeddable'; import { DashboardViewMode } from '../../../dashboard_view_mode'; import { PanelOptionsMenuForm } from '../panel_options_menu_form'; @@ -33,15 +33,15 @@ export function getCustomizePanelAction({ onUpdatePanelTitle: (title: string) => void; closeContextMenu: () => void; title?: string; -}): DashboardPanelAction { - return new DashboardPanelAction( +}): ContextMenuAction { + return new ContextMenuAction( { displayName: 'Customize panel', id: 'customizePanel', parentPanelId: 'mainMenu', }, { - childContextMenuPanel: new DashboardContextMenuPanel( + childContextMenuPanel: new ContextMenuPanel( { id: 'panelSubOptionsMenu', title: 'Customize panel', diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_edit_panel_action.tsx b/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_edit_panel_action.tsx index 5161b44f4b490..0b7bda9d1dfbf 100644 --- a/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_edit_panel_action.tsx +++ b/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_edit_panel_action.tsx @@ -21,15 +21,15 @@ import React from 'react'; import { EuiIcon } from '@elastic/eui'; -import { DashboardPanelAction } from 'ui/dashboard_panel_actions'; +import { ContextMenuAction } from 'ui/embeddable'; import { DashboardViewMode } from '../../../dashboard_view_mode'; /** * - * @return {DashboardPanelAction} + * @return {ContextMenuAction} */ export function getEditPanelAction() { - return new DashboardPanelAction( + return new ContextMenuAction( { displayName: 'Edit visualization', id: 'editPanel', diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_inspector_panel_action.tsx b/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_inspector_panel_action.tsx index 10dc81a6a2bb4..2739e2859a429 100644 --- a/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_inspector_panel_action.tsx +++ b/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_inspector_panel_action.tsx @@ -21,7 +21,7 @@ import React from 'react'; import { EuiIcon } from '@elastic/eui'; -import { DashboardPanelAction } from 'ui/dashboard_panel_actions'; +import { ContextMenuAction } from 'ui/embeddable'; import { Inspector } from 'ui/inspector'; /** @@ -29,7 +29,7 @@ import { Inspector } from 'ui/inspector'; * This will check if the embeddable inside the panel actually exposes inspector adapters * via its embeddable.getInspectorAdapters() method. If so - and if an inspector * could be shown for those adapters - the inspector icon will be visible. - * @return {DashboardPanelAction} + * @return {ContextMenuAction} */ export function getInspectorPanelAction({ closeContextMenu, @@ -38,7 +38,7 @@ export function getInspectorPanelAction({ closeContextMenu: () => void; panelTitle?: string; }) { - return new DashboardPanelAction( + return new ContextMenuAction( { id: 'openInspector', displayName: 'Inspect', diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_remove_panel_action.tsx b/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_remove_panel_action.tsx index 0f501b3205a6b..fce94f24b16ce 100644 --- a/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_remove_panel_action.tsx +++ b/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_remove_panel_action.tsx @@ -20,16 +20,16 @@ import { EuiIcon } from '@elastic/eui'; import React from 'react'; -import { DashboardPanelAction } from 'ui/dashboard_panel_actions'; +import { ContextMenuAction } from 'ui/embeddable'; import { DashboardViewMode } from '../../../dashboard_view_mode'; /** * * @param {function} onDeletePanel - * @return {DashboardPanelAction} + * @return {ContextMenuAction} */ export function getRemovePanelAction(onDeletePanel: () => void) { - return new DashboardPanelAction( + return new ContextMenuAction( { displayName: 'Delete from dashboard', id: 'deletePanel', diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_toggle_expand_panel_action.tsx b/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_toggle_expand_panel_action.tsx index 9da8cb11d71d6..27dca29c01ba6 100644 --- a/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_toggle_expand_panel_action.tsx +++ b/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_toggle_expand_panel_action.tsx @@ -20,13 +20,13 @@ import { EuiIcon } from '@elastic/eui'; import React from 'react'; -import { DashboardPanelAction } from 'ui/dashboard_panel_actions'; +import { ContextMenuAction } from 'ui/embeddable'; /** * Returns an action that toggles the panel into maximized or minimized state. * @param {boolean} isExpanded * @param {function} toggleExpandedPanel - * @return {DashboardPanelAction} + * @return {ContextMenuAction} */ export function getToggleExpandPanelAction({ isExpanded, @@ -35,7 +35,7 @@ export function getToggleExpandPanelAction({ isExpanded: boolean; toggleExpandedPanel: () => void; }) { - return new DashboardPanelAction( + return new ContextMenuAction( { displayName: isExpanded ? 'Minimize' : 'Full screen', id: 'togglePanel', diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/index.ts b/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/index.ts index ec369ffa4badc..b4cc5ea82e948 100644 --- a/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/index.ts +++ b/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/index.ts @@ -19,7 +19,6 @@ export { getEditPanelAction } from './get_edit_panel_action'; export { getRemovePanelAction } from './get_remove_panel_action'; -export { buildEuiContextMenuPanels } from './build_context_menu'; export { getCustomizePanelAction } from './get_customize_panel_action'; export { getToggleExpandPanelAction } from './get_toggle_expand_panel_action'; export { getInspectorPanelAction } from './get_inspector_panel_action'; diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_options_menu_container.ts b/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_options_menu_container.ts index d53f211a393c8..294a7dd2661ce 100644 --- a/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_options_menu_container.ts +++ b/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_options_menu_container.ts @@ -19,10 +19,14 @@ import { EuiContextMenuPanelDescriptor } from '@elastic/eui'; import { connect } from 'react-redux'; -import { ContainerState, Embeddable } from 'ui/embeddable'; -import { panelActionsStore } from '../../store/panel_actions_store'; import { buildEuiContextMenuPanels, + ContainerState, + ContextMenuPanel, + Embeddable, +} from 'ui/embeddable'; +import { panelActionsStore } from '../../store/panel_actions_store'; +import { getCustomizePanelAction, getEditPanelAction, getInspectorPanelAction, @@ -42,7 +46,6 @@ import { } from '../../actions'; import { Dispatch } from 'redux'; -import { DashboardContextMenuPanel } from 'ui/dashboard_panel_actions'; import { CoreKibanaState } from '../../../selectors'; import { DashboardViewMode } from '../../dashboard_view_mode'; import { @@ -163,7 +166,7 @@ const mergeProps = ( // Don't build the panels if the pop over is not open, or this gets expensive - this function is called once for // every panel, every time any state changes. if (isPopoverOpen) { - const contextMenuPanel = new DashboardContextMenuPanel({ + const contextMenuPanel = new ContextMenuPanel({ title: 'Options', id: 'mainMenu', }); diff --git a/src/core_plugins/kibana/public/dashboard/store/panel_actions_store.ts b/src/core_plugins/kibana/public/dashboard/store/panel_actions_store.ts index 59aca3583b0c2..449125d0ecfa4 100644 --- a/src/core_plugins/kibana/public/dashboard/store/panel_actions_store.ts +++ b/src/core_plugins/kibana/public/dashboard/store/panel_actions_store.ts @@ -17,16 +17,16 @@ * under the License. */ -import { DashboardPanelAction } from 'ui/dashboard_panel_actions'; +import { ContextMenuAction } from 'ui/embeddable'; class PanelActionsStore { - public actions: DashboardPanelAction[] = []; + public actions: ContextMenuAction[] = []; /** * * @type {IndexedArray} panelActionsRegistry */ - public initializeFromRegistry(panelActionsRegistry: DashboardPanelAction[]) { + public initializeFromRegistry(panelActionsRegistry: ContextMenuAction[]) { panelActionsRegistry.forEach(panelAction => { this.actions.push(panelAction); }); diff --git a/src/core_plugins/kibana/public/field_formats/__tests__/_source.js b/src/core_plugins/kibana/public/field_formats/__tests__/_source.js index 09a84ae775430..4726c3993771e 100644 --- a/src/core_plugins/kibana/public/field_formats/__tests__/_source.js +++ b/src/core_plugins/kibana/public/field_formats/__tests__/_source.js @@ -43,8 +43,9 @@ describe('_source formatting', function () { })); it('should use the text content type if a field is not passed', function () { - const hit = _.first(hits); - expect(convertHtml(hit._source)).to.be(`${JSON.stringify(hit._source)}`); + const hit = { 'foo': 'bar', 'number': 42, 'hello': '

World

', 'also': 'with "quotes" or \'single quotes\'' }; + // eslint-disable-next-line + expect(convertHtml(hit)).to.be('{"foo":"bar","number":42,"hello":"<h1>World</h1>","also":"with \\"quotes\\" or 'single quotes'"}'); }); it('uses the _source, field, and hit to create a
', function () { diff --git a/src/core_plugins/kibana/public/home/components/tutorial/__snapshots__/instruction_set.test.js.snap b/src/core_plugins/kibana/public/home/components/tutorial/__snapshots__/instruction_set.test.js.snap index 0f8dc642b9dcf..33da089084488 100644 --- a/src/core_plugins/kibana/public/home/components/tutorial/__snapshots__/instruction_set.test.js.snap +++ b/src/core_plugins/kibana/public/home/components/tutorial/__snapshots__/instruction_set.test.js.snap @@ -55,8 +55,6 @@ exports[`render 1`] = ` } paramValues={Object {}} replaceTemplateStrings={[Function]} - textPost={undefined} - textPre={undefined} />, "key": 0, "title": "step 1", @@ -70,8 +68,6 @@ exports[`render 1`] = ` } paramValues={Object {}} replaceTemplateStrings={[Function]} - textPost={undefined} - textPre={undefined} />, "key": 1, "title": "step 2", @@ -137,8 +133,6 @@ exports[`statusCheckState checking status 1`] = ` } paramValues={Object {}} replaceTemplateStrings={[Function]} - textPost={undefined} - textPre={undefined} />, "key": 0, "title": "step 1", @@ -152,14 +146,12 @@ exports[`statusCheckState checking status 1`] = ` } paramValues={Object {}} replaceTemplateStrings={[Function]} - textPost={undefined} - textPre={undefined} />, "key": 1, "title": "step 2", }, Object { - "children": + "children": - , + , "key": "checkStatusStep", "status": "incomplete", "title": "custom title", @@ -262,8 +254,6 @@ exports[`statusCheckState failed status check - error 1`] = ` } paramValues={Object {}} replaceTemplateStrings={[Function]} - textPost={undefined} - textPre={undefined} />, "key": 0, "title": "step 1", @@ -277,14 +267,12 @@ exports[`statusCheckState failed status check - error 1`] = ` } paramValues={Object {}} replaceTemplateStrings={[Function]} - textPost={undefined} - textPre={undefined} />, "key": 1, "title": "step 2", }, Object { - "children": + "children": - , + , "key": "checkStatusStep", "status": "danger", "title": "custom title", @@ -392,8 +380,6 @@ exports[`statusCheckState failed status check - no data 1`] = ` } paramValues={Object {}} replaceTemplateStrings={[Function]} - textPost={undefined} - textPre={undefined} />, "key": 0, "title": "step 1", @@ -407,14 +393,12 @@ exports[`statusCheckState failed status check - no data 1`] = ` } paramValues={Object {}} replaceTemplateStrings={[Function]} - textPost={undefined} - textPre={undefined} />, "key": 1, "title": "step 2", }, Object { - "children": + "children": - , + , "key": "checkStatusStep", "status": "warning", "title": "custom title", @@ -522,8 +506,6 @@ exports[`statusCheckState initial state - no check has been attempted 1`] = ` } paramValues={Object {}} replaceTemplateStrings={[Function]} - textPost={undefined} - textPre={undefined} />, "key": 0, "title": "step 1", @@ -537,14 +519,12 @@ exports[`statusCheckState initial state - no check has been attempted 1`] = ` } paramValues={Object {}} replaceTemplateStrings={[Function]} - textPost={undefined} - textPre={undefined} />, "key": 1, "title": "step 2", }, Object { - "children": + "children": - , + , "key": "checkStatusStep", "status": "incomplete", "title": "custom title", @@ -647,8 +627,6 @@ exports[`statusCheckState successful status check 1`] = ` } paramValues={Object {}} replaceTemplateStrings={[Function]} - textPost={undefined} - textPre={undefined} />, "key": 0, "title": "step 1", @@ -662,14 +640,12 @@ exports[`statusCheckState successful status check 1`] = ` } paramValues={Object {}} replaceTemplateStrings={[Function]} - textPost={undefined} - textPre={undefined} />, "key": 1, "title": "step 2", }, Object { - "children": + "children": - , + , "key": "checkStatusStep", "status": "complete", "title": "custom title", diff --git a/src/core_plugins/kibana/public/home/components/tutorial/__snapshots__/saved_objects_installer.test.js.snap b/src/core_plugins/kibana/public/home/components/tutorial/__snapshots__/saved_objects_installer.test.js.snap index 6a0f123c22907..f5ae01470c77e 100644 --- a/src/core_plugins/kibana/public/home/components/tutorial/__snapshots__/saved_objects_installer.test.js.snap +++ b/src/core_plugins/kibana/public/home/components/tutorial/__snapshots__/saved_objects_installer.test.js.snap @@ -130,6 +130,84 @@ exports[`bulkCreate should display error message when bulkCreate request fails 1 }, ], ], + "results": Array [ + Object { + "isThrow": false, + "value": "Load Kibana objects", + }, + Object { + "isThrow": false, + "value": "Imports index pattern, visualizations and pre-defined dashboards.", + }, + Object { + "isThrow": false, + "value": "Load Kibana objects", + }, + Object { + "isThrow": false, + "value": "Load Kibana objects", + }, + Object { + "isThrow": false, + "value": "Imports index pattern, visualizations and pre-defined dashboards.", + }, + Object { + "isThrow": false, + "value": "Load Kibana objects", + }, + Object { + "isThrow": false, + "value": "Imports index pattern, visualizations and pre-defined dashboards.", + }, + Object { + "isThrow": false, + "value": "Load Kibana objects", + }, + Object { + "isThrow": false, + "value": "{savedObjectsLength} saved objects successfully added", + }, + Object { + "isThrow": false, + "value": "Imports index pattern, visualizations and pre-defined dashboards.", + }, + Object { + "isThrow": false, + "value": "Load Kibana objects", + }, + Object { + "isThrow": false, + "value": "Load Kibana objects", + }, + Object { + "isThrow": false, + "value": "Imports index pattern, visualizations and pre-defined dashboards.", + }, + Object { + "isThrow": false, + "value": "Load Kibana objects", + }, + Object { + "isThrow": false, + "value": "Imports index pattern, visualizations and pre-defined dashboards.", + }, + Object { + "isThrow": false, + "value": "Load Kibana objects", + }, + Object { + "isThrow": false, + "value": "Request failed, Error: {message}", + }, + Object { + "isThrow": false, + "value": "Imports index pattern, visualizations and pre-defined dashboards.", + }, + Object { + "isThrow": false, + "value": "Load Kibana objects", + }, + ], }, "formatNumber": [MockFunction], "formatPlural": [MockFunction], @@ -155,7 +233,7 @@ exports[`bulkCreate should display error message when bulkCreate request fails 1 steps={ Array [ Object { - "children": + "children": - , + , "key": "installStep", "status": "incomplete", "title": "Load Kibana objects", @@ -439,6 +517,52 @@ exports[`bulkCreate should display success message when bulkCreate is successful }, ], ], + "results": Array [ + Object { + "isThrow": false, + "value": "Load Kibana objects", + }, + Object { + "isThrow": false, + "value": "Imports index pattern, visualizations and pre-defined dashboards.", + }, + Object { + "isThrow": false, + "value": "Load Kibana objects", + }, + Object { + "isThrow": false, + "value": "Load Kibana objects", + }, + Object { + "isThrow": false, + "value": "Imports index pattern, visualizations and pre-defined dashboards.", + }, + Object { + "isThrow": false, + "value": "Load Kibana objects", + }, + Object { + "isThrow": false, + "value": "Imports index pattern, visualizations and pre-defined dashboards.", + }, + Object { + "isThrow": false, + "value": "Load Kibana objects", + }, + Object { + "isThrow": false, + "value": "{savedObjectsLength} saved objects successfully added", + }, + Object { + "isThrow": false, + "value": "Imports index pattern, visualizations and pre-defined dashboards.", + }, + Object { + "isThrow": false, + "value": "Load Kibana objects", + }, + ], }, "formatNumber": [MockFunction], "formatPlural": [MockFunction], @@ -464,7 +588,7 @@ exports[`bulkCreate should display success message when bulkCreate is successful steps={ Array [ Object { - "children": + "children": - , + , "key": "installStep", "status": "complete", "title": "Load Kibana objects", @@ -716,7 +840,7 @@ exports[`renders 1`] = ` steps={ Array [ Object { - "children": + "children": - , + , "key": "installStep", "status": "incomplete", "title": "Load Kibana objects", diff --git a/src/core_plugins/kibana/public/kibana.js b/src/core_plugins/kibana/public/kibana.js index 522b6b7087025..5ebd66b5492bc 100644 --- a/src/core_plugins/kibana/public/kibana.js +++ b/src/core_plugins/kibana/public/kibana.js @@ -35,7 +35,7 @@ import 'uiExports/savedObjectTypes'; import 'uiExports/fieldFormats'; import 'uiExports/fieldFormatEditors'; import 'uiExports/navbarExtensions'; -import 'uiExports/dashboardPanelActions'; +import 'uiExports/contextMenuActions'; import 'uiExports/managementSections'; import 'uiExports/devTools'; import 'uiExports/docViews'; @@ -59,7 +59,6 @@ import 'ui/agg_types'; import 'ui/timepicker'; import { showAppRedirectNotification } from 'ui/notify'; import 'leaflet'; -import { KibanaRootController } from './kibana_root_controller'; routes.enable(); @@ -68,6 +67,4 @@ routes redirectTo: `/${chrome.getInjected('kbnDefaultAppId', 'discover')}` }); -chrome.setRootController('kibana', KibanaRootController); - uiModules.get('kibana').run(showAppRedirectNotification); diff --git a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/__tests__/render.test.js b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/__tests__/render.test.js index 16da9009f5d5f..9ef4a3e38ee9b 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/__tests__/render.test.js +++ b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/__tests__/render.test.js @@ -22,6 +22,12 @@ const unmountComponentAtNode = jest.fn(); jest.doMock('react-dom', () => ({ render, unmountComponentAtNode })); +// If we don't mock this, Jest fails with the error `TypeError: Cannot redefine property: prototype +// at Function.defineProperties`. +jest.mock('ui/index_patterns', () => ({ + INDEX_PATTERN_ILLEGAL_CHARACTERS: ['\\', '/', '?', '"', '<', '>', '|', ' '], +})); + jest.mock('ui/chrome', () => ({ getUiSettingsClient: () => ({ get: () => '', diff --git a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/components/step_index_pattern/__tests__/step_index_pattern.test.js b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/components/step_index_pattern/__tests__/step_index_pattern.test.js index 3b05ec4b71ec4..37df9762b54df 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/components/step_index_pattern/__tests__/step_index_pattern.test.js +++ b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/components/step_index_pattern/__tests__/step_index_pattern.test.js @@ -26,6 +26,12 @@ jest.mock('../../../lib/ensure_minimum_time', () => ({ ensureMinimumTime: async (promises) => Array.isArray(promises) ? await Promise.all(promises) : await promises })); +// If we don't mock this, Jest fails with the error `TypeError: Cannot redefine property: prototype +// at Function.defineProperties`. +jest.mock('ui/index_patterns', () => ({ + INDEX_PATTERN_ILLEGAL_CHARACTERS: ['\\', '/', '?', '"', '<', '>', '|', ' '], +})); + jest.mock('ui/chrome', () => ({ getUiSettingsClient: () => ({ get: () => '', diff --git a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.js b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.js index bf6468c3bf15b..133154de52619 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.js +++ b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.js @@ -19,10 +19,11 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { ILLEGAL_CHARACTERS, MAX_SEARCH_SIZE } from '../../constants'; +import { INDEX_PATTERN_ILLEGAL_CHARACTERS as ILLEGAL_CHARACTERS } from 'ui/index_patterns'; +import { MAX_SEARCH_SIZE } from '../../constants'; import { getIndices, - containsInvalidCharacters, + containsIllegalCharacters, getMatchedIndices, canAppendWildcard, ensureMinimumTime @@ -240,7 +241,7 @@ export class StepIndexPatternComponent extends Component { // This is an error scenario but do not report an error containsErrors = true; } - else if (!containsInvalidCharacters(query, ILLEGAL_CHARACTERS)) { + else if (containsIllegalCharacters(query, ILLEGAL_CHARACTERS)) { const errorMessage = intl.formatMessage( { id: 'kbn.management.createIndexPattern.step.invalidCharactersErrorMessage', diff --git a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/constants/index.js b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/constants/index.js index 79bdbaed0d732..86246903b4440 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/constants/index.js +++ b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/constants/index.js @@ -26,4 +26,3 @@ export const MAX_NUMBER_OF_MATCHING_INDICES = 100; export const MAX_SEARCH_SIZE = MAX_NUMBER_OF_MATCHING_INDICES + ESTIMATED_NUMBER_OF_SYSTEM_INDICES; export const PER_PAGE_INCREMENTS = [5, 10, 20, 50]; -export const ILLEGAL_CHARACTERS = ['\\', '/', '?', '"', '<', '>', '|', ' ']; diff --git a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/__tests__/contains_invalid_characters.test.js b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/__tests__/contains_invalid_characters.test.js index 2385f3baec6bc..05c4aba2571bd 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/__tests__/contains_invalid_characters.test.js +++ b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/__tests__/contains_invalid_characters.test.js @@ -17,16 +17,16 @@ * under the License. */ -import { containsInvalidCharacters } from '../contains_invalid_characters'; +import { containsIllegalCharacters } from '../contains_illegal_characters'; -describe('containsInvalidCharacters', () => { - it('should fail with illegal characters', () => { - const valid = containsInvalidCharacters('abc', ['a']); - expect(valid).toBeFalsy(); +describe('containsIllegalCharacters', () => { + it('returns true with illegal characters', () => { + const isInvalid = containsIllegalCharacters('abc', ['a']); + expect(isInvalid).toBe(true); }); - it('should pass with no illegal characters', () => { - const valid = containsInvalidCharacters('abc', ['%']); - expect(valid).toBeTruthy(); + it('returns false with no illegal characters', () => { + const isInvalid = containsIllegalCharacters('abc', ['%']); + expect(isInvalid).toBe(false); }); }); diff --git a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/__tests__/get_indices.test.js b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/__tests__/get_indices.test.js index 150753fd34e36..f5dcccf6da50b 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/__tests__/get_indices.test.js +++ b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/__tests__/get_indices.test.js @@ -80,7 +80,7 @@ describe('getIndices', () => { it('should throw exceptions', async () => { const es = { - search: () => { throw 'Fail'; } + search: () => { throw new Error('Fail'); } }; await expect(getIndices(es, 'kibana', 1)).rejects.toThrow(); diff --git a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/contains_invalid_characters.js b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/contains_illegal_characters.js similarity index 86% rename from src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/contains_invalid_characters.js rename to src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/contains_illegal_characters.js index 5dbe3d7111061..31485bb3daaa2 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/contains_invalid_characters.js +++ b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/contains_illegal_characters.js @@ -17,6 +17,6 @@ * under the License. */ -export function containsInvalidCharacters(pattern, illegalCharacters) { - return !illegalCharacters.some(char => pattern.includes(char)); +export function containsIllegalCharacters(pattern, illegalCharacters) { + return illegalCharacters.some(char => pattern.includes(char)); } diff --git a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/get_indices.js b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/get_indices.js index 9a2a712d82c13..7638daf1c5720 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/get_indices.js +++ b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/get_indices.js @@ -42,7 +42,7 @@ export async function getIndices(es, rawPattern, limit) { // We need to always provide a limit and not rely on the default if (!limit) { - throw '`getIndices()` was called without the required `limit` parameter.'; + throw new Error('`getIndices()` was called without the required `limit` parameter.'); } const params = { diff --git a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/index.js b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/index.js index 22efa498c84ab..0930eb82514e1 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/index.js +++ b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/index.js @@ -25,6 +25,6 @@ export { getIndices } from './get_indices'; export { getMatchedIndices } from './get_matched_indices'; -export { containsInvalidCharacters } from './contains_invalid_characters'; +export { containsIllegalCharacters } from './contains_illegal_characters'; export { extractTimeFields } from './extract_time_fields'; diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/source_filters_table/__tests__/__snapshots__/source_filters_table.test.js.snap b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/source_filters_table/__tests__/__snapshots__/source_filters_table.test.js.snap index 0206f5c9774df..3452fd16456e7 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/source_filters_table/__tests__/__snapshots__/source_filters_table.test.js.snap +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/source_filters_table/__tests__/__snapshots__/source_filters_table.test.js.snap @@ -18,6 +18,12 @@ exports[`SourceFiltersTable should add a filter 1`] = ` "calls": Array [ Array [], ], + "results": Array [ + Object { + "isThrow": false, + "value": undefined, + }, + ], }, "sourceFilters": Array [ Object { @@ -106,6 +112,12 @@ exports[`SourceFiltersTable should remove a filter 1`] = ` "calls": Array [ Array [], ], + "results": Array [ + Object { + "isThrow": false, + "value": undefined, + }, + ], }, "sourceFilters": Array [ Object { @@ -308,6 +320,12 @@ exports[`SourceFiltersTable should update a filter 1`] = ` "calls": Array [ Array [], ], + "results": Array [ + Object { + "isThrow": false, + "value": undefined, + }, + ], }, "sourceFilters": Array [ Object { diff --git a/src/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js b/src/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js index f99789b1932fe..16e856fb610cc 100644 --- a/src/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js +++ b/src/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js @@ -324,6 +324,7 @@ export class Flyout extends Component { const options = this.state.indexPatterns.map(indexPattern => ({ text: indexPattern.get('title'), value: indexPattern.id, + ['data-test-subj']: `indexPatternOption-${indexPattern.get('title')}`, })); options.unshift({ @@ -333,7 +334,7 @@ export class Flyout extends Component { return ( this.onIndexChanged(id, e)} options={options} /> diff --git a/src/core_plugins/kibana/public/management/sections/settings/__snapshots__/advanced_settings.test.js.snap b/src/core_plugins/kibana/public/management/sections/settings/__snapshots__/advanced_settings.test.js.snap index f219c199b1d87..85abe0571d116 100644 --- a/src/core_plugins/kibana/public/management/sections/settings/__snapshots__/advanced_settings.test.js.snap +++ b/src/core_plugins/kibana/public/management/sections/settings/__snapshots__/advanced_settings.test.js.snap @@ -302,6 +302,27 @@ exports[`AdvancedSettings should render normally 1`] = ` ], } } + showNoResultsMessage={true} + /> +
`; @@ -420,6 +441,45 @@ exports[`AdvancedSettings should render specific setting if given setting key 1` ], } } + showNoResultsMessage={true} + /> + `; diff --git a/src/core_plugins/kibana/public/management/sections/settings/advanced_settings.js b/src/core_plugins/kibana/public/management/sections/settings/advanced_settings.js index 2c67da99b6bf7..7ce4341f59ed8 100644 --- a/src/core_plugins/kibana/public/management/sections/settings/advanced_settings.js +++ b/src/core_plugins/kibana/public/management/sections/settings/advanced_settings.js @@ -35,7 +35,7 @@ import { Form } from './components/form'; import { getAriaName, toEditableConfig, DEFAULT_CATEGORY } from './lib'; import './advanced_settings.less'; -import { registerDefaultComponents, PAGE_TITLE_COMPONENT } from './components/default_component_registry'; +import { registerDefaultComponents, PAGE_TITLE_COMPONENT, PAGE_FOOTER_COMPONENT } from './components/default_component_registry'; import { getSettingsComponent } from './components/component_registry'; export class AdvancedSettings extends Component { @@ -51,6 +51,7 @@ export class AdvancedSettings extends Component { this.init(config); this.state = { query: parsedQuery, + footerQueryMatched: false, filteredSettings: this.mapSettings(Query.execute(parsedQuery, this.settings)), }; @@ -129,14 +130,22 @@ export class AdvancedSettings extends Component { clearQuery = () => { this.setState({ query: Query.parse(''), + footerQueryMatched: false, filteredSettings: this.groupedSettings, }); } + onFooterQueryMatchChange = (matched) => { + this.setState({ + footerQueryMatched: matched + }); + } + render() { - const { filteredSettings, query } = this.state; + const { filteredSettings, query, footerQueryMatched } = this.state; const PageTitle = getSettingsComponent(PAGE_TITLE_COMPONENT); + const PageFooter = getSettingsComponent(PAGE_FOOTER_COMPONENT); return (
@@ -162,7 +171,9 @@ export class AdvancedSettings extends Component { clearQuery={this.clearQuery} save={this.saveConfig} clear={this.clearConfig} + showNoResultsMessage={!footerQueryMatched} /> +
); } diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/default_component_registry.js b/src/core_plugins/kibana/public/management/sections/settings/components/default_component_registry.js index b88fb11d63bbc..221f8c2f82bf8 100644 --- a/src/core_plugins/kibana/public/management/sections/settings/components/default_component_registry.js +++ b/src/core_plugins/kibana/public/management/sections/settings/components/default_component_registry.js @@ -19,9 +19,12 @@ import { tryRegisterSettingsComponent } from './component_registry'; import { PageTitle } from './page_title'; +import { PageFooter } from './page_footer'; export const PAGE_TITLE_COMPONENT = 'advanced_settings_page_title'; +export const PAGE_FOOTER_COMPONENT = 'advanced_settings_page_footer'; export function registerDefaultComponents() { tryRegisterSettingsComponent(PAGE_TITLE_COMPONENT, PageTitle); + tryRegisterSettingsComponent(PAGE_FOOTER_COMPONENT, PageFooter); } \ No newline at end of file diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/field/__snapshots__/field.test.js.snap b/src/core_plugins/kibana/public/management/sections/settings/components/field/__snapshots__/field.test.js.snap index 881e228173b31..1c0861de51b3d 100644 --- a/src/core_plugins/kibana/public/management/sections/settings/components/field/__snapshots__/field.test.js.snap +++ b/src/core_plugins/kibana/public/management/sections/settings/components/field/__snapshots__/field.test.js.snap @@ -18,7 +18,7 @@ exports[`Field for array setting should render as read only with help text if ov +
- + @@ -34,15 +34,15 @@ exports[`Field for array setting should render as read only with help text if ov grow={true} size="xs" > - + Default: default_value - + - - + + } fullWidth={false} gutterSize="l" @@ -119,7 +119,7 @@ exports[`Field for array setting should render custom setting icon if it is cust +
- + } fullWidth={false} gutterSize="l" @@ -202,7 +202,7 @@ exports[`Field for array setting should render default value if there is no user +
- + } fullWidth={false} gutterSize="l" @@ -280,7 +280,7 @@ exports[`Field for array setting should render user value if there is user value +
- + @@ -296,15 +296,15 @@ exports[`Field for array setting should render user value if there is user value grow={true} size="xs" > - + Default: default_value - + - - + + } fullWidth={false} gutterSize="l" @@ -389,7 +389,7 @@ exports[`Field for boolean setting should render as read only with help text if +
- + @@ -405,15 +405,15 @@ exports[`Field for boolean setting should render as read only with help text if grow={true} size="xs" > - + Default: true - + - - + + } fullWidth={false} gutterSize="l" @@ -488,7 +488,7 @@ exports[`Field for boolean setting should render custom setting icon if it is cu +
- + } fullWidth={false} gutterSize="l" @@ -569,7 +569,7 @@ exports[`Field for boolean setting should render default value if there is no us +
- + } fullWidth={false} gutterSize="l" @@ -645,7 +645,7 @@ exports[`Field for boolean setting should render user value if there is user val +
- + @@ -661,15 +661,15 @@ exports[`Field for boolean setting should render user value if there is user val grow={true} size="xs" > - + Default: true - + - - + + } fullWidth={false} gutterSize="l" @@ -752,7 +752,7 @@ exports[`Field for image setting should render as read only with help text if ov +
- + @@ -768,15 +768,15 @@ exports[`Field for image setting should render as read only with help text if ov grow={true} size="xs" > - + Default: null - + - - + + } fullWidth={false} gutterSize="l" @@ -850,7 +850,7 @@ exports[`Field for image setting should render custom setting icon if it is cust +
- + } fullWidth={false} gutterSize="l" @@ -932,7 +932,7 @@ exports[`Field for image setting should render default value if there is no user +
- + } fullWidth={false} gutterSize="l" @@ -1009,7 +1009,7 @@ exports[`Field for image setting should render user value if there is user value +
- + @@ -1025,15 +1025,15 @@ exports[`Field for image setting should render user value if there is user value grow={true} size="xs" > - + Default: null - + - - + + } fullWidth={false} gutterSize="l" @@ -1126,7 +1126,7 @@ exports[`Field for json setting should render as read only with help text if ove +
- + @@ -1142,7 +1142,7 @@ exports[`Field for json setting should render as read only with help text if ove grow={true} size="xs" > - + Default: {} - + - - + + } fullWidth={false} gutterSize="l" @@ -1247,7 +1247,7 @@ exports[`Field for json setting should render custom setting icon if it is custo +
- + } fullWidth={false} gutterSize="l" @@ -1346,7 +1346,7 @@ exports[`Field for json setting should render default value if there is no user +
- + @@ -1362,7 +1362,7 @@ exports[`Field for json setting should render default value if there is no user grow={true} size="xs" > - + Default: {} - + - - + + } fullWidth={false} gutterSize="l" @@ -1475,7 +1475,7 @@ exports[`Field for json setting should render user value if there is user value +
- + @@ -1491,7 +1491,7 @@ exports[`Field for json setting should render user value if there is user value grow={true} size="xs" > - + Default: {} - + - - + + } fullWidth={false} gutterSize="l" @@ -1604,7 +1604,7 @@ exports[`Field for markdown setting should render as read only with help text if +
- + @@ -1620,15 +1620,15 @@ exports[`Field for markdown setting should render as read only with help text if grow={true} size="xs" > - + Default: null - + - - + + } fullWidth={false} gutterSize="l" @@ -1721,7 +1721,7 @@ exports[`Field for markdown setting should render custom setting icon if it is c +
- + } fullWidth={false} gutterSize="l" @@ -1820,7 +1820,7 @@ exports[`Field for markdown setting should render default value if there is no u +
- + } fullWidth={false} gutterSize="l" @@ -1914,7 +1914,7 @@ exports[`Field for markdown setting should render user value if there is user va +
- + @@ -1930,15 +1930,15 @@ exports[`Field for markdown setting should render user value if there is user va grow={true} size="xs" > - + Default: null - + - - + + } fullWidth={false} gutterSize="l" @@ -2039,7 +2039,7 @@ exports[`Field for number setting should render as read only with help text if o +
- + @@ -2055,15 +2055,15 @@ exports[`Field for number setting should render as read only with help text if o grow={true} size="xs" > - + Default: 5 - + - - + + } fullWidth={false} gutterSize="l" @@ -2140,7 +2140,7 @@ exports[`Field for number setting should render custom setting icon if it is cus +
- + } fullWidth={false} gutterSize="l" @@ -2223,7 +2223,7 @@ exports[`Field for number setting should render default value if there is no use +
- + } fullWidth={false} gutterSize="l" @@ -2301,7 +2301,7 @@ exports[`Field for number setting should render user value if there is user valu +
- + @@ -2317,15 +2317,15 @@ exports[`Field for number setting should render user value if there is user valu grow={true} size="xs" > - + Default: 5 - + - - + + } fullWidth={false} gutterSize="l" @@ -2410,7 +2410,7 @@ exports[`Field for select setting should render as read only with help text if o +
- + @@ -2426,15 +2426,15 @@ exports[`Field for select setting should render as read only with help text if o grow={true} size="xs" > - + Default: orange - + - - + + } fullWidth={false} gutterSize="l" @@ -2528,7 +2528,7 @@ exports[`Field for select setting should render custom setting icon if it is cus +
- + } fullWidth={false} gutterSize="l" @@ -2628,7 +2628,7 @@ exports[`Field for select setting should render default value if there is no use +
- + } fullWidth={false} gutterSize="l" @@ -2723,7 +2723,7 @@ exports[`Field for select setting should render user value if there is user valu +
- + @@ -2739,15 +2739,15 @@ exports[`Field for select setting should render user value if there is user valu grow={true} size="xs" > - + Default: orange - + - - + + } fullWidth={false} gutterSize="l" @@ -2849,7 +2849,7 @@ exports[`Field for string setting should render as read only with help text if o +
- + @@ -2865,15 +2865,15 @@ exports[`Field for string setting should render as read only with help text if o grow={true} size="xs" > - + Default: null - + - - + + } fullWidth={false} gutterSize="l" @@ -2950,7 +2950,7 @@ exports[`Field for string setting should render custom setting icon if it is cus +
- + } fullWidth={false} gutterSize="l" @@ -3033,7 +3033,7 @@ exports[`Field for string setting should render default value if there is no use +
- + } fullWidth={false} gutterSize="l" @@ -3111,7 +3111,7 @@ exports[`Field for string setting should render user value if there is user valu +
- + @@ -3127,15 +3127,15 @@ exports[`Field for string setting should render user value if there is user valu grow={true} size="xs" > - + Default: null - + - - + + } fullWidth={false} gutterSize="l" diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/field/field.js b/src/core_plugins/kibana/public/management/sections/settings/components/field/field.js index 68ff872e9ddfe..c21456953c17a 100644 --- a/src/core_plugins/kibana/public/management/sections/settings/components/field/field.js +++ b/src/core_plugins/kibana/public/management/sections/settings/components/field/field.js @@ -87,7 +87,7 @@ export class Field extends PureComponent { getEditableValue(type, value, defVal) { const val = (value === null || value === undefined) ? defVal : value; - switch(type) { + switch (type) { case 'array': return val.join(', '); case 'boolean': @@ -102,10 +102,10 @@ export class Field extends PureComponent { } getDisplayedDefaultValue(type, defVal) { - if(defVal === undefined || defVal === null || defVal === '') { + if (defVal === undefined || defVal === null || defVal === '') { return 'null'; } - switch(type) { + switch (type) { case 'array': return defVal.join(', '); default: @@ -193,7 +193,7 @@ export class Field extends PureComponent { } onImageChange = async (files) => { - if(!files.length) { + if (!files.length) { this.clearError(); this.setState({ unsavedValue: null, @@ -212,18 +212,18 @@ export class Field extends PureComponent { changeImage: true, unsavedValue: base64Image, }); - } catch(err) { + } catch (err) { toastNotifications.addDanger('Image could not be saved'); this.cancelChangeImage(); } } getImageAsBase64(file) { - if(!file instanceof File) { + if (!file instanceof File) { return null; } - const reader = new FileReader(); + const reader = new FileReader(); reader.readAsDataURL(file); return new Promise((resolve, reject) => { @@ -245,7 +245,7 @@ export class Field extends PureComponent { cancelChangeImage = () => { const { savedValue } = this.state; - if(this.changeImageForm) { + if (this.changeImageForm) { this.changeImageForm.fileInput.value = null; this.changeImageForm.handleChange(); } @@ -268,14 +268,14 @@ export class Field extends PureComponent { const { name, defVal, type } = this.props.setting; const { changeImage, savedValue, unsavedValue, isJsonArray } = this.state; - if(savedValue === unsavedValue) { + if (savedValue === unsavedValue) { return; } let valueToSave = unsavedValue; let isSameValue = false; - switch(type) { + switch (type) { case 'array': valueToSave = valueToSave.split(',').map(val => val.trim()); isSameValue = valueToSave.join(',') === defVal.join(','); @@ -295,10 +295,10 @@ export class Field extends PureComponent { await this.props.save(name, valueToSave); } - if(changeImage) { + if (changeImage) { this.cancelChangeImage(); } - } catch(e) { + } catch (e) { toastNotifications.addDanger(`Unable to save ${name}`); } this.setLoading(false); @@ -311,7 +311,7 @@ export class Field extends PureComponent { await this.props.clear(name); this.cancelChangeImage(); this.clearError(); - } catch(e) { + } catch (e) { toastNotifications.addDanger(`Unable to reset ${name}`); } this.setLoading(false); @@ -321,7 +321,7 @@ export class Field extends PureComponent { const { loading, changeImage, unsavedValue } = this.state; const { name, value, type, options, isOverridden } = setting; - switch(type) { + switch (type) { case 'boolean': return ( ); case 'image': - if(!isDefaultValue(setting) && !changeImage) { + if (!isDefaultValue(setting) && !changeImage) { return ( {setting.name} @@ -438,7 +438,7 @@ export class Field extends PureComponent { const defaultLink = this.renderResetToDefaultLink(setting); const imageLink = this.renderChangeImageLink(setting); - if(defaultLink || imageLink) { + if (defaultLink || imageLink) { return ( {defaultLink} @@ -462,8 +462,12 @@ export class Field extends PureComponent { } renderDescription(setting) { - return ( - + let description; + + if (React.isValidElement(setting.description)) { + description = setting.description; + } else { + description = (
+ ); + } + + return ( + + {description} {this.renderDefaultValue(setting)} ); @@ -478,14 +488,14 @@ export class Field extends PureComponent { renderDefaultValue(setting) { const { type, defVal } = setting; - if(isDefaultValue(setting)) { + if (isDefaultValue(setting)) { return; } return ( - { type === 'json' ? ( + {type === 'json' ? ( Default: ) : ( - Default: {this.getDisplayedDefaultValue(type, defVal)} + Default: {this.getDisplayedDefaultValue(type, defVal)} - ) } + )} ); @@ -508,7 +518,7 @@ export class Field extends PureComponent { renderResetToDefaultLink(setting) { const { ariaName, name } = setting; - if(isDefaultValue(setting)) { + if (isDefaultValue(setting)) { return; } return ( @@ -528,7 +538,7 @@ export class Field extends PureComponent { renderChangeImageLink(setting) { const { changeImage } = this.state; const { type, value, ariaName, name } = setting; - if(type !== 'image' || !value || changeImage) { + if (type !== 'image' || !value || changeImage) { return; } return ( diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/form/__snapshots__/form.test.js.snap b/src/core_plugins/kibana/public/management/sections/settings/components/form/__snapshots__/form.test.js.snap index 627f5d864d1d2..7d51699e975e7 100644 --- a/src/core_plugins/kibana/public/management/sections/settings/components/form/__snapshots__/form.test.js.snap +++ b/src/core_plugins/kibana/public/management/sections/settings/components/form/__snapshots__/form.test.js.snap @@ -1,5 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Form should not render no settings message when instructed not to 1`] = ``; + exports[`Form should render no settings message when there are no settings 1`] = ` @@ -95,12 +96,23 @@ export class Form extends PureComponent { ); } + maybeRenderNoSettings(clearQuery) { + if (this.props.showNoResultsMessage) { + return ( + + No settings found (Clear search) + + ); + } + return null; + } + render() { const { settings, categories, categoryCounts, clearQuery } = this.props; const currentCategories = []; categories.forEach(category => { - if(settings[category] && settings[category].length) { + if (settings[category] && settings[category].length) { currentCategories.push(category); } }); @@ -112,11 +124,7 @@ export class Form extends PureComponent { return ( this.renderCategory(category, settings[category], categoryCounts[category]) // fix this ); - }) : ( - - No settings found (Clear search) - - ) + }) : this.maybeRenderNoSettings(clearQuery) } ); diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/form/form.test.js b/src/core_plugins/kibana/public/management/sections/settings/components/form/form.test.js index cd3b3f2db5fb3..fddaae79ec44e 100644 --- a/src/core_plugins/kibana/public/management/sections/settings/components/form/form.test.js +++ b/src/core_plugins/kibana/public/management/sections/settings/components/form/form.test.js @@ -69,9 +69,9 @@ const categoryCounts = { dashboard: 1, 'x-pack': 10, }; -const save = () => {}; -const clear = () => {}; -const clearQuery = () => {}; +const save = () => { }; +const clear = () => { }; +const clearQuery = () => { }; describe('Form', () => { it('should render normally', async () => { @@ -83,6 +83,7 @@ describe('Form', () => { save={save} clear={clear} clearQuery={clearQuery} + showNoResultsMessage={true} /> ); @@ -98,6 +99,23 @@ describe('Form', () => { save={save} clear={clear} clearQuery={clearQuery} + showNoResultsMessage={true} + /> + ); + + expect(component).toMatchSnapshot(); + }); + + it('should not render no settings message when instructed not to', async () => { + const component = shallow( +
); diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/page_footer/__snapshots__/page_footer.test.js.snap b/src/core_plugins/kibana/public/management/sections/settings/components/page_footer/__snapshots__/page_footer.test.js.snap new file mode 100644 index 0000000000000..eea1003c8eb95 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/components/page_footer/__snapshots__/page_footer.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PageFooter should render normally 1`] = `""`; diff --git a/src/test_utils/base_auth.js b/src/core_plugins/kibana/public/management/sections/settings/components/page_footer/index.js similarity index 85% rename from src/test_utils/base_auth.js rename to src/core_plugins/kibana/public/management/sections/settings/components/page_footer/index.js index 270ed7563e7c1..2fae89ceb0380 100644 --- a/src/test_utils/base_auth.js +++ b/src/core_plugins/kibana/public/management/sections/settings/components/page_footer/index.js @@ -17,7 +17,4 @@ * under the License. */ -export function header(user, pass) { - const encoded = new Buffer(`${user}:${pass}`).toString('base64'); - return `Basic ${encoded}`; -} +export { PageFooter } from './page_footer'; \ No newline at end of file diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/page_footer/page_footer.js b/src/core_plugins/kibana/public/management/sections/settings/components/page_footer/page_footer.js new file mode 100644 index 0000000000000..e55fbbae3b5f8 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/components/page_footer/page_footer.js @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const PageFooter = () => null; \ No newline at end of file diff --git a/src/ui/public/dashboard_panel_actions/index.ts b/src/core_plugins/kibana/public/management/sections/settings/components/page_footer/page_footer.test.js similarity index 77% rename from src/ui/public/dashboard_panel_actions/index.ts rename to src/core_plugins/kibana/public/management/sections/settings/components/page_footer/page_footer.test.js index ec931eb48b5ae..e4ac6af0a88fe 100644 --- a/src/ui/public/dashboard_panel_actions/index.ts +++ b/src/core_plugins/kibana/public/management/sections/settings/components/page_footer/page_footer.test.js @@ -16,7 +16,13 @@ * specific language governing permissions and limitations * under the License. */ +import React from 'react'; +import { shallow } from 'enzyme'; -export { DashboardContextMenuPanel } from './dashboard_context_menu_panel'; -export { DashboardPanelAction } from './dashboard_panel_action'; -export { DashboardPanelActionsRegistryProvider } from './dashboard_panel_actions_registry'; +import { PageFooter } from './page_footer'; + +describe('PageFooter', () => { + it('should render normally', () => { + expect(shallow()).toMatchSnapshot(); + }); +}); \ No newline at end of file diff --git a/src/core_plugins/kibana/public/visualize/editor/styles/_editor.less b/src/core_plugins/kibana/public/visualize/editor/styles/_editor.less index 6991ae34d6cb1..2cfc573345885 100644 --- a/src/core_plugins/kibana/public/visualize/editor/styles/_editor.less +++ b/src/core_plugins/kibana/public/visualize/editor/styles/_editor.less @@ -124,7 +124,7 @@ /* Without setting this to 0 you will run into a bug where the filter bar modal is hidden under a tilemap in an iframe: https://github.com/elastic/kibana/issues/16457 */ - > visualize { + > .visualize { height: 100%; flex: 1 1 auto; display: flex; @@ -419,7 +419,7 @@ a tilemap in an iframe: https://github.com/elastic/kibana/issues/16457 */ flex-basis: 100%; } - visualize { + .visualize { .flex-parent(); flex: 1 1 100%; } diff --git a/src/core_plugins/kibana/server/lib/__tests__/manage_uuid.js b/src/core_plugins/kibana/server/lib/__tests__/manage_uuid.js index 54720cf98a827..2aa3953a78bd9 100644 --- a/src/core_plugins/kibana/server/lib/__tests__/manage_uuid.js +++ b/src/core_plugins/kibana/server/lib/__tests__/manage_uuid.js @@ -19,7 +19,7 @@ import expect from 'expect.js'; import sinon from 'sinon'; -import { startTestServers } from '../../../../../test_utils/kbn_server.js'; +import { startTestServers } from '../../../../../test_utils/kbn_server'; import manageUuid from '../manage_uuid'; describe('core_plugins/kibana/server/lib', function () { diff --git a/src/core_plugins/kibana/server/routes/api/export/index.js b/src/core_plugins/kibana/server/routes/api/export/index.js index 832369c1f0f46..34f45e9a6b2da 100644 --- a/src/core_plugins/kibana/server/routes/api/export/index.js +++ b/src/core_plugins/kibana/server/routes/api/export/index.js @@ -45,7 +45,7 @@ export function exportApi(server) { reply(json) .header('Content-Disposition', `attachment; filename="${filename}"`) .header('Content-Type', 'application/json') - .header('Content-Length', json.length); + .header('Content-Length', Buffer.byteLength(json, 'utf8')); }) .catch(err => reply(Boom.boomify(err, { statusCode: 400 }))); } diff --git a/src/core_plugins/kibana/server/routes/api/home/register_tutorials.js b/src/core_plugins/kibana/server/routes/api/home/register_tutorials.js index c7e0909631eae..89f1ff6213a14 100644 --- a/src/core_plugins/kibana/server/routes/api/home/register_tutorials.js +++ b/src/core_plugins/kibana/server/routes/api/home/register_tutorials.js @@ -23,7 +23,7 @@ export function registerTutorials(server) { path: '/api/kibana/home/tutorials', method: ['GET'], handler: async function (req, reply) { - reply(server.getTutorials()); + reply(server.getTutorials(req)); } }); } diff --git a/src/core_plugins/kibana/server/tutorials/apm/apm_client_instructions.js b/src/core_plugins/kibana/server/tutorials/apm/apm_client_instructions.js index 49c4b999af18e..138bebdc57998 100644 --- a/src/core_plugins/kibana/server/tutorials/apm/apm_client_instructions.js +++ b/src/core_plugins/kibana/server/tutorials/apm/apm_client_instructions.js @@ -17,122 +17,198 @@ * under the License. */ -/* eslint-disable max-len */ +import { i18n } from '@kbn/i18n'; export const createNodeClientInstructions = () => [ { - title: 'Install the APM agent', - textPre: 'Install the APM agent for Node.js as a dependency to your application.', + title: i18n.translate('kbn.server.tutorials.apm.nodeClient.install.title', { + defaultMessage: 'Install the APM agent', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.nodeClient.install.textPre', { + defaultMessage: 'Install the APM agent for Node.js as a dependency to your application.', + }), commands: ['npm install elastic-apm-node --save'], }, { - title: 'Configure the agent', - textPre: - 'Agents are libraries that run inside of your application process.' + - ' APM services are created programmatically based on the `serviceName`.' + - ' This agent supports Express, Koa, hapi, and custom Node.js.', - commands: `// Add this to the VERY top of the first file loaded in your app + title: i18n.translate('kbn.server.tutorials.apm.nodeClient.configure.title', { + defaultMessage: 'Configure the agent', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.nodeClient.configure.textPre', { + defaultMessage: 'Agents are libraries that run inside of your application process. \ +APM services are created programmatically based on the `serviceName`. \ +This agent supports a vararity of frameworks but can also be used with your custom stack.', + }), + commands: `// ${i18n.translate('kbn.server.tutorials.apm.nodeClient.configure.commands.addThisToTheFileTopComment', { + defaultMessage: 'Add this to the VERY top of the first file loaded in your app', + })} var apm = require('elastic-apm-node').start({curlyOpen} - // Set required service name (allowed characters: a-z, A-Z, 0-9, -, _, and space) + // ${i18n.translate('kbn.server.tutorials.apm.nodeClient.configure.commands.setRequiredServiceNameComment', { + defaultMessage: 'Override service name from package.json', + })} + // ${i18n.translate('kbn.server.tutorials.apm.nodeClient.configure.commands.allowedCharactersComment', { + defaultMessage: 'Allowed characters: a-z, A-Z, 0-9, -, _, and space', + })} serviceName: '', - // Use if APM Server requires a token + // ${i18n.translate('kbn.server.tutorials.apm.nodeClient.configure.commands.useIfApmRequiresTokenComment', { + defaultMessage: 'Use if APM Server requires a token', + })} secretToken: '', - // Set custom APM Server URL (default: http://localhost:8200) + // ${i18n.translate('kbn.server.tutorials.apm.nodeClient.configure.commands.setCustomApmServerUrlComment', { + defaultMessage: 'Set custom APM Server URL (default: {defaultApmServerUrl})', + values: { defaultApmServerUrl: 'http://localhost:8200' }, + })} serverUrl: '' {curlyClose})`.split('\n'), - textPost: `See [the documentation]({config.docs.base_url}guide/en/apm/agent/nodejs/1.x/index.html) for advanced usage, including how to use with [Babel/ES Modules]({config.docs.base_url}guide/en/apm/agent/nodejs/1.x/advanced-setup.html#es-modules).`, + textPost: i18n.translate('kbn.server.tutorials.apm.nodeClient.configure.textPost', { + defaultMessage: 'See [the documentation]({documentationLink}) for advanced usage, including how to use with \ +[Babel/ES Modules]({babelEsModulesLink}).', + values: { + documentationLink: '{config.docs.base_url}guide/en/apm/agent/nodejs/1.x/index.html', + babelEsModulesLink: '{config.docs.base_url}guide/en/apm/agent/nodejs/1.x/advanced-setup.html#es-modules', + }, + }), }, ]; export const createDjangoClientInstructions = () => [ { - title: 'Install the APM agent', - textPre: 'Install the APM agent for Python as a dependency.', + title: i18n.translate('kbn.server.tutorials.apm.djangoClient.install.title', { + defaultMessage: 'Install the APM agent', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.djangoClient.install.textPre', { + defaultMessage: 'Install the APM agent for Python as a dependency.', + }), commands: ['$ pip install elastic-apm'], }, { - title: 'Configure the agent', - textPre: - 'Agents are libraries that run inside of your application process.' + - ' APM services are created programmatically based on the `SERVICE_NAME`.', - commands: `# Add the agent to the installed apps + title: i18n.translate('kbn.server.tutorials.apm.djangoClient.configure.title', { + defaultMessage: 'Configure the agent', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.djangoClient.configure.textPre', { + defaultMessage: 'Agents are libraries that run inside of your application process. \ +APM services are created programmatically based on the `SERVICE_NAME`.', + }), + commands: `# ${i18n.translate('kbn.server.tutorials.apm.djangoClient.configure.commands.addAgentComment', { + defaultMessage: 'Add the agent to the installed apps', + })} INSTALLED_APPS = ( 'elasticapm.contrib.django', # ... ) ELASTIC_APM = {curlyOpen} - # Set required service name. Allowed characters: - # a-z, A-Z, 0-9, -, _, and space + # ${i18n.translate('kbn.server.tutorials.apm.djangoClient.configure.commands.setRequiredServiceNameComment', { + defaultMessage: 'Set required service name. Allowed characters:', + })} + # ${i18n.translate('kbn.server.tutorials.apm.djangoClient.configure.commands.allowedCharactersComment', { + defaultMessage: 'a-z, A-Z, 0-9, -, _, and space', + })} 'SERVICE_NAME': '', - # Use if APM Server requires a token + # ${i18n.translate('kbn.server.tutorials.apm.djangoClient.configure.commands.useIfApmServerRequiresTokenComment', { + defaultMessage: 'Use if APM Server requires a token', + })} 'SECRET_TOKEN': '', - # Set custom APM Server URL (default: http://localhost:8200) + # ${i18n.translate('kbn.server.tutorials.apm.djangoClient.configure.commands.setCustomApmServerUrlComment', { + defaultMessage: 'Set custom APM Server URL (default: {defaultApmServerUrl})', + values: { defaultApmServerUrl: 'http://localhost:8200' }, + })} 'SERVER_URL': '', {curlyClose} -# To send performance metrics, add our tracing middleware: +# ${i18n.translate('kbn.server.tutorials.apm.djangoClient.configure.commands.addTracingMiddlewareComment', { + defaultMessage: 'To send performance metrics, add our tracing middleware:', + })} MIDDLEWARE = ( 'elasticapm.contrib.django.middleware.TracingMiddleware', #... )`.split('\n'), - textPost: - 'See the [documentation]' + - '({config.docs.base_url}guide/en/apm/agent/python/2.x/django-support.html) for advanced usage.', + textPost: i18n.translate('kbn.server.tutorials.apm.djangoClient.configure.textPost', { + defaultMessage: 'See the [documentation]({documentationLink}) for advanced usage.', + values: { documentationLink: '{config.docs.base_url}guide/en/apm/agent/python/2.x/django-support.html' }, + }), }, ]; export const createFlaskClientInstructions = () => [ { - title: 'Install the APM agent', - textPre: 'Install the APM agent for Python as a dependency.', + title: i18n.translate('kbn.server.tutorials.apm.flaskClient.install.title', { + defaultMessage: 'Install the APM agent', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.flaskClient.install.textPre', { + defaultMessage: 'Install the APM agent for Python as a dependency.', + }), commands: ['$ pip install elastic-apm[flask]'], }, { - title: 'Configure the agent', - textPre: - 'Agents are libraries that run inside of your application process.' + - ' APM services are created programmatically based on the `SERVICE_NAME`.', - commands: `# initialize using environment variables + title: i18n.translate('kbn.server.tutorials.apm.flaskClient.configure.title', { + defaultMessage: 'Configure the agent', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.flaskClient.configure.textPre', { + defaultMessage: 'Agents are libraries that run inside of your application process. \ +APM services are created programmatically based on the `SERVICE_NAME`.', + }), + commands: `# ${i18n.translate('kbn.server.tutorials.apm.flaskClient.configure.commands.initializeUsingEnvironmentVariablesComment', { + defaultMessage: 'initialize using environment variables', + })} from elasticapm.contrib.flask import ElasticAPM app = Flask(__name__) apm = ElasticAPM(app) -# or configure to use ELASTIC_APM in your application's settings +# ${i18n.translate('kbn.server.tutorials.apm.flaskClient.configure.commands.configureElasticApmComment', { + defaultMessage: 'or configure to use ELASTIC_APM in your application\'s settings', + })} from elasticapm.contrib.flask import ElasticAPM app.config['ELASTIC_APM'] = {curlyOpen} - # Set required service name. Allowed characters: - # a-z, A-Z, 0-9, -, _, and space + # ${i18n.translate('kbn.server.tutorials.apm.flaskClient.configure.commands.setRequiredServiceNameComment', { + defaultMessage: 'Set required service name. Allowed characters:', + })} + # ${i18n.translate('kbn.server.tutorials.apm.flaskClient.configure.commands.allowedCharactersComment', { + defaultMessage: 'a-z, A-Z, 0-9, -, _, and space', + })} 'SERVICE_NAME': '', - # Use if APM Server requires a token + # ${i18n.translate('kbn.server.tutorials.apm.flaskClient.configure.commands.useIfApmServerRequiresTokenComment', { + defaultMessage: 'Use if APM Server requires a token', + })} 'SECRET_TOKEN': '', - # Set custom APM Server URL (default: http://localhost:8200) + # ${i18n.translate('kbn.server.tutorials.apm.flaskClient.configure.commands.setCustomApmServerUrlComment', { + defaultMessage: 'Set custom APM Server URL (default: {defaultApmServerUrl})', + values: { defaultApmServerUrl: 'http://localhost:8200' }, + })} 'SERVER_URL': '', {curlyClose} apm = ElasticAPM(app)`.split('\n'), - textPost: - 'See the [documentation]' + - '({config.docs.base_url}guide/en/apm/agent/python/2.x/flask-support.html) for advanced usage.', + textPost: i18n.translate('kbn.server.tutorials.apm.flaskClient.configure.textPost', { + defaultMessage: 'See the [documentation]({documentationLink}) for advanced usage.', + values: { documentationLink: '{config.docs.base_url}guide/en/apm/agent/python/2.x/flask-support.html' }, + }), }, ]; export const createRailsClientInstructions = () => [ { - title: 'Install the APM agent', - textPre: 'Add the agent to your Gemfile.', + title: i18n.translate('kbn.server.tutorials.apm.railsClient.install.title', { + defaultMessage: 'Install the APM agent', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.railsClient.install.textPre', { + defaultMessage: 'Add the agent to your Gemfile.', + }), commands: [`gem 'elastic-apm'`], }, { - title: 'Configure the agent', - textPre: - 'APM is automatically started when your app boots. Configure the agent, by creating the config file `config/elastic_apm.yml`', + title: i18n.translate('kbn.server.tutorials.apm.railsClient.configure.title', { + defaultMessage: 'Configure the agent', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.railsClient.configure.textPre', { + defaultMessage: 'APM is automatically started when your app boots. Configure the agent, by creating the config file {configFile}', + values: { configFile: '`config/elastic_apm.yml`' }, + }), commands: `# config/elastic_apm.yml: # Set service name - allowed characters: a-z, A-Z, 0-9, -, _ and space @@ -144,22 +220,30 @@ export const createRailsClientInstructions = () => [ # Set custom APM Server URL (default: http://localhost:8200) # server_url: 'http://localhost:8200'`.split('\n'), - textPost: - 'See the [documentation]' + - '({config.docs.base_url}guide/en/apm/agent/ruby/1.x/index.html) for configuration options and advanced usage.\n\n', + textPost: i18n.translate('kbn.server.tutorials.apm.railsClient.configure.textPost', { + defaultMessage: 'See the [documentation]({documentationLink}) for configuration options and advanced usage.\n\n', + values: { documentationLink: '{config.docs.base_url}guide/en/apm/agent/ruby/1.x/index.html' }, + }), }, ]; export const createRackClientInstructions = () => [ { - title: 'Install the APM agent', - textPre: 'Add the agent to your Gemfile.', + title: i18n.translate('kbn.server.tutorials.apm.rackClient.install.title', { + defaultMessage: 'Install the APM agent', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.rackClient.install.textPre', { + defaultMessage: 'Add the agent to your Gemfile.', + }), commands: [`gem 'elastic-apm'`], }, { - title: 'Configure the agent', - textPre: - 'For Rack or a compatible framework (e.g. Sinatra), include the middleware in your app and start the agent.', + title: i18n.translate('kbn.server.tutorials.apm.rackClient.configure.title', { + defaultMessage: 'Configure the agent', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.rackClient.configure.textPre', { + defaultMessage: 'For Rack or a compatible framework (e.g. Sinatra), include the middleware in your app and start the agent.', + }), commands: `# config.ru require 'sinatra/base' @@ -170,8 +254,12 @@ export const createRackClientInstructions = () => [ end ElasticAPM.start( - app: MySinatraApp, # required - config_file: '' # optional, defaults to config/elastic_apm.yml + app: MySinatraApp, # ${i18n.translate('kbn.server.tutorials.apm.rackClient.configure.commands.requiredComment', { + defaultMessage: 'required', + })} + config_file: '' # ${i18n.translate('kbn.server.tutorials.apm.rackClient.configure.commands.optionalComment', { + defaultMessage: 'optional, defaults to config/elastic_apm.yml', + })} ) run MySinatraApp @@ -179,90 +267,146 @@ export const createRackClientInstructions = () => [ at_exit {curlyOpen} ElasticAPM.stop {curlyClose}`.split('\n'), }, { - title: 'Create config file', - textPre: 'Create a config file `config/elastic_apm.yml`:', + title: i18n.translate('kbn.server.tutorials.apm.rackClient.createConfig.title', { + defaultMessage: 'Create config file', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.rackClient.createConfig.textPre', { + defaultMessage: 'Create a config file {configFile}:', + values: { configFile: '`config/elastic_apm.yml`' }, + }), commands: `# config/elastic_apm.yml: -# Set service name - allowed characters: a-z, A-Z, 0-9, -, _ and space -# Defaults to the name of your Rack app's class. +# ${i18n.translate('kbn.server.tutorials.apm.rackClient.createConfig.commands.setServiceNameComment', { + defaultMessage: 'Set service name - allowed characters: a-z, A-Z, 0-9, -, _ and space', + })} +# ${i18n.translate('kbn.server.tutorials.apm.rackClient.createConfig.commands.defaultsToTheNameOfRackAppClassComment', { + defaultMessage: 'Defaults to the name of your Rack app\'s class.', + })} # service_name: 'my-service' -# Use if APM Server requires a token +# ${i18n.translate('kbn.server.tutorials.apm.rackClient.createConfig.commands.useIfApmServerRequiresTokenComment', { + defaultMessage: 'Use if APM Server requires a token', + })} # secret_token: '' -# Set custom APM Server URL (default: http://localhost:8200) +# ${i18n.translate('kbn.server.tutorials.apm.rackClient.createConfig.commands.setCustomApmServerComment', { + defaultMessage: 'Set custom APM Server URL (default: {defaultServerUrl})', + values: { defaultServerUrl: 'http://localhost:8200' }, + })} # server_url: 'http://localhost:8200'`.split('\n'), - textPost: - 'See the [documentation]' + - '({config.docs.base_url}guide/en/apm/agent/ruby/1.x/index.html) for configuration options and advanced usage.\n\n', + textPost: i18n.translate('kbn.server.tutorials.apm.rackClient.createConfig.textPost', { + defaultMessage: 'See the [documentation]({documentationLink}) for configuration options and advanced usage.\n\n', + values: { documentationLink: '{config.docs.base_url}guide/en/apm/agent/ruby/1.x/index.html' }, + }), }, ]; export const createJsClientInstructions = () => [ { - title: 'Enable Real User Monitoring support in the APM server', - textPre: - 'Please refer to [the documentation]({config.docs.base_url}guide/en/apm/server/{config.docs.version}/rum.html).', + title: i18n.translate('kbn.server.tutorials.apm.jsClient.enableRealUserMonitoring.title', { + defaultMessage: 'Enable Real User Monitoring support in the APM server', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.jsClient.enableRealUserMonitoring.textPre', { + defaultMessage: 'Please refer to [the documentation]({documentationLink}).', + values: { documentationLink: '{config.docs.base_url}guide/en/apm/server/{config.docs.version}/rum.html' }, + }), }, { - title: 'Install the APM agent', - textPre: 'Install the APM agent for JavaScript as a dependency to your application:', + title: i18n.translate('kbn.server.tutorials.apm.jsClient.install.title', { + defaultMessage: 'Install the APM agent', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.jsClient.install.textPre', { + defaultMessage: 'Install the APM agent for JavaScript as a dependency to your application:', + }), commands: [`npm install elastic-apm-js-base --save`], }, { - title: 'Configure the agent', - textPre: 'Agents are libraries that run inside of your application.', + title: i18n.translate('kbn.server.tutorials.apm.jsClient.configure.title', { + defaultMessage: 'Configure the agent', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.jsClient.configure.textPre', { + defaultMessage: 'Agents are libraries that run inside of your application.', + }), commands: `import {curlyOpen} init as initApm {curlyClose} from 'elastic-apm-js-base' var apm = initApm({curlyOpen} - // Set required service name (allowed characters: a-z, A-Z, 0-9, -, _, and space) + // ${i18n.translate('kbn.server.tutorials.apm.jsClient.configure.commands.setRequiredServiceNameComment', { + defaultMessage: 'Set required service name (allowed characters: a-z, A-Z, 0-9, -, _, and space)', + })} serviceName: '', - // Set custom APM Server URL (default: http://localhost:8200) + // ${i18n.translate('kbn.server.tutorials.apm.jsClient.configure.commands.setCustomApmServerUrlComment', { + defaultMessage: 'Set custom APM Server URL (default: {defaultApmServerUrl})', + values: { defaultApmServerUrl: 'http://localhost:8200' }, + })} serverUrl: '', - // Set service version (required for sourcemap feature) + // ${i18n.translate('kbn.server.tutorials.apm.jsClient.configure.commands.setServiceVersionComment', { + defaultMessage: 'Set service version (required for sourcemap feature)', + })} serviceVersion: '' {curlyClose})`.split('\n'), - textPost: - 'See the [documentation]' + - '({config.docs.base_url}guide/en/apm/agent/js-base/current/index.html) for advanced usage.', + textPost: i18n.translate('kbn.server.tutorials.apm.jsClient.configure.textPost', { + defaultMessage: 'See the [documentation]({documentationLink}) for advanced usage.', + values: { documentationLink: '{config.docs.base_url}guide/en/apm/agent/js-base/current/index.html' }, + }), }, ]; export const createGoClientInstructions = () => [ { - title: 'Install the APM agent', - textPre: 'Install the APM agent packages for Go.', + title: i18n.translate('kbn.server.tutorials.apm.goClient.install.title', { + defaultMessage: 'Install the APM agent', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.goClient.install.textPre', { + defaultMessage: 'Install the APM agent packages for Go.', + }), commands: ['go get github.com/elastic/apm-agent-go'], }, { - title: 'Configure the agent', - textPre: - 'Agents are libraries that run inside of your application process.' + - ' APM services are created programmatically based on the executable ' + - ' file name, or the `ELASTIC_APM_SERVICE_NAME` environment variable.', - commands: `# Initialize using environment variables: - -# Set the service name. Allowed characters: # a-z, A-Z, 0-9, -, _, and space. -# If ELASTIC_APM_SERVICE_NAME is not specified, the executable name will be used. + title: i18n.translate('kbn.server.tutorials.apm.goClient.configure.title', { + defaultMessage: 'Configure the agent', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.goClient.configure.textPre', { + defaultMessage: 'Agents are libraries that run inside of your application process. \ +APM services are created programmatically based on the executable \ +file name, or the `ELASTIC_APM_SERVICE_NAME` environment variable.', + }), + commands: `# ${i18n.translate('kbn.server.tutorials.apm.goClient.configure.commands.initializeUsingEnvironmentVariablesComment', { + defaultMessage: 'Initialize using environment variables:', + })} + +# ${i18n.translate('kbn.server.tutorials.apm.goClient.configure.commands.setServiceNameComment', { + defaultMessage: 'Set the service name. Allowed characters: # a-z, A-Z, 0-9, -, _, and space.', + })} +# ${i18n.translate('kbn.server.tutorials.apm.goClient.configure.commands.usedExecutableNameComment', { + defaultMessage: 'If ELASTIC_APM_SERVICE_NAME is not specified, the executable name will be used.', + })} export ELASTIC_APM_SERVICE_NAME= -# Set the APM Server URL. If unspecified, the agent will effectively be disabled. +# ${i18n.translate('kbn.server.tutorials.apm.goClient.configure.commands.setAmpServerUrlComment', { + defaultMessage: 'Set the APM Server URL. If unspecified, the agent will effectively be disabled.', + })} export ELASTIC_APM_SERVER_URL= -# Set if APM Server requires a token. +# ${i18n.translate('kbn.server.tutorials.apm.goClient.configure.commands.setIfAmpServerRequiresTokenComment', { + defaultMessage: 'Set if APM Server requires a token.', + })} export ELASTIC_APM_SECRET_TOKEN= `.split('\n'), - textPost: - 'See the [documentation]' + - '({config.docs.base_url}guide/en/apm/agent/go/current/configuration.html) for advanced configuration.', + textPost: i18n.translate('kbn.server.tutorials.apm.goClient.configure.textPost', { + defaultMessage: 'See the [documentation]({documenationLink}) for advanced configuration.', + values: { documenationLink: '{config.docs.base_url}guide/en/apm/agent/go/current/configuration.html' }, + }), }, { - title: 'Instrument your application', - textPre: - 'Instrument your Go application by using one of the provided instrumentation modules or ' + - 'by using the tracer API directly.', + title: i18n.translate('kbn.server.tutorials.apm.goClient.instrument.title', { + defaultMessage: 'Instrument your application', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.goClient.instrument.textPre', { + defaultMessage: 'Instrument your Go application by using one of the provided instrumentation modules or \ +by using the tracer API directly.', + }), commands: ` import ( "net/http" @@ -276,37 +420,46 @@ func main() {curlyOpen} http.ListenAndServe(":8080", apmhttp.Wrap(mux)) {curlyClose} `.split('\n'), - textPost: - 'See the [documentation]' + - '({config.docs.base_url}guide/en/apm/agent/go/current/instrumenting-source.html) for a detailed ' + - 'guide to instrumenting Go source code.\n\n' + - '**Warning: The Go agent is currently in Beta and not meant for production use.**', + textPost: i18n.translate('kbn.server.tutorials.apm.goClient.instrument.textPost', { + defaultMessage: 'See the [documentation]({documentationLink}) for a detailed \ +guide to instrumenting Go source code.\n\n\ +**Warning: The Go agent is currently in Beta and not meant for production use.**', + values: { documentationLink: '{config.docs.base_url}guide/en/apm/agent/go/current/instrumenting-source.html' }, + }), }, ]; export const createJavaClientInstructions = () => [ { - title: 'Download the APM agent', - textPre: - 'Download the agent jar from [Maven Central](http://search.maven.org/#search%7Cga%7C1%7Ca%3Aelastic-apm-agent). ' + - 'Do **not** add the agent as a dependency to your application.', + title: i18n.translate('kbn.server.tutorials.apm.javaClient.download.title', { + defaultMessage: 'Download the APM agent', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.javaClient.download.textPre', { + defaultMessage: 'Download the agent jar from [Maven Central]({mavenCentralLink}). \ +Do **not** add the agent as a dependency to your application.', + values: { mavenCentralLink: 'http://search.maven.org/#search%7Cga%7C1%7Ca%3Aelastic-apm-agent' }, + }), }, { - title: 'Start your application with the javaagent flag', - textPre: - 'Add the `-javaagent` flag and configure the agent with system properties.\n' + - '\n' + - ' * Set required service name (allowed characters: a-z, A-Z, 0-9, -, _, and space)\n' + - ' * Set custom APM Server URL (default: http://localhost:8200)\n' + - ' * Set the base package of your application', + title: i18n.translate('kbn.server.tutorials.apm.javaClient.startApplication.title', { + defaultMessage: 'Start your application with the javaagent flag', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.javaClient.startApplication.textPre', { + defaultMessage: 'Add the `-javaagent` flag and configure the agent with system properties.\n\n \ +* Set required service name (allowed characters: a-z, A-Z, 0-9, -, _, and space)\n \ +* Set custom APM Server URL (default: {customApmServerUrl})\n \ +* Set the base package of your application', + values: { customApmServerUrl: 'http://localhost:8200' }, + }), commands: `java -javaagent:/path/to/elastic-apm-agent-.jar \\ -Delastic.apm.service_name=my-application \\ -Delastic.apm.server_url=http://localhost:8200 \\ -Delastic.apm.application_packages=org.example \\ -jar my-application.jar`.split('\n'), - textPost: - 'See the [documentation]' + - '({config.docs.base_url}guide/en/apm/agent/java/current/index.html) for configuration options and advanced usage.\n\n' + - '**Warning: The Java agent is currently in Beta and not meant for production use.**', + textPost: i18n.translate('kbn.server.tutorials.apm.javaClient.startApplication.textPost', { + defaultMessage: 'See the [documentation]({documenationLink}) for configuration options and advanced \ +usage.\n\n**Warning: The Java agent is currently in Beta and not meant for production use.**', + values: { documenationLink: '{config.docs.base_url}guide/en/apm/agent/java/current/index.html' }, + }), }, ]; diff --git a/src/core_plugins/kibana/server/tutorials/apm/apm_server_instructions.js b/src/core_plugins/kibana/server/tutorials/apm/apm_server_instructions.js index 59d4d5fe75c1b..2826aca5194db 100644 --- a/src/core_plugins/kibana/server/tutorials/apm/apm_server_instructions.js +++ b/src/core_plugins/kibana/server/tutorials/apm/apm_server_instructions.js @@ -17,11 +17,16 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; + export const createEditConfig = () => ({ - title: 'Edit the configuration', - textPre: - `If you're using an X-Pack secured version of Elastic Stack, you must specify` + - ' credentials in the `apm-server.yml` config file.', + title: i18n.translate('kbn.server.tutorials.apm.editConfig.title', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.editConfig.textPre', { + defaultMessage: 'If you\'re using an X-Pack secured version of Elastic Stack, you must specify \ +credentials in the `apm-server.yml` config file.', + }), commands: [ 'output.elasticsearch:', ' hosts: [""]', @@ -31,8 +36,12 @@ export const createEditConfig = () => ({ }); const createStartServer = () => ({ - title: 'Start APM Server', - textPre: 'The server processes and stores application performance metrics in Elasticsearch.', + title: i18n.translate('kbn.server.tutorials.apm.startServer.title', { + defaultMessage: 'Start APM Server', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.startServer.textPre', { + defaultMessage: 'The server processes and stores application performance metrics in Elasticsearch.', + }), }); export function createStartServerUnix() { @@ -45,7 +54,9 @@ export function createStartServerUnix() { }; } -const createDownloadServerTitle = () => 'Download and unpack APM Server'; +const createDownloadServerTitle = () => i18n.translate('kbn.server.tutorials.apm.downloadServer.title', { + defaultMessage: 'Download and unpack APM Server', +}); export const createDownloadServerOsx = () => ({ title: createDownloadServerTitle(), @@ -62,8 +73,10 @@ export const createDownloadServerDeb = () => ({ 'curl -L -O https://artifacts.elastic.co/downloads/apm-server/apm-server-{config.kibana.version}-amd64.deb', 'sudo dpkg -i apm-server-{config.kibana.version}-amd64.deb', ], - textPost: - 'Looking for the 32-bit packages? See the [Download page]({config.docs.base_url}downloads/apm/apm-server).', + textPost: i18n.translate('kbn.server.tutorials.apm.downloadServerTitle', { + defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({downloadPageLink}).', + values: { downloadPageLink: '{config.docs.base_url}downloads/apm/apm-server' }, + }), }); export const createDownloadServerRpm = () => ({ @@ -72,8 +85,10 @@ export const createDownloadServerRpm = () => ({ 'curl -L -O https://artifacts.elastic.co/downloads/apm-server/apm-server-{config.kibana.version}-x86_64.rpm', 'sudo rpm -vi apm-server-{config.kibana.version}-x86_64.rpm', ], - textPost: - 'Looking for the 32-bit packages? See the [Download page]({config.docs.base_url}downloads/apm/apm-server).', + textPost: i18n.translate('kbn.server.tutorials.apm.downloadServerRpm', { + defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({downloadPageLink}).', + values: { downloadPageLink: '{config.docs.base_url}downloads/apm/apm-server' }, + }), }); export function createWindowsServerInstructions() { @@ -82,20 +97,32 @@ export function createWindowsServerInstructions() { return [ { title: createDownloadServerTitle(), - textPre: - '1. Download the APM Server Windows zip file from the [Download page](https://www.elastic.co/downloads/apm/apm-server).\n' + - '2. Extract the contents of the zip file into `C:\\Program Files`.\n' + - '3. Rename the `apm-server-{config.kibana.version}-windows` directory to `APM-Server`.\n' + - '4. Open a PowerShell prompt as an Administrator (right-click the PowerShell icon and select' + - ' **Run As Administrator**). If you are running Windows XP, you might need to download and install PowerShell.\n' + - '5. From the PowerShell prompt, run the following commands to install APM Server as a Windows service:', + textPre: i18n.translate('kbn.server.tutorials.apm.windowsServerInstructions.textPre', { + defaultMessage: '1. Download the APM Server Windows zip file from the \ +[Download page]({downloadPageLink}).\n2. Extract the contents of \ +the zip file into {zipFileExtractFolder}.\n3. Rename the {apmServerDirectory} \ +directory to `APM-Server`.\n4. Open a PowerShell prompt as an Administrator \ +(right-click the PowerShell icon and select \ +**Run As Administrator**). If you are running Windows XP, you might need to download and install \ +PowerShell.\n5. From the PowerShell prompt, run the following commands to install APM Server as a Windows service:', + values: { + downloadPageLink: 'https://www.elastic.co/downloads/apm/apm-server', + zipFileExtractFolder: '`C:\\Program Files`', + apmServerDirectory: '`apm-server-{config.kibana.version}-windows`', + } + }), commands: [ `PS > cd 'C:\\Program Files\\APM-Server'`, `PS C:\\Program Files\\APM-Server> .\\install-service-apm-server.ps1`, ], - textPost: - 'Note: If script execution is disabled on your system, you need to set the execution policy for the current session' + - ' to allow the script to run. For example: `PowerShell.exe -ExecutionPolicy UnRestricted -File .\\install-service-apm-server.ps1`.', + textPost: i18n.translate('kbn.server.tutorials.apm.windowsServerInstructions.textPost', { + defaultMessage: 'Note: If script execution is disabled on your system, \ +you need to set the execution policy for the current session \ +to allow the script to run. For example: {command}.', + values: { + command: '`PowerShell.exe -ExecutionPolicy UnRestricted -File .\\install-service-apm-server.ps1`' + } + }), }, createEditConfig(), { diff --git a/src/core_plugins/kibana/server/tutorials/apm/elastic_cloud.js b/src/core_plugins/kibana/server/tutorials/apm/elastic_cloud.js index 4bce847b082e7..0a18d06d3fc81 100644 --- a/src/core_plugins/kibana/server/tutorials/apm/elastic_cloud.js +++ b/src/core_plugins/kibana/server/tutorials/apm/elastic_cloud.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; import { @@ -31,9 +32,13 @@ import { } from './apm_client_instructions'; const createServerUrlInstruction = () => ({ - title: 'APM Server endpoint', - textPre: `Retrieve the APM Server URL from the Deployments section on the Elastic Cloud dashboard. - You will also need the APM Server secret token, which was generated on deployment.`, + title: i18n.translate('kbn.server.tutorials.apm.serverUrlInstruction.title', { + defaultMessage: 'APM Server endpoint', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.serverUrlInstruction.textPre', { + defaultMessage: 'Retrieve the APM Server URL from the Deployments section on the Elastic Cloud dashboard. \ +You will also need the APM Server secret token, which was generated on deployment.', + }), }); export function createElasticCloudInstructions() { @@ -42,7 +47,9 @@ export function createElasticCloudInstructions() { return { instructionSets: [ { - title: 'APM Agents', + title: i18n.translate('kbn.server.tutorials.apm.elasticCloudInstructions.title', { + defaultMessage: 'APM Agents', + }), instructionVariants: [ { id: INSTRUCTION_VARIANT.NODE, diff --git a/src/core_plugins/kibana/server/tutorials/apm/index.js b/src/core_plugins/kibana/server/tutorials/apm/index.js index fb92e9acb9609..512805a4231dd 100644 --- a/src/core_plugins/kibana/server/tutorials/apm/index.js +++ b/src/core_plugins/kibana/server/tutorials/apm/index.js @@ -17,12 +17,15 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions } from './on_prem'; import { createElasticCloudInstructions } from './elastic_cloud'; import { getSavedObjects } from './saved_objects/get_saved_objects'; -const apmIntro = 'Collect in-depth performance metrics and errors from inside your applications.'; +const apmIntro = i18n.translate('kbn.server.tutorials.apm.introduction', { + defaultMessage: 'Collect in-depth performance metrics and errors from inside your applications.', +}); function isEnabled(config) { const ENABLED_KEY = 'xpack.apm.ui.enabled'; @@ -41,7 +44,9 @@ export function apmSpecProvider(server) { dashboards: [ { id: '8d3ed660-7828-11e7-8c47-65b845b5cfb3', - linkLabel: 'APM dashboard', + linkLabel: i18n.translate('kbn.server.tutorials.apm.specProvider.artifacts.dashboards.linkLabel', { + defaultMessage: 'APM dashboard', + }), isOverview: true, }, ], @@ -50,28 +55,35 @@ export function apmSpecProvider(server) { if (isEnabled(config)) { artifacts.application = { path: '/app/apm', - label: 'Launch APM', + label: i18n.translate('kbn.server.tutorials.apm.specProvider.artifacts.application.label', { + defaultMessage: 'Launch APM', + }), }; } return { id: 'apm', - name: 'APM', + name: i18n.translate('kbn.server.tutorials.apm.specProvider.name', { + defaultMessage: 'APM', + }), category: TUTORIAL_CATEGORY.OTHER, shortDescription: apmIntro, - longDescription: - 'Application Performance Monitoring (APM) collects in-depth' + - ' performance metrics and errors from inside your application.' + - ' It allows you to monitor the performance of thousands of applications in real time.' + - ' [Learn more]({config.docs.base_url}guide/en/apm/get-started/{config.docs.version}/index.html).', + longDescription: i18n.translate('kbn.server.tutorials.apm.specProvider.longDescription', { + defaultMessage: 'Application Performance Monitoring (APM) collects in-depth \ +performance metrics and errors from inside your application. \ +It allows you to monitor the performance of thousands of applications in real time. \ +[Learn more]({learnMoreLink}).', + values: { learnMoreLink: '{config.docs.base_url}guide/en/apm/get-started/{config.docs.version}/index.html' }, + }), euiIconType: 'apmApp', artifacts: artifacts, onPrem: onPremInstructions(apmIndexPattern), elasticCloud: createElasticCloudInstructions(), previewImagePath: '/plugins/kibana/home/tutorial_resources/apm/apm.png', savedObjects: getSavedObjects(apmIndexPattern), - savedObjectsInstallMsg: - 'Load index pattern, visualizations, and pre-defined dashboards.' + - ' An index pattern is required for some features in the APM UI.', + savedObjectsInstallMsg: i18n.translate('kbn.server.tutorials.apm.specProvider.savedObjectsInstallMsg', { + defaultMessage: 'Load index pattern, visualizations, and pre-defined dashboards. \ +An index pattern is required for some features in the APM UI.', + }), }; } diff --git a/src/core_plugins/kibana/server/tutorials/apm/on_prem.js b/src/core_plugins/kibana/server/tutorials/apm/on_prem.js index b1a3938592e73..a75ce27384b21 100644 --- a/src/core_plugins/kibana/server/tutorials/apm/on_prem.js +++ b/src/core_plugins/kibana/server/tutorials/apm/on_prem.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; import { createWindowsServerInstructions, @@ -44,7 +45,9 @@ export function onPremInstructions(apmIndexPattern) { return { instructionSets: [ { - title: 'APM Server', + title: i18n.translate('kbn.server.tutorials.apm.apmServer.title', { + defaultMessage: 'APM Server', + }), instructionVariants: [ { id: INSTRUCTION_VARIANT.OSX, @@ -64,11 +67,21 @@ export function onPremInstructions(apmIndexPattern) { }, ], statusCheck: { - title: 'APM Server status', - text: 'Make sure APM Server is running before you start implementing the APM agents.', - btnLabel: 'Check APM Server status', - success: 'You have correctly setup APM-Server', - error: 'APM-Server has still not connected to Elasticsearch', + title: i18n.translate('kbn.server.tutorials.apm.apmServer.statusCheck.title', { + defaultMessage: 'APM Server status', + }), + text: i18n.translate('kbn.server.tutorials.apm.apmServer.statusCheck.text', { + defaultMessage: 'Make sure APM Server is running before you start implementing the APM agents.', + }), + btnLabel: i18n.translate('kbn.server.tutorials.apm.apmServer.statusCheck.btnLabel', { + defaultMessage: 'Check APM Server status', + }), + success: i18n.translate('kbn.server.tutorials.apm.apmServer.statusCheck.successMessage', { + defaultMessage: 'You have correctly setup APM-Server', + }), + error: i18n.translate('kbn.server.tutorials.apm.apmServer.statusCheck.errorMessage', { + defaultMessage: 'APM-Server has still not connected to Elasticsearch', + }), esHitsCheck: { index: apmIndexPattern, query: { @@ -84,7 +97,9 @@ export function onPremInstructions(apmIndexPattern) { }, }, { - title: 'APM Agents', + title: i18n.translate('kbn.server.tutorials.apm.apmAgents.title', { + defaultMessage: 'APM Agents', + }), instructionVariants: [ { id: INSTRUCTION_VARIANT.NODE, @@ -120,11 +135,21 @@ export function onPremInstructions(apmIndexPattern) { }, ], statusCheck: { - title: 'Agent status', - text: 'Make sure your application is running and the agents are sending data.', - btnLabel: 'Check agent status', - success: 'Data successfully received from one or more agents', - error: `No data has been received from agents yet`, + title: i18n.translate('kbn.server.tutorials.apm.apmAgents.statusCheck.title', { + defaultMessage: 'Agent status', + }), + text: i18n.translate('kbn.server.tutorials.apm.apmAgents.statusCheck.text', { + defaultMessage: 'Make sure your application is running and the agents are sending data.', + }), + btnLabel: i18n.translate('kbn.server.tutorials.apm.apmAgents.statusCheck.btnLabel', { + defaultMessage: 'Check agent status', + }), + success: i18n.translate('kbn.server.tutorials.apm.apmAgents.statusCheck.successMessage', { + defaultMessage: 'Data successfully received from one or more agents', + }), + error: i18n.translate('kbn.server.tutorials.apm.apmAgents.statusCheck.errorMessage', { + defaultMessage: 'No data has been received from agents yet', + }), esHitsCheck: { index: apmIndexPattern, query: { diff --git a/src/core_plugins/metrics/public/visualizations/components/gauge_vis.js b/src/core_plugins/metrics/public/visualizations/components/gauge_vis.js index 7f129d3e061a1..bb0e205e87393 100644 --- a/src/core_plugins/metrics/public/visualizations/components/gauge_vis.js +++ b/src/core_plugins/metrics/public/visualizations/components/gauge_vis.js @@ -85,7 +85,8 @@ class GaugeVis extends Component { position: 'relative', display: 'flex', rowDirection: 'column', - flex: '1 0 auto' + flex: '1 0 auto', + overflow: 'hidden', // Fixes IE scrollbars issue }, svg: { position: 'absolute', diff --git a/src/core_plugins/region_map/public/__tests__/region_map_visualization.js b/src/core_plugins/region_map/public/__tests__/region_map_visualization.js index 7f3064b99380f..32b9c22839ff7 100644 --- a/src/core_plugins/region_map/public/__tests__/region_map_visualization.js +++ b/src/core_plugins/region_map/public/__tests__/region_map_visualization.js @@ -36,9 +36,9 @@ import afterdatachangeandresizePng from './afterdatachangeandresize.png'; import aftercolorchangePng from './aftercolorchange.png'; import changestartupPng from './changestartup.png'; -const manifestUrl = 'https://staging-dot-catalogue-dot-elastic-layer.appspot.com/v1/manifest'; -const tmsManifestUrl = `"https://tiles-maps-stage.elastic.co/v2/manifest`; -const vectorManifestUrl = `"https://staging-dot-elastic-layer.appspot.com/v1/manifest`; +const manifestUrl = 'https://catalogue-staging.maps.elastic.co/v2/manifest'; +const tmsManifestUrl = `https://tiles-maps-stage.elastic.co/v2/manifest`; +const vectorManifestUrl = `https://vector-staging.maps.elastic.co/v2/manifest`; const manifest = { 'services': [{ 'id': 'tiles_v2', @@ -189,7 +189,7 @@ describe('RegionMapsVisualizationTests', function () { 'attribution': '

Made with NaturalEarth | Elastic Maps Service

', 'name': 'World Countries', 'format': 'geojson', - 'url': 'https://staging-dot-elastic-layer.appspot.com/blob/5715999101812736?elastic_tile_service_tos=agree&my_app_version=7.0.0-alpha1', + 'url': 'https://vector-staging.maps.elastic.co/blob/5715999101812736?elastic_tile_service_tos=agree&my_app_version=7.0.0-alpha1', 'fields': [{ 'name': 'iso2', 'description': 'Two letter abbreviation' }, { 'name': 'iso3', 'description': 'Three letter abbreviation' diff --git a/src/core_plugins/tests_bundle/tests_entry_template.js b/src/core_plugins/tests_bundle/tests_entry_template.js index eb015454aaf27..799a25033bef2 100644 --- a/src/core_plugins/tests_bundle/tests_entry_template.js +++ b/src/core_plugins/tests_bundle/tests_entry_template.js @@ -60,7 +60,7 @@ const legacyMetadata = { }, mapConfig: { includeElasticMapsService: true, - manifestServiceUrl: 'https://staging-dot-catalogue-dot-elastic-layer.appspot.com/v1/manifest' + manifestServiceUrl: 'https://catalogue-staging.maps.elastic.co/v2/manifest' }, vegaConfig: { enabled: true, diff --git a/src/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js b/src/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js index b1cd3bfd37c36..426c2c37a08ed 100644 --- a/src/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js +++ b/src/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js @@ -50,7 +50,7 @@ function mockRawData() { mockRawData(); -const manifestUrl = 'https://staging-dot-catalogue-dot-elastic-layer.appspot.com/v1/manifest'; +const manifestUrl = 'https://catalogue-staging.maps.elastic.co/v2/manifest'; const tmsManifestUrl = `"https://tiles-maps-stage.elastic.co/v2/manifest`; const manifest = { 'services': [{ diff --git a/src/core_plugins/timelion/public/app.js b/src/core_plugins/timelion/public/app.js index ffadf7c9cdb1b..316a9704b5c3d 100644 --- a/src/core_plugins/timelion/public/app.js +++ b/src/core_plugins/timelion/public/app.js @@ -18,7 +18,6 @@ */ import _ from 'lodash'; -import moment from 'moment-timezone'; import { DocTitleProvider } from 'ui/doc_title'; import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; @@ -101,9 +100,6 @@ app.controller('timelion', function ( $scope.page = config.get('timelion:showTutorial', true) ? 1 : 0; $scope.setPage = (page) => $scope.page = page; - // TODO: For some reason the Kibana core doesn't correctly do this for all apps. - moment.tz.setDefault(config.get('dateFormat:tz')); - timefilter.enableAutoRefreshSelector(); timefilter.enableTimeRangeSelector(); diff --git a/src/dev/ci_setup/git_setup.sh b/src/dev/ci_setup/git_setup.sh index b5e6902e2f258..d0e2f3ffd87e4 100755 --- a/src/dev/ci_setup/git_setup.sh +++ b/src/dev/ci_setup/git_setup.sh @@ -70,6 +70,7 @@ function checkout_sibling { cloneBranch="$PR_TARGET_BRANCH" if clone_target_is_valid ; then + export TEST_ES_FROM=snapshot return 0 fi diff --git a/src/dev/i18n/__snapshots__/extract_code_messages.test.js.snap b/src/dev/i18n/__snapshots__/extract_code_messages.test.js.snap deleted file mode 100644 index 507efdfd61595..0000000000000 --- a/src/dev/i18n/__snapshots__/extract_code_messages.test.js.snap +++ /dev/null @@ -1,31 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`extractCodeMessages extracts React, server-side and angular service default messages 1`] = ` -Array [ - Array [ - "kbn.mgmt.id-1", - Object { - "context": undefined, - "message": "Message text 1", - }, - ], - Array [ - "kbn.mgmt.id-2", - Object { - "context": "Message context", - "message": "Message text 2", - }, - ], - Array [ - "kbn.mgmt.id-3", - Object { - "context": undefined, - "message": "Message text 3", - }, - ], -] -`; - -exports[`extractCodeMessages throws on empty id 1`] = `" I18N ERROR  Empty \\"id\\" value in i18n() or i18n.translate() is not allowed."`; - -exports[`extractCodeMessages throws on missing defaultMessage 1`] = `" I18N ERROR  Empty defaultMessage in intl.formatMessage() is not allowed (\\"message-id\\")."`; diff --git a/src/dev/i18n/__snapshots__/extract_default_translations.test.js.snap b/src/dev/i18n/__snapshots__/extract_default_translations.test.js.snap index 750280b7f8d95..1a9997dc07dd9 100644 --- a/src/dev/i18n/__snapshots__/extract_default_translations.test.js.snap +++ b/src/dev/i18n/__snapshots__/extract_default_translations.test.js.snap @@ -1,142 +1,63 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`dev/i18n/extract_default_translations extracts messages to en.json 1`] = ` -"{ - \\"formats\\": { - \\"number\\": { - \\"currency\\": { - \\"style\\": \\"currency\\" - }, - \\"percent\\": { - \\"style\\": \\"percent\\" - } +exports[`dev/i18n/extract_default_translations extracts messages from path to map 1`] = ` +Array [ + Array [ + "plugin_1.id_1", + Object { + "context": undefined, + "message": "Message 1", }, - \\"date\\": { - \\"short\\": { - \\"month\\": \\"numeric\\", - \\"day\\": \\"numeric\\", - \\"year\\": \\"2-digit\\" - }, - \\"medium\\": { - \\"month\\": \\"short\\", - \\"day\\": \\"numeric\\", - \\"year\\": \\"numeric\\" - }, - \\"long\\": { - \\"month\\": \\"long\\", - \\"day\\": \\"numeric\\", - \\"year\\": \\"numeric\\" - }, - \\"full\\": { - \\"weekday\\": \\"long\\", - \\"month\\": \\"long\\", - \\"day\\": \\"numeric\\", - \\"year\\": \\"numeric\\" - } + ], + Array [ + "plugin_1.id_2", + Object { + "context": "Message context", + "message": "Message 2", }, - \\"time\\": { - \\"short\\": { - \\"hour\\": \\"numeric\\", - \\"minute\\": \\"numeric\\" - }, - \\"medium\\": { - \\"hour\\": \\"numeric\\", - \\"minute\\": \\"numeric\\", - \\"second\\": \\"numeric\\" - }, - \\"long\\": { - \\"hour\\": \\"numeric\\", - \\"minute\\": \\"numeric\\", - \\"second\\": \\"numeric\\", - \\"timeZoneName\\": \\"short\\" - }, - \\"full\\": { - \\"hour\\": \\"numeric\\", - \\"minute\\": \\"numeric\\", - \\"second\\": \\"numeric\\", - \\"timeZoneName\\": \\"short\\" - } - } - }, - \\"plugin_1.id_1\\": \\"Message 1\\", - \\"plugin_1.id_2\\": { - \\"text\\": \\"Message 2\\", - \\"comment\\": \\"Message context\\" - }, - \\"plugin_1.id_3\\": \\"Message 3\\", - \\"plugin_1.id_4\\": \\"Message 4\\", - \\"plugin_1.id_5\\": \\"Message 5\\", - \\"plugin_1.id_6\\": \\"Message 6\\", - \\"plugin_1.id_7\\": \\"Message 7\\" -}" -`; - -exports[`dev/i18n/extract_default_translations injects default formats into en.json 1`] = ` -"{ - \\"formats\\": { - \\"number\\": { - \\"currency\\": { - \\"style\\": \\"currency\\" - }, - \\"percent\\": { - \\"style\\": \\"percent\\" - } + ], + Array [ + "plugin_1.id_3", + Object { + "context": undefined, + "message": "Message 3", + }, + ], + Array [ + "plugin_1.id_4", + Object { + "context": undefined, + "message": "Message 4", + }, + ], + Array [ + "plugin_1.id_5", + Object { + "context": undefined, + "message": "Message 5", + }, + ], + Array [ + "plugin_1.id_6", + Object { + "context": "", + "message": "Message 6", }, - \\"date\\": { - \\"short\\": { - \\"month\\": \\"numeric\\", - \\"day\\": \\"numeric\\", - \\"year\\": \\"2-digit\\" - }, - \\"medium\\": { - \\"month\\": \\"short\\", - \\"day\\": \\"numeric\\", - \\"year\\": \\"numeric\\" - }, - \\"long\\": { - \\"month\\": \\"long\\", - \\"day\\": \\"numeric\\", - \\"year\\": \\"numeric\\" - }, - \\"full\\": { - \\"weekday\\": \\"long\\", - \\"month\\": \\"long\\", - \\"day\\": \\"numeric\\", - \\"year\\": \\"numeric\\" - } + ], + Array [ + "plugin_1.id_7", + Object { + "context": undefined, + "message": "Message 7", }, - \\"time\\": { - \\"short\\": { - \\"hour\\": \\"numeric\\", - \\"minute\\": \\"numeric\\" - }, - \\"medium\\": { - \\"hour\\": \\"numeric\\", - \\"minute\\": \\"numeric\\", - \\"second\\": \\"numeric\\" - }, - \\"long\\": { - \\"hour\\": \\"numeric\\", - \\"minute\\": \\"numeric\\", - \\"second\\": \\"numeric\\", - \\"timeZoneName\\": \\"short\\" - }, - \\"full\\": { - \\"hour\\": \\"numeric\\", - \\"minute\\": \\"numeric\\", - \\"second\\": \\"numeric\\", - \\"timeZoneName\\": \\"short\\" - } - } - }, - \\"plugin_2.message-id\\": \\"Message text\\" -}" + ], +] `; exports[`dev/i18n/extract_default_translations throws on id collision 1`] = ` " I18N ERROR  Error in src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_3/test_file.jsx -Error:  I18N ERROR  There is more than one default message for the same id \\"plugin_3.duplicate_id\\": +Error: There is more than one default message for the same id \\"plugin_3.duplicate_id\\": \\"Message 1\\" and \\"Message 2\\"" `; -exports[`dev/i18n/extract_default_translations throws on wrong message namespace 1`] = `" I18N ERROR  Expected \\"wrong_plugin_namespace.message-id\\" id to have \\"plugin_2\\" namespace. See i18nrc.json for the list of supported namespaces."`; +exports[`dev/i18n/extract_default_translations throws on wrong message namespace 1`] = `"Expected \\"wrong_plugin_namespace.message-id\\" id to have \\"plugin_2\\" namespace. See .i18nrc.json for the list of supported namespaces."`; diff --git a/src/dev/i18n/__snapshots__/extract_handlebars_messages.test.js.snap b/src/dev/i18n/__snapshots__/extract_handlebars_messages.test.js.snap deleted file mode 100644 index 7c9f72a6921ba..0000000000000 --- a/src/dev/i18n/__snapshots__/extract_handlebars_messages.test.js.snap +++ /dev/null @@ -1,21 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`dev/i18n/extract_handlebars_messages extracts handlebars default messages 1`] = ` -Array [ - Array [ - "ui.id-1", - Object { - "context": "Message context", - "message": "Message text", - }, - ], -] -`; - -exports[`dev/i18n/extract_handlebars_messages throws on empty id 1`] = `" I18N ERROR  Empty id argument in Handlebars i18n is not allowed."`; - -exports[`dev/i18n/extract_handlebars_messages throws on missing defaultMessage property 1`] = `" I18N ERROR  Empty defaultMessage in Handlebars i18n is not allowed (\\"message-id\\")."`; - -exports[`dev/i18n/extract_handlebars_messages throws on wrong number of arguments 1`] = `" I18N ERROR  Wrong number of arguments for handlebars i18n call."`; - -exports[`dev/i18n/extract_handlebars_messages throws on wrong properties argument type 1`] = `" I18N ERROR  Properties string in Handlebars i18n should be a string literal (\\"ui.id-1\\")."`; diff --git a/src/dev/i18n/__snapshots__/extract_html_messages.test.js.snap b/src/dev/i18n/__snapshots__/extract_html_messages.test.js.snap deleted file mode 100644 index aa6048b92b84c..0000000000000 --- a/src/dev/i18n/__snapshots__/extract_html_messages.test.js.snap +++ /dev/null @@ -1,31 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`dev/i18n/extract_html_messages extracts default messages from HTML 1`] = ` -Array [ - Array [ - "kbn.dashboard.id-1", - Object { - "context": "Message context 1", - "message": "Message text 1", - }, - ], - Array [ - "kbn.dashboard.id-2", - Object { - "context": undefined, - "message": "Message text 2", - }, - ], - Array [ - "kbn.dashboard.id-3", - Object { - "context": "Message context 3", - "message": "Message text 3", - }, - ], -] -`; - -exports[`dev/i18n/extract_html_messages throws on empty i18n-id 1`] = `" I18N ERROR  Empty \\"i18n-id\\" value in angular directive is not allowed."`; - -exports[`dev/i18n/extract_html_messages throws on missing i18n-default-message attribute 1`] = `" I18N ERROR  Empty defaultMessage in angular directive is not allowed (\\"message-id\\")."`; diff --git a/src/dev/i18n/__snapshots__/extract_i18n_call_messages.test.js.snap b/src/dev/i18n/__snapshots__/extract_i18n_call_messages.test.js.snap deleted file mode 100644 index 13a79e578861c..0000000000000 --- a/src/dev/i18n/__snapshots__/extract_i18n_call_messages.test.js.snap +++ /dev/null @@ -1,29 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`extractI18nCallMessages extracts "i18n" and "i18n.translate" functions call message 1`] = ` -Array [ - "message-id-1", - Object { - "context": "Message context 1", - "message": "Default message 1", - }, -] -`; - -exports[`extractI18nCallMessages extracts "i18n" and "i18n.translate" functions call message 2`] = ` -Array [ - "message-id-2", - Object { - "context": "Message context 2", - "message": "Default message 2", - }, -] -`; - -exports[`extractI18nCallMessages throws if defaultMessage is not a string literal 1`] = `" I18N ERROR  defaultMessage value in i18n() or i18n.translate() should be a string literal (\\"message-id\\")."`; - -exports[`extractI18nCallMessages throws if message id value is not a string literal 1`] = `" I18N ERROR  Message id in i18n() or i18n.translate() should be a string literal."`; - -exports[`extractI18nCallMessages throws if properties object is not provided 1`] = `" I18N ERROR  Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`; - -exports[`extractI18nCallMessages throws on empty defaultMessage 1`] = `" I18N ERROR  Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`; diff --git a/src/dev/i18n/__snapshots__/extract_pug_messages.test.js.snap b/src/dev/i18n/__snapshots__/extract_pug_messages.test.js.snap deleted file mode 100644 index 16767f882063a..0000000000000 --- a/src/dev/i18n/__snapshots__/extract_pug_messages.test.js.snap +++ /dev/null @@ -1,15 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`extractPugMessages extracts messages from pug template 1`] = ` -Array [ - "message-id", - Object { - "context": "Message context", - "message": "Default message", - }, -] -`; - -exports[`extractPugMessages throws on empty id 1`] = `" I18N ERROR  Empty \\"id\\" value in i18n() or i18n.translate() is not allowed."`; - -exports[`extractPugMessages throws on missing default message 1`] = `" I18N ERROR  Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`; diff --git a/src/dev/i18n/__snapshots__/extract_react_messages.test.js.snap b/src/dev/i18n/__snapshots__/extract_react_messages.test.js.snap deleted file mode 100644 index 2bf17cab30c28..0000000000000 --- a/src/dev/i18n/__snapshots__/extract_react_messages.test.js.snap +++ /dev/null @@ -1,27 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`dev/i18n/extract_react_messages extractFormattedMessages extracts messages from "" element 1`] = ` -Array [ - "message-id-2", - Object { - "context": "Message context 2", - "message": "Default message 2", - }, -] -`; - -exports[`dev/i18n/extract_react_messages extractIntlMessages extracts messages from "intl.formatMessage" function call 1`] = ` -Array [ - "message-id-1", - Object { - "context": "Message context 1", - "message": "Default message 1", - }, -] -`; - -exports[`dev/i18n/extract_react_messages extractIntlMessages throws if context value is not a string literal 1`] = `" I18N ERROR  context value should be a string literal (\\"message-id\\")."`; - -exports[`dev/i18n/extract_react_messages extractIntlMessages throws if defaultMessage value is not a string literal 1`] = `" I18N ERROR  defaultMessage value should be a string literal (\\"message-id\\")."`; - -exports[`dev/i18n/extract_react_messages extractIntlMessages throws if message id is not a string literal 1`] = `" I18N ERROR  Message id should be a string literal."`; diff --git a/src/dev/i18n/__snapshots__/utils.test.js.snap b/src/dev/i18n/__snapshots__/utils.test.js.snap index 85e61058072b1..2a2f196d3f13f 100644 --- a/src/dev/i18n/__snapshots__/utils.test.js.snap +++ b/src/dev/i18n/__snapshots__/utils.test.js.snap @@ -1,3 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`i18n utils should escape linebreaks 1`] = `"Text with\\\\n\\\\n\\\\nline-breaks and \\\\n\\\\n\\\\n \\\\n\\\\n\\\\n "`; +exports[`i18n utils should not escape linebreaks 1`] = ` +"Text + with + line-breaks +" +`; diff --git a/src/dev/i18n/extract_default_translations.js b/src/dev/i18n/extract_default_translations.js index 5bbfaa221b433..06d397961e233 100644 --- a/src/dev/i18n/extract_default_translations.js +++ b/src/dev/i18n/extract_default_translations.js @@ -18,28 +18,27 @@ */ import path from 'path'; -import { i18n } from '@kbn/i18n'; -import JSON5 from 'json5'; import normalize from 'normalize-path'; import chalk from 'chalk'; -import { extractHtmlMessages } from './extract_html_messages'; -import { extractCodeMessages } from './extract_code_messages'; -import { extractPugMessages } from './extract_pug_messages'; -import { extractHandlebarsMessages } from './extract_handlebars_messages'; -import { globAsync, readFileAsync, writeFileAsync } from './utils'; +import { + extractHtmlMessages, + extractCodeMessages, + extractPugMessages, + extractHandlebarsMessages, +} from './extractors'; +import { globAsync, readFileAsync } from './utils'; import { paths, exclude } from '../../../.i18nrc.json'; -import { createFailError } from '../run'; - -const ESCAPE_SINGLE_QUOTE_REGEX = /\\([\s\S])|(')/g; +import { createFailError, isFailError } from '../run'; function addMessageToMap(targetMap, key, value) { const existingValue = targetMap.get(key); + if (targetMap.has(key) && existingValue.message !== value.message) { - throw createFailError(`${chalk.white.bgRed(' I18N ERROR ')} \ -There is more than one default message for the same id "${key}": + throw createFailError(`There is more than one default message for the same id "${key}": "${existingValue.message}" and "${value.message}"`); } + targetMap.set(key, value); } @@ -47,7 +46,7 @@ function normalizePath(inputPath) { return normalize(path.relative('.', inputPath)); } -function filterPaths(inputPaths) { +export function filterPaths(inputPaths) { const availablePaths = Object.values(paths); const pathsForExtraction = new Set(); @@ -79,9 +78,8 @@ export function validateMessageNamespace(id, filePath) { ); if (!id.startsWith(`${expectedNamespace}.`)) { - throw createFailError(`${chalk.white.bgRed(' I18N ERROR ')} \ -Expected "${id}" id to have "${expectedNamespace}" namespace. \ -See i18nrc.json for the list of supported namespaces.`); + throw createFailError(`Expected "${id}" id to have "${expectedNamespace}" namespace. \ +See .i18nrc.json for the list of supported namespaces.`); } } @@ -133,72 +131,15 @@ export async function extractMessagesFromPathToMap(inputPath, targetMap) { addMessageToMap(targetMap, id, value); } } catch (error) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} Error in ${normalizePath(name)}\n${error}` - ); + if (isFailError(error)) { + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} Error in ${normalizePath(name)}\n${error}` + ); + } + + throw error; } } }) ); } - -function serializeToJson5(defaultMessages) { - // .slice(0, -1): remove closing curly brace from json to append messages - let jsonBuffer = Buffer.from( - JSON5.stringify({ formats: i18n.formats }, { quote: `'`, space: 2 }).slice(0, -1) - ); - - for (const [mapKey, mapValue] of defaultMessages) { - const formattedMessage = mapValue.message.replace(ESCAPE_SINGLE_QUOTE_REGEX, '\\$1$2'); - const formattedContext = mapValue.context - ? mapValue.context.replace(ESCAPE_SINGLE_QUOTE_REGEX, '\\$1$2') - : ''; - - jsonBuffer = Buffer.concat([ - jsonBuffer, - Buffer.from(` '${mapKey}': '${formattedMessage}',`), - Buffer.from(formattedContext ? ` // ${formattedContext}\n` : '\n'), - ]); - } - - // append previously removed closing curly brace - jsonBuffer = Buffer.concat([jsonBuffer, Buffer.from('}\n')]); - - return jsonBuffer; -} - -function serializeToJson(defaultMessages) { - const resultJsonObject = { formats: i18n.formats }; - - for (const [mapKey, mapValue] of defaultMessages) { - if (mapValue.context) { - resultJsonObject[mapKey] = { text: mapValue.message, comment: mapValue.context }; - } else { - resultJsonObject[mapKey] = mapValue.message; - } - } - - return JSON.stringify(resultJsonObject, undefined, 2); -} - -export async function extractDefaultTranslations({ paths, output, outputFormat }) { - const defaultMessagesMap = new Map(); - - for (const inputPath of filterPaths(paths)) { - await extractMessagesFromPathToMap(inputPath, defaultMessagesMap); - } - - // messages shouldn't be extracted to a file if output is not supplied - if (!output || !defaultMessagesMap.size) { - return; - } - - const defaultMessages = [...defaultMessagesMap].sort(([key1], [key2]) => - key1.localeCompare(key2) - ); - - await writeFileAsync( - path.resolve(output, 'en.json'), - outputFormat === 'json5' ? serializeToJson5(defaultMessages) : serializeToJson(defaultMessages) - ); -} diff --git a/src/dev/i18n/extract_default_translations.test.js b/src/dev/i18n/extract_default_translations.test.js index 57e89f731fc6a..b89361e87fcf7 100644 --- a/src/dev/i18n/extract_default_translations.test.js +++ b/src/dev/i18n/extract_default_translations.test.js @@ -20,7 +20,7 @@ import path from 'path'; import { - extractDefaultTranslations, + extractMessagesFromPathToMap, validateMessageNamespace, } from './extract_default_translations'; @@ -40,42 +40,21 @@ jest.mock('../../../.i18nrc.json', () => ({ exclude: [], })); -const utils = require('./utils'); -utils.writeFileAsync = jest.fn(); - describe('dev/i18n/extract_default_translations', () => { - test('extracts messages to en.json', async () => { + test('extracts messages from path to map', async () => { const [pluginPath] = pluginsPaths; + const resultMap = new Map(); - utils.writeFileAsync.mockClear(); - await extractDefaultTranslations({ - paths: [pluginPath], - output: pluginPath, - }); - - const [[, json]] = utils.writeFileAsync.mock.calls; - - expect(json.toString()).toMatchSnapshot(); - }); - - test('injects default formats into en.json', async () => { - const [, pluginPath] = pluginsPaths; - - utils.writeFileAsync.mockClear(); - await extractDefaultTranslations({ - paths: [pluginPath], - output: pluginPath, - }); + await extractMessagesFromPathToMap(pluginPath, resultMap); - const [[, json]] = utils.writeFileAsync.mock.calls; - - expect(json.toString()).toMatchSnapshot(); + expect([...resultMap].sort()).toMatchSnapshot(); }); test('throws on id collision', async () => { const [, , pluginPath] = pluginsPaths; + await expect( - extractDefaultTranslations({ paths: [pluginPath], output: pluginPath }) + extractMessagesFromPathToMap(pluginPath, new Map()) ).rejects.toThrowErrorMatchingSnapshot(); }); diff --git a/src/dev/i18n/extractors/__snapshots__/code.test.js.snap b/src/dev/i18n/extractors/__snapshots__/code.test.js.snap new file mode 100644 index 0000000000000..26c621e32964d --- /dev/null +++ b/src/dev/i18n/extractors/__snapshots__/code.test.js.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dev/i18n/extractors/code extracts React, server-side and angular service default messages 1`] = ` +Array [ + Array [ + "kbn.mgmt.id-1", + Object { + "context": undefined, + "message": "Message text 1", + }, + ], + Array [ + "kbn.mgmt.id-2", + Object { + "context": "Message context", + "message": "Message text 2", + }, + ], + Array [ + "kbn.mgmt.id-3", + Object { + "context": undefined, + "message": "Message text 3", + }, + ], +] +`; + +exports[`dev/i18n/extractors/code throws on empty id 1`] = `"Empty \\"id\\" value in i18n() or i18n.translate() is not allowed."`; + +exports[`dev/i18n/extractors/code throws on missing defaultMessage 1`] = `"Empty defaultMessage in intl.formatMessage() is not allowed (\\"message-id\\")."`; diff --git a/src/dev/i18n/extractors/__snapshots__/handlebars.test.js.snap b/src/dev/i18n/extractors/__snapshots__/handlebars.test.js.snap new file mode 100644 index 0000000000000..7ca5178c7538f --- /dev/null +++ b/src/dev/i18n/extractors/__snapshots__/handlebars.test.js.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dev/i18n/extractors/handlebars extracts handlebars default messages 1`] = ` +Array [ + Array [ + "ui.id-1", + Object { + "context": "Message context", + "message": "Message text", + }, + ], +] +`; + +exports[`dev/i18n/extractors/handlebars throws on empty id 1`] = `"Empty id argument in Handlebars i18n is not allowed."`; + +exports[`dev/i18n/extractors/handlebars throws on missing defaultMessage property 1`] = `"Empty defaultMessage in Handlebars i18n is not allowed (\\"message-id\\")."`; + +exports[`dev/i18n/extractors/handlebars throws on wrong number of arguments 1`] = `"Wrong number of arguments for handlebars i18n call."`; + +exports[`dev/i18n/extractors/handlebars throws on wrong properties argument type 1`] = `"Properties string in Handlebars i18n should be a string literal (\\"ui.id-1\\")."`; diff --git a/src/dev/i18n/extractors/__snapshots__/html.test.js.snap b/src/dev/i18n/extractors/__snapshots__/html.test.js.snap new file mode 100644 index 0000000000000..982341c880074 --- /dev/null +++ b/src/dev/i18n/extractors/__snapshots__/html.test.js.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dev/i18n/extractors/html extracts default messages from HTML 1`] = ` +Array [ + Array [ + "kbn.dashboard.id-1", + Object { + "context": "Message context 1", + "message": "Message text 1", + }, + ], + Array [ + "kbn.dashboard.id-2", + Object { + "context": undefined, + "message": "Message text 2", + }, + ], + Array [ + "kbn.dashboard.id-3", + Object { + "context": "Message context 3", + "message": "Message text 3", + }, + ], +] +`; + +exports[`dev/i18n/extractors/html throws on empty i18n-id 1`] = `"Empty \\"i18n-id\\" value in angular directive is not allowed."`; + +exports[`dev/i18n/extractors/html throws on missing i18n-default-message attribute 1`] = `"Empty defaultMessage in angular directive is not allowed (\\"message-id\\")."`; diff --git a/src/dev/i18n/extractors/__snapshots__/i18n_call.test.js.snap b/src/dev/i18n/extractors/__snapshots__/i18n_call.test.js.snap new file mode 100644 index 0000000000000..c9bf2f07716d4 --- /dev/null +++ b/src/dev/i18n/extractors/__snapshots__/i18n_call.test.js.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dev/i18n/extractors/i18n_call extracts "i18n" and "i18n.translate" functions call message 1`] = ` +Array [ + "message-id-1", + Object { + "context": "Message context 1", + "message": "Default message 1", + }, +] +`; + +exports[`dev/i18n/extractors/i18n_call extracts "i18n" and "i18n.translate" functions call message 2`] = ` +Array [ + "message-id-2", + Object { + "context": "Message context 2", + "message": "Default message 2", + }, +] +`; + +exports[`dev/i18n/extractors/i18n_call throws if defaultMessage is not a string literal 1`] = `"defaultMessage value in i18n() or i18n.translate() should be a string literal (\\"message-id\\")."`; + +exports[`dev/i18n/extractors/i18n_call throws if message id value is not a string literal 1`] = `"Message id in i18n() or i18n.translate() should be a string literal."`; + +exports[`dev/i18n/extractors/i18n_call throws if properties object is not provided 1`] = `"Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`; + +exports[`dev/i18n/extractors/i18n_call throws on empty defaultMessage 1`] = `"Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`; diff --git a/src/dev/i18n/extractors/__snapshots__/pug.test.js.snap b/src/dev/i18n/extractors/__snapshots__/pug.test.js.snap new file mode 100644 index 0000000000000..c95fb0d149cd0 --- /dev/null +++ b/src/dev/i18n/extractors/__snapshots__/pug.test.js.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dev/i18n/extractors/pug extracts messages from pug template 1`] = ` +Array [ + "message-id", + Object { + "context": "Message context", + "message": "Default message", + }, +] +`; + +exports[`dev/i18n/extractors/pug throws on empty id 1`] = `"Empty \\"id\\" value in i18n() or i18n.translate() is not allowed."`; + +exports[`dev/i18n/extractors/pug throws on missing default message 1`] = `"Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`; diff --git a/src/dev/i18n/extractors/__snapshots__/react.test.js.snap b/src/dev/i18n/extractors/__snapshots__/react.test.js.snap new file mode 100644 index 0000000000000..6a51a5e216004 --- /dev/null +++ b/src/dev/i18n/extractors/__snapshots__/react.test.js.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dev/i18n/extractors/react extractFormattedMessages extracts messages from "" element 1`] = ` +Array [ + "message-id-2", + Object { + "context": "Message context 2", + "message": "Default message 2", + }, +] +`; + +exports[`dev/i18n/extractors/react extractIntlMessages extracts messages from "intl.formatMessage" function call 1`] = ` +Array [ + "message-id-1", + Object { + "context": "Message context 1", + "message": "Default message 1", + }, +] +`; + +exports[`dev/i18n/extractors/react extractIntlMessages throws if context value is not a string literal 1`] = `"context value should be a string literal (\\"message-id\\")."`; + +exports[`dev/i18n/extractors/react extractIntlMessages throws if defaultMessage value is not a string literal 1`] = `"defaultMessage value should be a string literal (\\"message-id\\")."`; + +exports[`dev/i18n/extractors/react extractIntlMessages throws if message id is not a string literal 1`] = `"Message id should be a string literal."`; diff --git a/src/dev/i18n/extract_code_messages.js b/src/dev/i18n/extractors/code.js similarity index 94% rename from src/dev/i18n/extract_code_messages.js rename to src/dev/i18n/extractors/code.js index e7b72e6efa162..e7477b17e2759 100644 --- a/src/dev/i18n/extract_code_messages.js +++ b/src/dev/i18n/extractors/code.js @@ -26,9 +26,9 @@ import { isMemberExpression, } from '@babel/types'; -import { extractI18nCallMessages } from './extract_i18n_call_messages'; -import { isI18nTranslateFunction, traverseNodes } from './utils'; -import { extractIntlMessages, extractFormattedMessages } from './extract_react_messages'; +import { extractI18nCallMessages } from './i18n_call'; +import { isI18nTranslateFunction, traverseNodes } from '../utils'; +import { extractIntlMessages, extractFormattedMessages } from './react'; /** * Detect Intl.formatMessage() function call (React). diff --git a/src/dev/i18n/extract_code_messages.test.js b/src/dev/i18n/extractors/code.test.js similarity index 89% rename from src/dev/i18n/extract_code_messages.test.js rename to src/dev/i18n/extractors/code.test.js index 5b3e64ebb4f07..3cc7d39f78d40 100644 --- a/src/dev/i18n/extract_code_messages.test.js +++ b/src/dev/i18n/extractors/code.test.js @@ -24,8 +24,8 @@ import { extractCodeMessages, isFormattedMessageElement, isIntlFormatMessageFunction, -} from './extract_code_messages'; -import { traverseNodes } from './utils'; +} from './code'; +import { traverseNodes } from '../utils'; const extractCodeMessagesSource = Buffer.from(` i18n('kbn.mgmt.id-1', { defaultMessage: 'Message text 1' }); @@ -65,7 +65,7 @@ function f() { } `; -describe('extractCodeMessages', () => { +describe('dev/i18n/extractors/code', () => { test('extracts React, server-side and angular service default messages', () => { const actual = Array.from(extractCodeMessages(extractCodeMessagesSource)); expect(actual.sort()).toMatchSnapshot(); @@ -84,12 +84,16 @@ describe('extractCodeMessages', () => { describe('isIntlFormatMessageFunction', () => { test('detects intl.formatMessage call expression', () => { - const callExpressionNodes = [...traverseNodes(parse(intlFormatMessageSource).program.body)].filter( - node => isCallExpression(node) - ); + const callExpressionNodes = [ + ...traverseNodes(parse(intlFormatMessageSource).program.body), + ].filter(node => isCallExpression(node)); expect(callExpressionNodes).toHaveLength(4); - expect(callExpressionNodes.every(callExpressionNode => isIntlFormatMessageFunction(callExpressionNode))).toBe(true); + expect( + callExpressionNodes.every(callExpressionNode => + isIntlFormatMessageFunction(callExpressionNode) + ) + ).toBe(true); }); }); diff --git a/src/dev/i18n/extract_handlebars_messages.js b/src/dev/i18n/extractors/handlebars.js similarity index 68% rename from src/dev/i18n/extract_handlebars_messages.js rename to src/dev/i18n/extractors/handlebars.js index 1aabdb61e2be1..7c57c8d0da731 100644 --- a/src/dev/i18n/extract_handlebars_messages.js +++ b/src/dev/i18n/extractors/handlebars.js @@ -17,10 +17,8 @@ * under the License. */ -import chalk from 'chalk'; - -import { formatJSString } from './utils'; -import { createFailError } from '../run'; +import { formatJSString } from '../utils'; +import { createFailError } from '../../run'; const HBS_REGEX = /(?<=\{\{)([\s\S]*?)(?=\}\})/g; const TOKENS_REGEX = /[^'\s]+|(?:'([^'\\]|\\[\s\S])*')/g; @@ -39,29 +37,22 @@ export function* extractHandlebarsMessages(buffer) { } if (tokens.length !== 3) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} Wrong number of arguments for handlebars i18n call.` - ); + throw createFailError(`Wrong number of arguments for handlebars i18n call.`); } if (!idString.startsWith(`'`) || !idString.endsWith(`'`)) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} Message id should be a string literal.` - ); + throw createFailError(`Message id should be a string literal.`); } const messageId = formatJSString(idString.slice(1, -1)); if (!messageId) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} Empty id argument in Handlebars i18n is not allowed.` - ); + throw createFailError(`Empty id argument in Handlebars i18n is not allowed.`); } if (!propertiesString.startsWith(`'`) || !propertiesString.endsWith(`'`)) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Properties string in Handlebars i18n should be a string literal ("${messageId}").` + `Properties string in Handlebars i18n should be a string literal ("${messageId}").` ); } @@ -70,15 +61,13 @@ Properties string in Handlebars i18n should be a string literal ("${messageId}") if (typeof message !== 'string') { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -defaultMessage value in Handlebars i18n should be a string ("${messageId}").` + `defaultMessage value in Handlebars i18n should be a string ("${messageId}").` ); } if (!message) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty defaultMessage in Handlebars i18n is not allowed ("${messageId}").` + `Empty defaultMessage in Handlebars i18n is not allowed ("${messageId}").` ); } @@ -86,8 +75,7 @@ Empty defaultMessage in Handlebars i18n is not allowed ("${messageId}").` if (context != null && typeof context !== 'string') { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Context value in Handlebars i18n should be a string ("${messageId}").` + `Context value in Handlebars i18n should be a string ("${messageId}").` ); } diff --git a/src/dev/i18n/extract_handlebars_messages.test.js b/src/dev/i18n/extractors/handlebars.test.js similarity index 95% rename from src/dev/i18n/extract_handlebars_messages.test.js rename to src/dev/i18n/extractors/handlebars.test.js index e4f53852b6cc3..52365989bd7fd 100644 --- a/src/dev/i18n/extract_handlebars_messages.test.js +++ b/src/dev/i18n/extractors/handlebars.test.js @@ -17,9 +17,9 @@ * under the License. */ -import { extractHandlebarsMessages } from './extract_handlebars_messages'; +import { extractHandlebarsMessages } from './handlebars'; -describe('dev/i18n/extract_handlebars_messages', () => { +describe('dev/i18n/extractors/handlebars', () => { test('extracts handlebars default messages', () => { const source = Buffer.from(`\ window.onload = function () { diff --git a/src/dev/i18n/extract_html_messages.js b/src/dev/i18n/extractors/html.js similarity index 78% rename from src/dev/i18n/extract_html_messages.js rename to src/dev/i18n/extractors/html.js index 4c8cad3ce1008..b576acb31c6d2 100644 --- a/src/dev/i18n/extract_html_messages.js +++ b/src/dev/i18n/extractors/html.js @@ -17,14 +17,13 @@ * under the License. */ -import chalk from 'chalk'; import { jsdom } from 'jsdom'; import { parse } from '@babel/parser'; import { isDirectiveLiteral, isObjectExpression, isStringLiteral } from '@babel/types'; -import { isPropertyWithKey, formatHTMLString, formatJSString, traverseNodes } from './utils'; -import { DEFAULT_MESSAGE_KEY, CONTEXT_KEY } from './constants'; -import { createFailError } from '../run'; +import { isPropertyWithKey, formatHTMLString, formatJSString, traverseNodes } from '../utils'; +import { DEFAULT_MESSAGE_KEY, CONTEXT_KEY } from '../constants'; +import { createFailError } from '../../run'; /** * Find all substrings of "{{ any text }}" pattern @@ -53,17 +52,13 @@ function parseFilterObjectExpression(expression) { for (const property of node.properties) { if (isPropertyWithKey(property, DEFAULT_MESSAGE_KEY)) { if (!isStringLiteral(property.value)) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} defaultMessage value should be a string literal.` - ); + throw createFailError(`defaultMessage value should be a string literal.`); } message = formatJSString(property.value.value); } else if (isPropertyWithKey(property, CONTEXT_KEY)) { if (!isStringLiteral(property.value)) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} context value should be a string literal.` - ); + throw createFailError(`context value should be a string literal.`); } context = formatJSString(property.value.value); @@ -101,27 +96,20 @@ function* getFilterMessages(htmlContent) { const filterObjectExpression = expression.slice(filterStart + I18N_FILTER_MARKER.length).trim(); if (!filterObjectExpression || !idExpression) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Cannot parse i18n filter expression: {{ ${expression} }}` - ); + throw createFailError(`Cannot parse i18n filter expression: {{ ${expression} }}`); } const messageId = parseIdExpression(idExpression); if (!messageId) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty "id" value in angular filter expression is not allowed.` - ); + throw createFailError(`Empty "id" value in angular filter expression is not allowed.`); } const { message, context } = parseFilterObjectExpression(filterObjectExpression) || {}; if (!message) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty defaultMessage in angular filter expression is not allowed ("${messageId}").` + `Empty defaultMessage in angular filter expression is not allowed ("${messageId}").` ); } @@ -137,17 +125,13 @@ function* getDirectiveMessages(htmlContent) { for (const element of document.querySelectorAll('[i18n-id]')) { const messageId = formatHTMLString(element.getAttribute('i18n-id')); if (!messageId) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty "i18n-id" value in angular directive is not allowed.` - ); + throw createFailError(`Empty "i18n-id" value in angular directive is not allowed.`); } const message = formatHTMLString(element.getAttribute('i18n-default-message')); if (!message) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty defaultMessage in angular directive is not allowed ("${messageId}").` + `Empty defaultMessage in angular directive is not allowed ("${messageId}").` ); } diff --git a/src/dev/i18n/extract_html_messages.test.js b/src/dev/i18n/extractors/html.test.js similarity index 94% rename from src/dev/i18n/extract_html_messages.test.js rename to src/dev/i18n/extractors/html.test.js index d5cf7d6fd5ee2..40664edd81e4a 100644 --- a/src/dev/i18n/extract_html_messages.test.js +++ b/src/dev/i18n/extractors/html.test.js @@ -17,7 +17,7 @@ * under the License. */ -import { extractHtmlMessages } from './extract_html_messages'; +import { extractHtmlMessages } from './html'; const htmlSourceBuffer = Buffer.from(`
@@ -37,7 +37,7 @@ const htmlSourceBuffer = Buffer.from(`
`); -describe('dev/i18n/extract_html_messages', () => { +describe('dev/i18n/extractors/html', () => { test('extracts default messages from HTML', () => { const actual = Array.from(extractHtmlMessages(htmlSourceBuffer)); expect(actual.sort()).toMatchSnapshot(); diff --git a/src/dev/i18n/extract_i18n_call_messages.js b/src/dev/i18n/extractors/i18n_call.js similarity index 64% rename from src/dev/i18n/extract_i18n_call_messages.js rename to src/dev/i18n/extractors/i18n_call.js index ba146c06621fe..1adcf42598e16 100644 --- a/src/dev/i18n/extract_i18n_call_messages.js +++ b/src/dev/i18n/extractors/i18n_call.js @@ -17,12 +17,11 @@ * under the License. */ -import chalk from 'chalk'; import { isObjectExpression, isStringLiteral } from '@babel/types'; -import { isPropertyWithKey, formatJSString } from './utils'; -import { DEFAULT_MESSAGE_KEY, CONTEXT_KEY } from './constants'; -import { createFailError } from '../run'; +import { isPropertyWithKey, formatJSString } from '../utils'; +import { DEFAULT_MESSAGE_KEY, CONTEXT_KEY } from '../constants'; +import { createFailError } from '../../run'; /** * Extract messages from `funcName('id', { defaultMessage: 'Message text' })` call expression AST @@ -31,19 +30,13 @@ export function extractI18nCallMessages(node) { const [idSubTree, optionsSubTree] = node.arguments; if (!isStringLiteral(idSubTree)) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Message id in i18n() or i18n.translate() should be a string literal.` - ); + throw createFailError(`Message id in i18n() or i18n.translate() should be a string literal.`); } const messageId = idSubTree.value; if (!messageId) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty "id" value in i18n() or i18n.translate() is not allowed.` - ); + throw createFailError(`Empty "id" value in i18n() or i18n.translate() is not allowed.`); } let message; @@ -51,8 +44,7 @@ Empty "id" value in i18n() or i18n.translate() is not allowed.` if (!isObjectExpression(optionsSubTree)) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty defaultMessage in i18n() or i18n.translate() is not allowed ("${messageId}").` + `Empty defaultMessage in i18n() or i18n.translate() is not allowed ("${messageId}").` ); } @@ -60,8 +52,7 @@ Empty defaultMessage in i18n() or i18n.translate() is not allowed ("${messageId} if (isPropertyWithKey(prop, DEFAULT_MESSAGE_KEY)) { if (!isStringLiteral(prop.value)) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -defaultMessage value in i18n() or i18n.translate() should be a string literal ("${messageId}").` + `defaultMessage value in i18n() or i18n.translate() should be a string literal ("${messageId}").` ); } @@ -69,8 +60,7 @@ defaultMessage value in i18n() or i18n.translate() should be a string literal (" } else if (isPropertyWithKey(prop, CONTEXT_KEY)) { if (!isStringLiteral(prop.value)) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -context value in i18n() or i18n.translate() should be a string literal ("${messageId}").` + `context value in i18n() or i18n.translate() should be a string literal ("${messageId}").` ); } @@ -80,8 +70,7 @@ context value in i18n() or i18n.translate() should be a string literal ("${messa if (!message) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty defaultMessage in i18n() or i18n.translate() is not allowed ("${messageId}").` + `Empty defaultMessage in i18n() or i18n.translate() is not allowed ("${messageId}").` ); } diff --git a/src/dev/i18n/extract_i18n_call_messages.test.js b/src/dev/i18n/extractors/i18n_call.test.js similarity index 95% rename from src/dev/i18n/extract_i18n_call_messages.test.js rename to src/dev/i18n/extractors/i18n_call.test.js index 0985233e4b3dd..f3ab92f4f1d6e 100644 --- a/src/dev/i18n/extract_i18n_call_messages.test.js +++ b/src/dev/i18n/extractors/i18n_call.test.js @@ -20,8 +20,8 @@ import { parse } from '@babel/parser'; import { isCallExpression } from '@babel/types'; -import { extractI18nCallMessages } from './extract_i18n_call_messages'; -import { traverseNodes } from './utils'; +import { extractI18nCallMessages } from './i18n_call'; +import { traverseNodes } from '../utils'; const i18nCallMessageSource = ` i18n('message-id-1', { defaultMessage: 'Default message 1', context: 'Message context 1' }); @@ -31,7 +31,7 @@ const translateCallMessageSource = ` i18n.translate('message-id-2', { defaultMessage: 'Default message 2', context: 'Message context 2' }); `; -describe('extractI18nCallMessages', () => { +describe('dev/i18n/extractors/i18n_call', () => { test('extracts "i18n" and "i18n.translate" functions call message', () => { let callExpressionNode = [...traverseNodes(parse(i18nCallMessageSource).program.body)].find( node => isCallExpression(node) diff --git a/src/dev/i18n/extractors/index.js b/src/dev/i18n/extractors/index.js new file mode 100644 index 0000000000000..7362eeb4e7003 --- /dev/null +++ b/src/dev/i18n/extractors/index.js @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { extractCodeMessages } from './code'; +export { extractHandlebarsMessages } from './handlebars'; +export { extractHtmlMessages } from './html'; +export { extractI18nCallMessages } from './i18n_call'; +export { extractPugMessages } from './pug'; +export { extractFormattedMessages, extractIntlMessages } from './react'; diff --git a/src/dev/i18n/extract_pug_messages.js b/src/dev/i18n/extractors/pug.js similarity index 91% rename from src/dev/i18n/extract_pug_messages.js rename to src/dev/i18n/extractors/pug.js index 8451c0b11db24..59851d19e88ab 100644 --- a/src/dev/i18n/extract_pug_messages.js +++ b/src/dev/i18n/extractors/pug.js @@ -19,8 +19,8 @@ import { parse } from '@babel/parser'; -import { extractI18nCallMessages } from './extract_i18n_call_messages'; -import { isI18nTranslateFunction, traverseNodes } from './utils'; +import { extractI18nCallMessages } from './i18n_call'; +import { isI18nTranslateFunction, traverseNodes } from '../utils'; /** * Matches `i18n(...)` in `#{i18n('id', { defaultMessage: 'Message text' })}` diff --git a/src/dev/i18n/extract_pug_messages.test.js b/src/dev/i18n/extractors/pug.test.js similarity index 94% rename from src/dev/i18n/extract_pug_messages.test.js rename to src/dev/i18n/extractors/pug.test.js index 0f72c13a6a339..7f901d1d992db 100644 --- a/src/dev/i18n/extract_pug_messages.test.js +++ b/src/dev/i18n/extractors/pug.test.js @@ -17,9 +17,9 @@ * under the License. */ -import { extractPugMessages } from './extract_pug_messages'; +import { extractPugMessages } from './pug'; -describe('extractPugMessages', () => { +describe('dev/i18n/extractors/pug', () => { test('extracts messages from pug template', () => { const source = Buffer.from(`\ #{i18n('message-id', { defaultMessage: 'Default message', context: 'Message context' })} diff --git a/src/dev/i18n/extract_react_messages.js b/src/dev/i18n/extractors/react.js similarity index 73% rename from src/dev/i18n/extract_react_messages.js rename to src/dev/i18n/extractors/react.js index 014f1214d0a18..074af4a76d5b4 100644 --- a/src/dev/i18n/extract_react_messages.js +++ b/src/dev/i18n/extractors/react.js @@ -18,17 +18,14 @@ */ import { isJSXIdentifier, isObjectExpression, isStringLiteral } from '@babel/types'; -import chalk from 'chalk'; -import { isPropertyWithKey, formatJSString, formatHTMLString } from './utils'; -import { DEFAULT_MESSAGE_KEY, CONTEXT_KEY } from './constants'; -import { createFailError } from '../run'; +import { isPropertyWithKey, formatJSString, formatHTMLString } from '../utils'; +import { DEFAULT_MESSAGE_KEY, CONTEXT_KEY } from '../constants'; +import { createFailError } from '../../run'; function extractMessageId(value) { if (!isStringLiteral(value)) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} Message id should be a string literal.` - ); + throw createFailError(`Message id should be a string literal.`); } return value.value; @@ -36,10 +33,7 @@ function extractMessageId(value) { function extractMessageValue(value, id) { if (!isStringLiteral(value)) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -defaultMessage value should be a string literal ("${id}").` - ); + throw createFailError(`defaultMessage value should be a string literal ("${id}").`); } return value.value; @@ -47,9 +41,7 @@ defaultMessage value should be a string literal ("${id}").` function extractContextValue(value, id) { if (!isStringLiteral(value)) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} context value should be a string literal ("${id}").` - ); + throw createFailError(`context value should be a string literal ("${id}").`); } return value.value; @@ -65,8 +57,7 @@ export function extractIntlMessages(node) { if (!isObjectExpression(options)) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Object with defaultMessage property is not passed to intl.formatMessage().` + `Object with defaultMessage property is not passed to intl.formatMessage().` ); } @@ -81,10 +72,7 @@ Object with defaultMessage property is not passed to intl.formatMessage().` : undefined; if (!messageId) { - createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty "id" value in intl.formatMessage() is not allowed.` - ); + createFailError(`Empty "id" value in intl.formatMessage() is not allowed.`); } const message = messageProperty @@ -93,8 +81,7 @@ Empty "id" value in intl.formatMessage() is not allowed.` if (!message) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty defaultMessage in intl.formatMessage() is not allowed ("${messageId}").` + `Empty defaultMessage in intl.formatMessage() is not allowed ("${messageId}").` ); } @@ -122,9 +109,7 @@ export function extractFormattedMessages(node) { : undefined; if (!messageId) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} Empty "id" value in is not allowed.` - ); + throw createFailError(`Empty "id" value in is not allowed.`); } const message = messageProperty @@ -133,8 +118,7 @@ export function extractFormattedMessages(node) { if (!message) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty default message in is not allowed ("${messageId}").` + `Empty default message in is not allowed ("${messageId}").` ); } diff --git a/src/dev/i18n/extract_react_messages.test.js b/src/dev/i18n/extractors/react.test.js similarity index 97% rename from src/dev/i18n/extract_react_messages.test.js rename to src/dev/i18n/extractors/react.test.js index 00233ac1abed2..91e65a0ecc20f 100644 --- a/src/dev/i18n/extract_react_messages.test.js +++ b/src/dev/i18n/extractors/react.test.js @@ -20,8 +20,8 @@ import { parse } from '@babel/parser'; import { isCallExpression, isJSXOpeningElement, isJSXIdentifier } from '@babel/types'; -import { extractIntlMessages, extractFormattedMessages } from './extract_react_messages'; -import { traverseNodes } from './utils'; +import { extractIntlMessages, extractFormattedMessages } from './react'; +import { traverseNodes } from '../utils'; const intlFormatMessageCallSource = ` const MyComponentContent = ({ intl }) => ( @@ -79,7 +79,7 @@ intl.formatMessage({ `, ]; -describe('dev/i18n/extract_react_messages', () => { +describe('dev/i18n/extractors/react', () => { describe('extractIntlMessages', () => { test('extracts messages from "intl.formatMessage" function call', () => { const ast = parse(intlFormatMessageCallSource, { plugins: ['jsx'] }); diff --git a/src/dev/i18n/index.js b/src/dev/i18n/index.js new file mode 100644 index 0000000000000..703e6ac682855 --- /dev/null +++ b/src/dev/i18n/index.js @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { filterPaths, extractMessagesFromPathToMap } from './extract_default_translations'; +export { writeFileAsync } from './utils'; +export { serializeToJson, serializeToJson5 } from './serializers'; diff --git a/src/dev/i18n/serializers/__snapshots__/json.test.js.snap b/src/dev/i18n/serializers/__snapshots__/json.test.js.snap new file mode 100644 index 0000000000000..c35e91e25cbb6 --- /dev/null +++ b/src/dev/i18n/serializers/__snapshots__/json.test.js.snap @@ -0,0 +1,67 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dev/i18n/serializers/json should serialize default messages to JSON 1`] = ` +"{ + \\"formats\\": { + \\"number\\": { + \\"currency\\": { + \\"style\\": \\"currency\\" + }, + \\"percent\\": { + \\"style\\": \\"percent\\" + } + }, + \\"date\\": { + \\"short\\": { + \\"month\\": \\"numeric\\", + \\"day\\": \\"numeric\\", + \\"year\\": \\"2-digit\\" + }, + \\"medium\\": { + \\"month\\": \\"short\\", + \\"day\\": \\"numeric\\", + \\"year\\": \\"numeric\\" + }, + \\"long\\": { + \\"month\\": \\"long\\", + \\"day\\": \\"numeric\\", + \\"year\\": \\"numeric\\" + }, + \\"full\\": { + \\"weekday\\": \\"long\\", + \\"month\\": \\"long\\", + \\"day\\": \\"numeric\\", + \\"year\\": \\"numeric\\" + } + }, + \\"time\\": { + \\"short\\": { + \\"hour\\": \\"numeric\\", + \\"minute\\": \\"numeric\\" + }, + \\"medium\\": { + \\"hour\\": \\"numeric\\", + \\"minute\\": \\"numeric\\", + \\"second\\": \\"numeric\\" + }, + \\"long\\": { + \\"hour\\": \\"numeric\\", + \\"minute\\": \\"numeric\\", + \\"second\\": \\"numeric\\", + \\"timeZoneName\\": \\"short\\" + }, + \\"full\\": { + \\"hour\\": \\"numeric\\", + \\"minute\\": \\"numeric\\", + \\"second\\": \\"numeric\\", + \\"timeZoneName\\": \\"short\\" + } + } + }, + \\"plugin1.message.id-1\\": \\"Message text 1 \\", + \\"plugin2.message.id-2\\": { + \\"text\\": \\"Message text 2\\", + \\"comment\\": \\"Message context\\" + } +}" +`; diff --git a/src/dev/i18n/serializers/__snapshots__/json5.test.js.snap b/src/dev/i18n/serializers/__snapshots__/json5.test.js.snap new file mode 100644 index 0000000000000..2166b32f28fd1 --- /dev/null +++ b/src/dev/i18n/serializers/__snapshots__/json5.test.js.snap @@ -0,0 +1,65 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dev/i18n/serializers/json5 should serialize default messages to JSON5 1`] = ` +"{ + formats: { + number: { + currency: { + style: 'currency', + }, + percent: { + style: 'percent', + }, + }, + date: { + short: { + month: 'numeric', + day: 'numeric', + year: '2-digit', + }, + medium: { + month: 'short', + day: 'numeric', + year: 'numeric', + }, + long: { + month: 'long', + day: 'numeric', + year: 'numeric', + }, + full: { + weekday: 'long', + month: 'long', + day: 'numeric', + year: 'numeric', + }, + }, + time: { + short: { + hour: 'numeric', + minute: 'numeric', + }, + medium: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + }, + long: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + }, + full: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + }, + }, + }, + 'plugin1.message.id-1': 'Message text 1', + 'plugin2.message.id-2': 'Message text 2', // Message context +} +" +`; diff --git a/src/dev/i18n/serializers/index.js b/src/dev/i18n/serializers/index.js new file mode 100644 index 0000000000000..3c10d7754563d --- /dev/null +++ b/src/dev/i18n/serializers/index.js @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { serializeToJson } from './json'; +export { serializeToJson5 } from './json5'; diff --git a/src/functional_test_runner/__tests__/lib/kibana.js b/src/dev/i18n/serializers/json.js similarity index 65% rename from src/functional_test_runner/__tests__/lib/kibana.js rename to src/dev/i18n/serializers/json.js index df046e34b2658..8e615af1e81d3 100644 --- a/src/functional_test_runner/__tests__/lib/kibana.js +++ b/src/dev/i18n/serializers/json.js @@ -17,20 +17,18 @@ * under the License. */ -import { createServerWithCorePlugins } from '../../../test_utils/kbn_server'; +import { i18n } from '@kbn/i18n'; -export async function startupKibana({ port, esUrl }) { - const server = createServerWithCorePlugins({ - server: { - port, - autoListen: true, - }, +export function serializeToJson(defaultMessages) { + const resultJsonObject = { formats: i18n.formats }; - elasticsearch: { - url: esUrl + for (const [mapKey, mapValue] of defaultMessages) { + if (mapValue.context) { + resultJsonObject[mapKey] = { text: mapValue.message, comment: mapValue.context }; + } else { + resultJsonObject[mapKey] = mapValue.message; } - }); + } - await server.ready(); - return server; + return JSON.stringify(resultJsonObject, undefined, 2); } diff --git a/src/core_plugins/kibana/public/kibana_root_controller.js b/src/dev/i18n/serializers/json.test.js similarity index 63% rename from src/core_plugins/kibana/public/kibana_root_controller.js rename to src/dev/i18n/serializers/json.test.js index 830cd0bd16c7b..9486a999fe7db 100644 --- a/src/core_plugins/kibana/public/kibana_root_controller.js +++ b/src/dev/i18n/serializers/json.test.js @@ -17,18 +17,21 @@ * under the License. */ -import moment from 'moment-timezone'; +import { serializeToJson } from './json'; -export function KibanaRootController($scope, courier, config) { - config.watch('dateFormat:tz', setDefaultTimezone, $scope); - config.watch('dateFormat:dow', setStartDayOfWeek, $scope); +describe('dev/i18n/serializers/json', () => { + test('should serialize default messages to JSON', () => { + const messages = new Map([ + ['plugin1.message.id-1', { message: 'Message text 1 ' }], + [ + 'plugin2.message.id-2', + { + message: 'Message text 2', + context: 'Message context', + }, + ], + ]); - function setDefaultTimezone(tz) { - moment.tz.setDefault(tz); - } - - function setStartDayOfWeek(day) { - const dow = moment.weekdays().indexOf(day); - moment.updateLocale(moment.locale(), { week: { dow } }); - } -} + expect(serializeToJson(messages)).toMatchSnapshot(); + }); +}); diff --git a/src/dev/i18n/serializers/json5.js b/src/dev/i18n/serializers/json5.js new file mode 100644 index 0000000000000..0156053d5f43b --- /dev/null +++ b/src/dev/i18n/serializers/json5.js @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import JSON5 from 'json5'; +import { i18n } from '@kbn/i18n'; + +const ESCAPE_SINGLE_QUOTE_REGEX = /\\([\s\S])|(')/g; + +export function serializeToJson5(defaultMessages) { + // .slice(0, -1): remove closing curly brace from json to append messages + let jsonBuffer = Buffer.from( + JSON5.stringify({ formats: i18n.formats }, { quote: `'`, space: 2 }).slice(0, -1) + ); + + for (const [mapKey, mapValue] of defaultMessages) { + const formattedMessage = mapValue.message.replace(ESCAPE_SINGLE_QUOTE_REGEX, '\\$1$2'); + const formattedContext = mapValue.context + ? mapValue.context.replace(ESCAPE_SINGLE_QUOTE_REGEX, '\\$1$2') + : ''; + + jsonBuffer = Buffer.concat([ + jsonBuffer, + Buffer.from(` '${mapKey}': '${formattedMessage}',`), + Buffer.from(formattedContext ? ` // ${formattedContext}\n` : '\n'), + ]); + } + + // append previously removed closing curly brace + jsonBuffer = Buffer.concat([jsonBuffer, Buffer.from('}\n')]); + + return jsonBuffer; +} diff --git a/src/dev/i18n/serializers/json5.test.js b/src/dev/i18n/serializers/json5.test.js new file mode 100644 index 0000000000000..90be880bd32a3 --- /dev/null +++ b/src/dev/i18n/serializers/json5.test.js @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { serializeToJson5 } from './json5'; + +describe('dev/i18n/serializers/json5', () => { + test('should serialize default messages to JSON5', () => { + const messages = new Map([ + [ + 'plugin1.message.id-1', + { + message: 'Message text 1', + }, + ], + [ + 'plugin2.message.id-2', + { + message: 'Message text 2', + context: 'Message context', + }, + ], + ]); + + expect(serializeToJson5(messages).toString()).toMatchSnapshot(); + }); +}); diff --git a/src/dev/i18n/utils.js b/src/dev/i18n/utils.js index af01c4721f14a..658c1cbe67177 100644 --- a/src/dev/i18n/utils.js +++ b/src/dev/i18n/utils.js @@ -30,7 +30,6 @@ import { promisify } from 'util'; const ESCAPE_LINE_BREAK_REGEX = /(? { test('should remove escaped linebreak', () => { expect(formatJSString('Test\\\n str\\\ning')).toEqual('Test string'); }); - - test('should escape linebreaks', () => { + test('should not escape linebreaks', () => { expect( - formatJSString(`Text with - - -line-breaks and \n\n - \n\n - `) + formatJSString(`Text \n with + line-breaks +`) ).toMatchSnapshot(); }); - test('should detect i18n translate function call', () => { let source = i18nTranslateSources[0]; let expressionStatementNode = [...traverseNodes(parse(source).program.body)].find(node => diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index 99b22b14c2d71..4bb11a6d43785 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -96,4 +96,6 @@ export default { 'default', '/src/dev/jest/junit_reporter.js', ], + // TODO: prevent tests from making web requests that rely on this setting, see https://github.com/facebook/jest/pull/6792 + testURL: 'about:blank', }; diff --git a/src/dev/jest/ts_transform.ts b/src/dev/jest/ts_transform.ts index 60f0b11ec94a4..ed366bcd091a0 100644 --- a/src/dev/jest/ts_transform.ts +++ b/src/dev/jest/ts_transform.ts @@ -17,8 +17,7 @@ * under the License. */ -import { getCacheKey, install, process } from 'ts-jest'; -import { JestConfig, TransformOptions } from 'ts-jest/dist/jest-types'; +import TsJest from 'ts-jest'; import { getTsProjectForAbsolutePath } from '../typescript'; @@ -26,11 +25,11 @@ const DEFAULT_TS_CONFIG_PATH = require.resolve('../../../tsconfig.json'); const DEFAULT_BROWSER_TS_CONFIG_PATH = require.resolve('../../../tsconfig.browser.json'); function extendJestConfigJSON(jestConfigJSON: string, filePath: string) { - const jestConfig = JSON.parse(jestConfigJSON) as JestConfig; + const jestConfig = JSON.parse(jestConfigJSON) as jest.ProjectConfig; return JSON.stringify(extendJestConfig(jestConfig, filePath)); } -function extendJestConfig(jestConfig: JestConfig, filePath: string) { +function extendJestConfig(jestConfig: jest.ProjectConfig, filePath: string) { let tsConfigFile = getTsProjectForAbsolutePath(filePath).tsConfigPath; // swap ts config file for jest tests @@ -51,25 +50,25 @@ function extendJestConfig(jestConfig: JestConfig, filePath: string) { } module.exports = { + canInstrument: true, + process( src: string, - filePath: string, - jestConfig: JestConfig, - transformOptions: TransformOptions + filePath: jest.Path, + jestConfig: jest.ProjectConfig, + transformOptions: jest.TransformOptions ) { const extendedConfig = extendJestConfig(jestConfig, filePath); - return process(src, filePath, extendedConfig, transformOptions); + return TsJest.process(src, filePath, extendedConfig, transformOptions); }, getCacheKey( src: string, filePath: string, jestConfigJSON: string, - transformOptions: TransformOptions + transformOptions: jest.TransformOptions ) { const extendedConfigJSON = extendJestConfigJSON(jestConfigJSON, filePath); - return getCacheKey(src, filePath, extendedConfigJSON, transformOptions); + return TsJest.getCacheKey!(src, filePath, extendedConfigJSON, transformOptions); }, - - install, }; diff --git a/src/dev/run/index.js b/src/dev/run/index.js index 1eef88d60b0a5..b176ac365fcf4 100644 --- a/src/dev/run/index.js +++ b/src/dev/run/index.js @@ -18,4 +18,4 @@ */ export { run } from './run'; -export { createFailError, combineErrors } from './fail'; +export { createFailError, combineErrors, isFailError } from './fail'; diff --git a/src/dev/run_i18n_check.js b/src/dev/run_i18n_check.js index 9d5f0011d6f38..02d70622b54cd 100644 --- a/src/dev/run_i18n_check.js +++ b/src/dev/run_i18n_check.js @@ -17,13 +17,46 @@ * under the License. */ -import { run } from './run'; -import { extractDefaultTranslations } from './i18n/extract_default_translations'; +import chalk from 'chalk'; +import Listr from 'listr'; +import { resolve } from 'path'; + +import { run, createFailError } from './run'; +import { + filterPaths, + extractMessagesFromPathToMap, + writeFileAsync, + serializeToJson, + serializeToJson5, +} from './i18n/'; run(async ({ flags: { path, output, 'output-format': outputFormat } }) => { - await extractDefaultTranslations({ - paths: Array.isArray(path) ? path : [path || './'], - output, - outputFormat, - }); + const paths = Array.isArray(path) ? path : [path || './']; + const filteredPaths = filterPaths(paths); + + if (filteredPaths.length === 0) { + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} \ +None of input paths is available for extraction or validation. See .i18nrc.json.` + ); + } + + const list = new Listr( + filteredPaths.map(filteredPath => ({ + task: messages => extractMessagesFromPathToMap(filteredPath, messages), + title: filteredPath, + })) + ); + + // messages shouldn't be extracted to a file if output is not supplied + const messages = await list.run(new Map()); + if (!output || !messages.size) { + return; + } + + const sortedMessages = [...messages].sort(([key1], [key2]) => key1.localeCompare(key2)); + await writeFileAsync( + resolve(output, 'en.json'), + outputFormat === 'json5' ? serializeToJson5(sortedMessages) : serializeToJson(sortedMessages) + ); }); diff --git a/src/functional_test_runner/lib/providers/async_instance.js b/src/functional_test_runner/lib/providers/async_instance.js index f6118656ae0c4..aead3291efc91 100644 --- a/src/functional_test_runner/lib/providers/async_instance.js +++ b/src/functional_test_runner/lib/providers/async_instance.js @@ -18,95 +18,102 @@ */ const createdInstanceProxies = new WeakSet(); +const INITIALIZING = Symbol('async instance initializing'); export const isAsyncInstance = val =>( createdInstanceProxies.has(val) ); export const createAsyncInstance = (type, name, promiseForValue) => { - let finalValue; + let instance = INITIALIZING; - const initPromise = promiseForValue.then(v => finalValue = v); + const initPromise = promiseForValue.then(v => instance = v); const initFn = () => initPromise; const assertReady = desc => { - if (!finalValue) { + if (instance === INITIALIZING) { throw new Error(` ${type} \`${desc}\` is loaded asynchronously but isn't available yet. Either await the promise returned from ${name}.init(), or move this access into a test hook like \`before()\` or \`beforeEach()\`. `); } + + if (typeof instance !== 'object') { + throw new TypeError(` + ${type} \`${desc}\` is not supported because ${name} is ${typeof instance} + `); + } }; const proxy = new Proxy({}, { apply(target, context, args) { assertReady(`${name}()`); - return Reflect.apply(finalValue, context, args); + return Reflect.apply(instance, context, args); }, construct(target, args, newTarget) { assertReady(`new ${name}()`); - return Reflect.construct(finalValue, args, newTarget); + return Reflect.construct(instance, args, newTarget); }, defineProperty(target, prop, descriptor) { assertReady(`${name}.${prop}`); - return Reflect.defineProperty(finalValue, prop, descriptor); + return Reflect.defineProperty(instance, prop, descriptor); }, deleteProperty(target, prop) { assertReady(`${name}.${prop}`); - return Reflect.deleteProperty(finalValue, prop); + return Reflect.deleteProperty(instance, prop); }, get(target, prop, receiver) { if (prop === 'init') return initFn; assertReady(`${name}.${prop}`); - return Reflect.get(finalValue, prop, receiver); + return Reflect.get(instance, prop, receiver); }, getOwnPropertyDescriptor(target, prop) { assertReady(`${name}.${prop}`); - return Reflect.getOwnPropertyDescriptor(finalValue, prop); + return Reflect.getOwnPropertyDescriptor(instance, prop); }, getPrototypeOf() { assertReady(`${name}`); - return Reflect.getPrototypeOf(finalValue); + return Reflect.getPrototypeOf(instance); }, has(target, prop) { if (prop === 'init') return true; assertReady(`${name}.${prop}`); - return Reflect.has(finalValue, prop); + return Reflect.has(instance, prop); }, isExtensible() { assertReady(`${name}`); - return Reflect.isExtensible(finalValue); + return Reflect.isExtensible(instance); }, ownKeys() { assertReady(`${name}`); - return Reflect.ownKeys(finalValue); + return Reflect.ownKeys(instance); }, preventExtensions() { assertReady(`${name}`); - return Reflect.preventExtensions(finalValue); + return Reflect.preventExtensions(instance); }, set(target, prop, value, receiver) { assertReady(`${name}.${prop}`); - return Reflect.set(finalValue, prop, value, receiver); + return Reflect.set(instance, prop, value, receiver); }, setPrototypeOf(target, prototype) { assertReady(`${name}`); - return Reflect.setPrototypeOf(finalValue, prototype); + return Reflect.setPrototypeOf(instance, prototype); } }); diff --git a/src/server/config/__tests__/deprecation_warnings.js b/src/server/config/__tests__/deprecation_warnings.js index 4e90708456b25..9935a2e4bddbf 100644 --- a/src/server/config/__tests__/deprecation_warnings.js +++ b/src/server/config/__tests__/deprecation_warnings.js @@ -40,7 +40,8 @@ describe('config/deprecation warnings mixin', function () { env: { CREATE_SERVER_OPTS: JSON.stringify({ logging: { - quiet: false + quiet: false, + silent: false }, uiSettings: { enabled: true diff --git a/src/server/config/__tests__/fixtures/run_kbn_server_startup.js b/src/server/config/__tests__/fixtures/run_kbn_server_startup.js index 46eb6b4661f49..d6622cf69ddb0 100644 --- a/src/server/config/__tests__/fixtures/run_kbn_server_startup.js +++ b/src/server/config/__tests__/fixtures/run_kbn_server_startup.js @@ -17,18 +17,18 @@ * under the License. */ -import { createServer } from '../../../../test_utils/kbn_server'; +import { createRoot } from '../../../../test_utils/kbn_server'; (async function run() { - const server = createServer(JSON.parse(process.env.CREATE_SERVER_OPTS)); + const root = createRoot(JSON.parse(process.env.CREATE_SERVER_OPTS)); // We just need the server to run through startup so that it will // log the deprecation messages. Once it has started up we close it // to allow the process to exit naturally try { - await server.ready(); + await root.start(); } finally { - await server.close(); + await root.shutdown(); } }()); diff --git a/src/server/config/schema.js b/src/server/config/schema.js index 6fa09a400cc09..3648e88fac2e8 100644 --- a/src/server/config/schema.js +++ b/src/server/config/schema.js @@ -186,7 +186,7 @@ export default () => Joi.object({ then: Joi.default(!process.stdout.isTTY), otherwise: Joi.default(true) }), - useUTC: Joi.boolean().default(true), + timezone: Joi.string().allow(false).default('UTC') }).default(), ops: Joi.object({ diff --git a/src/server/config/transform_deprecations.js b/src/server/config/transform_deprecations.js index 44571cee6c58d..d15e171f031ff 100644 --- a/src/server/config/transform_deprecations.js +++ b/src/server/config/transform_deprecations.js @@ -17,8 +17,9 @@ * under the License. */ -import _, { partial } from 'lodash'; +import _, { partial, set } from 'lodash'; import { createTransform, Deprecations } from '../../deprecation'; +import { unset } from '../../utils'; const { rename, unused } = Deprecations; @@ -55,6 +56,15 @@ const rewriteBasePath = (settings, log) => { } }; +const loggingTimezone = (settings, log) => { + if (_.has(settings, 'logging.useUTC')) { + const timezone = settings.logging.useUTC ? 'UTC' : false; + set('logging.timezone', timezone); + unset(settings, 'logging.UTC'); + log(`Config key "logging.useUTC" is deprecated. It has been replaced with "logging.timezone"`); + } +}; + const deprecations = [ //server rename('server.ssl.cert', 'server.ssl.certificate'), @@ -68,6 +78,7 @@ const deprecations = [ serverSslEnabled, savedObjectsIndexCheckTimeout, rewriteBasePath, + loggingTimezone, ]; export const transformDeprecations = createTransform(deprecations); diff --git a/src/server/http/__snapshots__/max_payload_size.test.js.snap b/src/server/http/__snapshots__/max_payload_size.test.js.snap deleted file mode 100644 index 12e9ab278e1fb..0000000000000 --- a/src/server/http/__snapshots__/max_payload_size.test.js.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`fails with 400 if payload size is larger than default and route config allows 1`] = `"{\\"statusCode\\":400,\\"error\\":\\"Bad Request\\",\\"message\\":\\"Payload content length greater than maximum allowed: 200\\"}"`; diff --git a/src/server/http/__snapshots__/xsrf.test.js.snap b/src/server/http/__snapshots__/xsrf.test.js.snap deleted file mode 100644 index 2113d27927dce..0000000000000 --- a/src/server/http/__snapshots__/xsrf.test.js.snap +++ /dev/null @@ -1,7 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`xsrf request filter destructiveMethod: DELETE rejects requests without either an xsrf or version header: DELETE reject response 1`] = `"{\\"statusCode\\":400,\\"error\\":\\"Bad Request\\",\\"message\\":\\"Request must contain a kbn-xsrf header.\\"}"`; - -exports[`xsrf request filter destructiveMethod: POST rejects requests without either an xsrf or version header: POST reject response 1`] = `"{\\"statusCode\\":400,\\"error\\":\\"Bad Request\\",\\"message\\":\\"Request must contain a kbn-xsrf header.\\"}"`; - -exports[`xsrf request filter destructiveMethod: PUT rejects requests without either an xsrf or version header: PUT reject response 1`] = `"{\\"statusCode\\":400,\\"error\\":\\"Bad Request\\",\\"message\\":\\"Request must contain a kbn-xsrf header.\\"}"`; diff --git a/src/server/http/index.js b/src/server/http/index.js index 865de185a144c..d7a79b0d02fa7 100644 --- a/src/server/http/index.js +++ b/src/server/http/index.js @@ -31,35 +31,7 @@ export default async function (kbnServer, server, config) { kbnServer.server = new Hapi.Server(); server = kbnServer.server; - // Note that all connection options configured here should be exactly the same - // as in `getServerOptions()` in the new platform (see `src/core/server/http/http_tools`). - // - // The only exception is `tls` property: TLS is entirely handled by the new - // platform and we don't have to duplicate all TLS related settings here, we just need - // to indicate to Hapi connection that TLS is used so that it can use correct protocol - // name in `server.info` and `request.connection.info` that are used throughout Kibana. - // - // Any change SHOULD BE applied in both places. - server.connection({ - host: config.get('server.host'), - port: config.get('server.port'), - tls: config.get('server.ssl.enabled'), - listener: kbnServer.newPlatform.proxyListener, - state: { - strictHeader: false, - }, - routes: { - cors: config.get('server.cors'), - payload: { - maxBytes: config.get('server.maxPayloadBytes'), - }, - validate: { - options: { - abortEarly: false, - }, - }, - }, - }); + server.connection(kbnServer.core.serverOptions); setupBasePathProvider(server, config); diff --git a/src/server/http/integration_tests/max_payload_size.test.js b/src/server/http/integration_tests/max_payload_size.test.js new file mode 100644 index 0000000000000..3fa7ca721e1ef --- /dev/null +++ b/src/server/http/integration_tests/max_payload_size.test.js @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import * as kbnTestServer from '../../../test_utils/kbn_server'; + +let root; +beforeAll(async () => { + root = kbnTestServer.createRoot({ server: { maxPayloadBytes: 100 } }); + + await root.start(); + + kbnTestServer.getKbnServer(root).server.route({ + path: '/payload_size_check/test/route', + method: 'POST', + config: { payload: { maxBytes: 200 } }, + handler: (req, reply) => reply(null, req.payload.data.slice(0, 5)), + }); +}, 30000); + +afterAll(async () => await root.shutdown()); + +test('accepts payload with a size larger than default but smaller than route config allows', async () => { + await kbnTestServer.request.post(root, '/payload_size_check/test/route') + .send({ data: Array(150).fill('+').join('') }) + .expect(200, '+++++'); +}); + +test('fails with 400 if payload size is larger than default and route config allows', async () => { + await kbnTestServer.request.post(root, '/payload_size_check/test/route') + .send({ data: Array(250).fill('+').join('') }) + .expect(400, { + statusCode: 400, + error: 'Bad Request', + message: 'Payload content length greater than maximum allowed: 200' + }); +}); diff --git a/src/server/http/version_check.test.js b/src/server/http/integration_tests/version_check.test.js similarity index 53% rename from src/server/http/version_check.test.js rename to src/server/http/integration_tests/version_check.test.js index e5257f814e8ae..676391ce3233b 100644 --- a/src/server/http/version_check.test.js +++ b/src/server/http/integration_tests/version_check.test.js @@ -18,71 +18,48 @@ */ import { resolve } from 'path'; -import * as kbnTestServer from '../../test_utils/kbn_server'; +import * as kbnTestServer from '../../../test_utils/kbn_server'; -const src = resolve.bind(null, __dirname, '../../../src'); +const src = resolve.bind(null, __dirname, '../../../../src'); const versionHeader = 'kbn-version'; const version = require(src('../package.json')).version; describe('version_check request filter', function () { - async function makeRequest(kbnServer, opts) { - return await kbnTestServer.makeRequest(kbnServer, opts); - } + let root; + beforeAll(async () => { + root = kbnTestServer.createRoot(); - async function makeServer() { - const kbnServer = kbnTestServer.createServer(); + await root.start(); - await kbnServer.ready(); - - kbnServer.server.route({ + kbnTestServer.getKbnServer(root).server.route({ path: '/version_check/test/route', method: 'GET', handler: function (req, reply) { reply(null, 'ok'); } }); + }, 30000); - return kbnServer; - } - - let kbnServer; - beforeEach(async () => kbnServer = await makeServer()); - afterEach(async () => await kbnServer.close()); + afterAll(async () => await root.shutdown()); it('accepts requests with the correct version passed in the version header', async function () { - const resp = await makeRequest(kbnServer, { - url: '/version_check/test/route', - method: 'GET', - headers: { - [versionHeader]: version, - }, - }); - - expect(resp.statusCode).toBe(200); - expect(resp.payload).toBe('ok'); + await kbnTestServer.request + .get(root, '/version_check/test/route') + .set(versionHeader, version) + .expect(200, 'ok'); }); it('rejects requests with an incorrect version passed in the version header', async function () { - const resp = await makeRequest(kbnServer, { - url: '/version_check/test/route', - method: 'GET', - headers: { - [versionHeader]: `invalid:${version}`, - }, - }); - - expect(resp.statusCode).toBe(400); - expect(resp.payload).toMatch(/"Browser client is out of date/); + await kbnTestServer.request + .get(root, '/version_check/test/route') + .set(versionHeader, `invalid:${version}`) + .expect(400, /"Browser client is out of date/); }); it('accepts requests that do not include a version header', async function () { - const resp = await makeRequest(kbnServer, { - url: '/version_check/test/route', - method: 'GET' - }); - - expect(resp.statusCode).toBe(200); - expect(resp.payload).toBe('ok'); + await kbnTestServer.request + .get(root, '/version_check/test/route') + .expect(200, 'ok'); }); }); diff --git a/src/server/http/xsrf.test.js b/src/server/http/integration_tests/xsrf.test.js similarity index 55% rename from src/server/http/xsrf.test.js rename to src/server/http/integration_tests/xsrf.test.js index 2fc6dba4703ef..a8c87653e9b40 100644 --- a/src/server/http/xsrf.test.js +++ b/src/server/http/integration_tests/xsrf.test.js @@ -18,10 +18,10 @@ */ import { resolve } from 'path'; -import * as kbnTestServer from '../../test_utils/kbn_server'; +import * as kbnTestServer from '../../../test_utils/kbn_server'; const destructiveMethods = ['POST', 'PUT', 'DELETE']; -const src = resolve.bind(null, __dirname, '../../../src'); +const src = resolve.bind(null, __dirname, '../../../../src'); const xsrfHeader = 'kbn-xsrf'; const versionHeader = 'kbn-version'; @@ -29,23 +29,18 @@ const testPath = '/xsrf/test/route'; const whitelistedTestPath = '/xsrf/test/route/whitelisted'; const actualVersion = require(src('../package.json')).version; -describe('xsrf request filter', function () { - async function inject(kbnServer, opts) { - return await kbnTestServer.makeRequest(kbnServer, opts); - } - - const makeServer = async function () { - const kbnServer = kbnTestServer.createServer({ +describe('xsrf request filter', () => { + let root; + beforeAll(async () => { + root = kbnTestServer.createRoot({ server: { - xsrf: { - disableProtection: false, - whitelist: [whitelistedTestPath] - } + xsrf: { disableProtection: false, whitelist: [whitelistedTestPath] } } }); - await kbnServer.ready(); + await root.start(); + const kbnServer = kbnTestServer.getKbnServer(root); kbnServer.server.route({ path: testPath, method: 'GET', @@ -81,117 +76,68 @@ describe('xsrf request filter', function () { reply(null, 'ok'); } }); + }, 30000); - return kbnServer; - }; - - let kbnServer; - beforeEach(async () => { - kbnServer = await makeServer(); - }); - - afterEach(async () => { - await kbnServer.close(); - }); + afterAll(async () => await root.shutdown()); describe(`nonDestructiveMethod: GET`, function () { it('accepts requests without a token', async function () { - const resp = await inject(kbnServer, { - url: testPath, - method: 'GET' - }); - - expect(resp.statusCode).toBe(200); - expect(resp.payload).toBe('ok'); + await kbnTestServer.request + .get(root, testPath) + .expect(200, 'ok'); }); it('accepts requests with the xsrf header', async function () { - const resp = await inject(kbnServer, { - url: testPath, - method: 'GET', - headers: { - [xsrfHeader]: 'anything', - }, - }); - - expect(resp.statusCode).toBe(200); - expect(resp.payload).toBe('ok'); + await kbnTestServer.request + .get(root, testPath) + .set(xsrfHeader, 'anything') + .expect(200, 'ok'); }); }); describe(`nonDestructiveMethod: HEAD`, function () { it('accepts requests without a token', async function () { - const resp = await inject(kbnServer, { - url: testPath, - method: 'HEAD' - }); - - expect(resp.statusCode).toBe(200); - expect(resp.payload).toHaveLength(0); + await kbnTestServer.request + .head(root, testPath) + .expect(200, undefined); }); it('accepts requests with the xsrf header', async function () { - const resp = await inject(kbnServer, { - url: testPath, - method: 'HEAD', - headers: { - [xsrfHeader]: 'anything', - }, - }); - - expect(resp.statusCode).toBe(200); - expect(resp.payload).toHaveLength(0); + await kbnTestServer.request + .head(root, testPath) + .set(xsrfHeader, 'anything') + .expect(200, undefined); }); }); for (const method of destructiveMethods) { describe(`destructiveMethod: ${method}`, function () { // eslint-disable-line no-loop-func it('accepts requests with the xsrf header', async function () { - const resp = await inject(kbnServer, { - url: testPath, - method: method, - headers: { - [xsrfHeader]: 'anything', - }, - }); - - expect(resp.statusCode).toBe(200); - expect(resp.payload).toBe('ok'); + await kbnTestServer.request[method.toLowerCase()](root, testPath) + .set(xsrfHeader, 'anything') + .expect(200, 'ok'); }); // this is still valid for existing csrf protection support // it does not actually do any validation on the version value itself it('accepts requests with the version header', async function () { - const resp = await inject(kbnServer, { - url: testPath, - method: method, - headers: { - [versionHeader]: actualVersion, - }, - }); - - expect(resp.statusCode).toBe(200); - expect(resp.payload).toBe('ok'); + await kbnTestServer.request[method.toLowerCase()](root, testPath) + .set(versionHeader, actualVersion) + .expect(200, 'ok'); }); it('rejects requests without either an xsrf or version header', async function () { - const resp = await inject(kbnServer, { - url: testPath, - method: method - }); - - expect(resp.statusCode).toBe(400); - expect(resp.result).toMatchSnapshot(`${method} reject response`); + await kbnTestServer.request[method.toLowerCase()](root, testPath) + .expect(400, { + statusCode: 400, + error: 'Bad Request', + message: 'Request must contain a kbn-xsrf header.' + }); }); it('accepts whitelisted requests without either an xsrf or version header', async function () { - const resp = await inject(kbnServer, { - url: whitelistedTestPath, - method: method - }); - - expect(resp.statusCode).toBe(200); - expect(resp.payload).toBe('ok'); + await kbnTestServer.request[method.toLowerCase()](root, whitelistedTestPath) + .expect(200, 'ok'); }); }); } diff --git a/src/server/http/max_payload_size.test.js b/src/server/http/max_payload_size.test.js deleted file mode 100644 index 499ce43b8d09a..0000000000000 --- a/src/server/http/max_payload_size.test.js +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import * as kbnTestServer from '../../test_utils/kbn_server'; - -let kbnServer; -async function makeServer({ maxPayloadBytesDefault, maxPayloadBytesRoute }) { - kbnServer = kbnTestServer.createServer({ - server: { maxPayloadBytes: maxPayloadBytesDefault } - }); - - await kbnServer.ready(); - - kbnServer.server.route({ - path: '/payload_size_check/test/route', - method: 'POST', - config: { payload: { maxBytes: maxPayloadBytesRoute } }, - handler: function (req, reply) { - reply(null, req.payload.data.slice(0, 5)); - } - }); -} - -async function makeRequest(opts) { - return await kbnTestServer.makeRequest(kbnServer, opts); -} - -afterEach(async () => await kbnServer.close()); - -test('accepts payload with a size larger than default but smaller than route config allows', async () => { - await makeServer({ maxPayloadBytesDefault: 100, maxPayloadBytesRoute: 200 }); - - const resp = await makeRequest({ - url: '/payload_size_check/test/route', - method: 'POST', - payload: { data: Array(150).fill('+').join('') }, - }); - - expect(resp.statusCode).toBe(200); - expect(resp.payload).toBe('+++++'); -}); - -test('fails with 400 if payload size is larger than default and route config allows', async () => { - await makeServer({ maxPayloadBytesDefault: 100, maxPayloadBytesRoute: 200 }); - - const resp = await makeRequest({ - url: '/payload_size_check/test/route', - method: 'POST', - payload: { data: Array(250).fill('+').join('') }, - }); - - expect(resp.statusCode).toBe(400); - expect(resp.payload).toMatchSnapshot(); -}); diff --git a/src/server/http/setup_connection.js b/src/server/http/setup_connection.js deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/src/server/kbn_server.js b/src/server/kbn_server.js index 7279a8f407b11..4f4334d764ddb 100644 --- a/src/server/kbn_server.js +++ b/src/server/kbn_server.js @@ -21,6 +21,7 @@ import { constant, once, compact, flatten } from 'lodash'; import { fromNode } from 'bluebird'; import { isWorker } from 'cluster'; import { fromRoot, pkg } from '../utils'; +import { Config } from './config'; import loggingConfiguration from './logging/configuration'; import configSetupMixin from './config/setup'; import httpMixin from './http'; @@ -30,6 +31,7 @@ import { usageMixin } from './usage'; import { statusMixin } from './status'; import pidMixin from './pid'; import { configDeprecationWarningsMixin } from './config/deprecation_warnings'; +import { transformDeprecations } from './config/transform_deprecations'; import configCompleteMixin from './config/complete'; import optimizeMixin from '../optimize'; import * as Plugins from './plugins'; @@ -41,27 +43,26 @@ import { urlShorteningMixin } from './url_shortening'; import { serverExtensionsMixin } from './server_extensions'; import { uiMixin } from '../ui'; import { sassMixin } from './sass'; -import { injectIntoKbnServer as newPlatformMixin } from '../core'; import { i18nMixin } from './i18n'; const rootDir = fromRoot('.'); export default class KbnServer { - constructor(settings) { + constructor(settings, core) { this.name = pkg.name; this.version = pkg.version; this.build = pkg.build || false; this.rootDir = rootDir; this.settings = settings || {}; + this.core = core; + this.ready = constant(this.mixin( Plugins.waitForInitSetupMixin, // sets this.config, reads this.settings configSetupMixin, - newPlatformMixin, - // sets this.server httpMixin, @@ -111,13 +112,6 @@ export default class KbnServer { // notify any deferred setup logic that plugins have initialized Plugins.waitForInitResolveMixin, - - () => { - if (this.config.get('server.autoListen')) { - this.ready = constant(Promise.resolve()); - return this.listen(); - } - } )); this.listen = once(this.listen); @@ -148,14 +142,17 @@ export default class KbnServer { async listen() { await this.ready(); - const { server } = this; - await fromNode(cb => server.start(cb)); - if (isWorker) { // help parent process know when we are ready process.send(['WORKER_LISTENING']); } + const { server, config } = this; + server.log(['listening', 'info'], `Server running at ${server.info.uri}${ + config.get('server.rewriteBasePath') + ? config.get('server.basePath') + : '' + }`); return server; } @@ -171,7 +168,12 @@ export default class KbnServer { return await this.server.inject(opts); } - async applyLoggingConfiguration(config) { + applyLoggingConfiguration(settings) { + const config = new Config( + this.config.getSchema(), + transformDeprecations(settings) + ); + const loggingOptions = loggingConfiguration(config); const subset = { ops: config.get('ops'), diff --git a/src/server/logging/configuration.js b/src/server/logging/configuration.js index beec30b54bccb..59019ad873129 100644 --- a/src/server/logging/configuration.js +++ b/src/server/logging/configuration.js @@ -61,7 +61,7 @@ export default function loggingConfiguration(config) { config: { json: config.get('logging.json'), dest: config.get('logging.dest'), - useUTC: config.get('logging.useUTC'), + timezone: config.get('logging.timezone'), // I'm adding the default here because if you add another filter // using the commandline it will remove authorization. I want users diff --git a/src/server/logging/log_format.js b/src/server/logging/log_format.js index 43ffca7fd39c6..994b5af8b89f4 100644 --- a/src/server/logging/log_format.js +++ b/src/server/logging/log_format.js @@ -18,7 +18,7 @@ */ import Stream from 'stream'; -import moment from 'moment'; +import moment from 'moment-timezone'; import { get, _ } from 'lodash'; import numeral from '@elastic/numeral'; import chalk from 'chalk'; @@ -66,10 +66,10 @@ export default class TransformObjStream extends Stream.Transform { } extractAndFormatTimestamp(data, format) { - const { useUTC } = this.config; + const { timezone } = this.config; const date = moment(data['@timestamp']); - if (useUTC) { - date.utc(); + if (timezone) { + date.tz(timezone); } return date.format(format); } diff --git a/src/server/logging/log_format_json.test.js b/src/server/logging/log_format_json.test.js index b9878e63f0898..1632b2b401c8a 100644 --- a/src/server/logging/log_format_json.test.js +++ b/src/server/logging/log_format_json.test.js @@ -196,10 +196,10 @@ describe('KbnLoggerJsonFormat', () => { }); }); - describe('useUTC', () => { - it('logs in UTC when useUTC is true', async () => { + describe('timezone', () => { + it('logs in UTC', async () => { const format = new KbnLoggerJsonFormat({ - useUTC: true + timezone: 'UTC' }); const result = await createPromiseFromStreams([ @@ -211,10 +211,8 @@ describe('KbnLoggerJsonFormat', () => { expect(timestamp).toBe(moment.utc(time).format()); }); - it('logs in local timezone when useUTC is false', async () => { - const format = new KbnLoggerJsonFormat({ - useUTC: false - }); + it('logs in local timezone timezone is undefined', async () => { + const format = new KbnLoggerJsonFormat({}); const result = await createPromiseFromStreams([ createListStream([makeEvent('log')]), diff --git a/src/server/logging/log_format_string.test.js b/src/server/logging/log_format_string.test.js index ca572f8c03e66..e20b5eb59b76c 100644 --- a/src/server/logging/log_format_string.test.js +++ b/src/server/logging/log_format_string.test.js @@ -37,9 +37,9 @@ const makeEvent = () => ({ }); describe('KbnLoggerStringFormat', () => { - it('logs in UTC when useUTC is true', async () => { + it('logs in UTC', async () => { const format = new KbnLoggerStringFormat({ - useUTC: true + timezone: 'UTC' }); const result = await createPromiseFromStreams([ @@ -51,10 +51,8 @@ describe('KbnLoggerStringFormat', () => { .toContain(moment.utc(time).format('HH:mm:ss.SSS')); }); - it('logs in local timezone when useUTC is false', async () => { - const format = new KbnLoggerStringFormat({ - useUTC: false - }); + it('logs in local timezone when timezone is undefined', async () => { + const format = new KbnLoggerStringFormat({}); const result = await createPromiseFromStreams([ createListStream([makeEvent()]), diff --git a/src/server/saved_objects/service/lib/search_dsl/sorting_params.js b/src/server/saved_objects/service/lib/search_dsl/sorting_params.js index b977924ff62c5..bf22e2818939c 100644 --- a/src/server/saved_objects/service/lib/search_dsl/sorting_params.js +++ b/src/server/saved_objects/service/lib/search_dsl/sorting_params.js @@ -26,24 +26,29 @@ export function getSortingParams(mappings, type, sortField, sortOrder) { return {}; } + let typeField = type; + if (Array.isArray(type)) { - const rootField = getProperty(mappings, sortField); - if (!rootField) { - throw Boom.badRequest(`Unable to sort multiple types by field ${sortField}, not a root property`); - } + if (type.length === 1) { + typeField = type[0]; + } else { + const rootField = getProperty(mappings, sortField); + if (!rootField) { + throw Boom.badRequest(`Unable to sort multiple types by field ${sortField}, not a root property`); + } - return { - sort: [{ - [sortField]: { - order: sortOrder, - unmapped_type: rootField.type - } - }] - }; + return { + sort: [{ + [sortField]: { + order: sortOrder, + unmapped_type: rootField.type + } + }] + }; + } } - - const key = `${type}.${sortField}`; + const key = `${typeField}.${sortField}`; const field = getProperty(mappings, key); if (!field) { throw Boom.badRequest(`Unknown sort field ${sortField}`); diff --git a/src/server/saved_objects/service/lib/search_dsl/sorting_params.test.js b/src/server/saved_objects/service/lib/search_dsl/sorting_params.test.js index a7ff1041f313a..71833f1395659 100644 --- a/src/server/saved_objects/service/lib/search_dsl/sorting_params.test.js +++ b/src/server/saved_objects/service/lib/search_dsl/sorting_params.test.js @@ -138,6 +138,21 @@ describe('searchDsl/getSortParams', () => { }); }); }); + describe('sortField is multi-field with single type as array', () => { + it('returns correct params', () => { + expect(getSortingParams(MAPPINGS, ['saved'], 'title.raw')) + .toEqual({ + sort: [ + { + 'saved.title.raw': { + order: undefined, + unmapped_type: 'keyword' + } + } + ] + }); + }); + }); describe('sortField is root multi-field with multiple types', () => { it('returns correct params', () => { expect(getSortingParams(MAPPINGS, ['saved', 'pending'], 'type.raw')) diff --git a/src/test_utils/kbn_server.js b/src/test_utils/kbn_server.js deleted file mode 100644 index a0c802ce052e5..0000000000000 --- a/src/test_utils/kbn_server.js +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { resolve } from 'path'; -import { defaultsDeep, set } from 'lodash'; -import { header as basicAuthHeader } from './base_auth'; -import { createEsTestCluster, esTestConfig, kibanaTestUser, kibanaServerTestUser } from '@kbn/test'; -import KbnServer from '../../src/server/kbn_server'; -import { ToolingLog } from '@kbn/dev-utils'; - -const DEFAULTS_SETTINGS = { - server: { - autoListen: true, - // Use the ephemeral port to make sure that tests use the first available - // port and aren't affected by the timing issues in test environment. - port: 0, - xsrf: { - disableProtection: true - } - }, - logging: { - quiet: true - }, - plugins: {}, - optimize: { - enabled: false - }, -}; - -const DEFAULT_SETTINGS_WITH_CORE_PLUGINS = { - plugins: { - scanDirs: [ - resolve(__dirname, '../core_plugins'), - ], - }, - elasticsearch: { - url: esTestConfig.getUrl(), - username: kibanaServerTestUser.username, - password: kibanaServerTestUser.password - }, -}; - -/** - * Creates an instance of KbnServer with default configuration - * tailored for unit tests - * - * @param {Object} [settings={}] Any config overrides for this instance - * @return {KbnServer} - */ -export function createServer(settings = {}) { - return new KbnServer(defaultsDeep({}, settings, DEFAULTS_SETTINGS)); -} - -/** - * Creates an instance of KbnServer, including all of the core plugins, - * with default configuration tailored for unit tests, and starts es. - * - * @param {Object} options - * @prop {Object} settings Any config overrides for this instance - * @prop {function} adjustTimeout A function(t) => this.timeout(t) that adjust the timeout of a test, - * ensuring the test properly waits for the server to boot without timing out. - * @return {KbnServer} - */ -export async function startTestServers({ adjustTimeout, settings = {} }) { - if (!adjustTimeout) { - throw new Error('adjustTimeout is required in order to avoid flaky tests'); - } - - const log = new ToolingLog({ - level: 'debug', - writeTo: process.stdout - }); - - log.indent(6); - log.info('starting elasticsearch'); - log.indent(4); - - const es = createEsTestCluster({ log }); - - log.indent(-4); - - adjustTimeout(es.getStartTimeout()); - - await es.start(); - - const kbnServer = createServerWithCorePlugins(settings); - - await kbnServer.ready(); - await kbnServer.server.plugins.elasticsearch.waitUntilReady(); - - return { - kbnServer, - es, - - async stop() { - await this.kbnServer.close(); - await es.cleanup(); - }, - }; -} - -/** - * Creates an instance of KbnServer, including all of the core plugins, - * with default configuration tailored for unit tests - * - * @param {Object} [settings={}] Any config overrides for this instance - * @return {KbnServer} - */ -export function createServerWithCorePlugins(settings = {}) { - return new KbnServer(defaultsDeep({}, settings, DEFAULT_SETTINGS_WITH_CORE_PLUGINS, DEFAULTS_SETTINGS)); -} - -/** - * Creates request configuration with a basic auth header - */ -export function authOptions() { - const { username, password } = kibanaTestUser; - const authHeader = basicAuthHeader(username, password); - return set({}, 'headers.Authorization', authHeader); -} - -/** - * Makes a request with test headers via hapi server inject() - * - * The given options are decorated with default testing options, so it's - * recommended to use this function instead of using inject() directly whenever - * possible throughout the tests. - * - * @param {KbnServer} kbnServer - * @param {object} options Any additional options or overrides for inject() - */ -export async function makeRequest(kbnServer, options) { - // Since all requests to Kibana hit core http server first and only after that - // are proxied to the "legacy" Kibana we should inject requests through the top - // level Hapi server used by the core. - return await kbnServer.newPlatform.proxyListener.root.server.http.service.httpServer.server.inject( - defaultsDeep({}, authOptions(), options) - ); -} diff --git a/src/test_utils/kbn_server.ts b/src/test_utils/kbn_server.ts new file mode 100644 index 0000000000000..3b841c2b6ac02 --- /dev/null +++ b/src/test_utils/kbn_server.ts @@ -0,0 +1,184 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ToolingLog } from '@kbn/dev-utils'; +// @ts-ignore: implicit any for JS file +import { createEsTestCluster, esTestConfig, kibanaServerTestUser, kibanaTestUser } from '@kbn/test'; +import { defaultsDeep } from 'lodash'; +import { resolve } from 'path'; +import { BehaviorSubject } from 'rxjs'; +import supertest from 'supertest'; +import { Env } from '../core/server/config'; +import { LegacyObjectToConfigAdapter } from '../core/server/legacy_compat'; +import { Root } from '../core/server/root'; + +type HttpMethod = 'delete' | 'get' | 'head' | 'post' | 'put'; + +const DEFAULTS_SETTINGS = { + server: { + autoListen: true, + // Use the ephemeral port to make sure that tests use the first available + // port and aren't affected by the timing issues in test environment. + port: 0, + xsrf: { disableProtection: true }, + }, + logging: { silent: true }, + plugins: {}, + optimize: { enabled: false }, +}; + +const DEFAULT_SETTINGS_WITH_CORE_PLUGINS = { + plugins: { scanDirs: [resolve(__dirname, '../core_plugins')] }, + elasticsearch: { + url: esTestConfig.getUrl(), + username: kibanaServerTestUser.username, + password: kibanaServerTestUser.password, + }, +}; + +export function createRootWithSettings(...settings: Array>) { + const env = Env.createDefault({ + configs: [], + cliArgs: { + dev: false, + quiet: false, + silent: false, + watch: false, + repl: false, + basePath: false, + }, + isDevClusterMaster: false, + }); + + return new Root( + new BehaviorSubject( + new LegacyObjectToConfigAdapter(defaultsDeep({}, ...settings, DEFAULTS_SETTINGS)) + ), + env + ); +} + +/** + * Returns supertest request attached to the core's internal native Node server. + * @param root + * @param method + * @param path + */ +function getSupertest(root: Root, method: HttpMethod, path: string) { + const testUserCredentials = new Buffer(`${kibanaTestUser.username}:${kibanaTestUser.password}`); + return supertest((root as any).server.http.service.httpServer.server.listener) + [method](path) + .set('Authorization', `Basic ${testUserCredentials.toString('base64')}`); +} + +/** + * Creates an instance of Root with default configuration + * tailored for unit tests. + * + * @param {Object} [settings={}] Any config overrides for this instance. + * @returns {Root} + */ +export function createRoot(settings = {}) { + return createRootWithSettings(settings); +} + +/** + * Creates an instance of Root, including all of the core plugins, + * with default configuration tailored for unit tests. + * + * @param {Object} [settings={}] Any config overrides for this instance. + * @returns {Root} + */ +export function createRootWithCorePlugins(settings = {}) { + return createRootWithSettings(settings, DEFAULT_SETTINGS_WITH_CORE_PLUGINS); +} + +/** + * Returns `kbnServer` instance used in the "legacy" Kibana. + * @param root + */ +export function getKbnServer(root: Root) { + return (root as any).server.legacy.service.kbnServer; +} + +export const request: Record< + HttpMethod, + (root: Root, path: string) => ReturnType +> = { + delete: (root, path) => getSupertest(root, 'delete', path), + get: (root, path) => getSupertest(root, 'get', path), + head: (root, path) => getSupertest(root, 'head', path), + post: (root, path) => getSupertest(root, 'post', path), + put: (root, path) => getSupertest(root, 'put', path), +}; + +/** + * Creates an instance of the Root, including all of the core "legacy" plugins, + * with default configuration tailored for unit tests, and starts es. + * + * @param options + * @prop settings Any config overrides for this instance. + * @prop adjustTimeout A function(t) => this.timeout(t) that adjust the timeout of a + * test, ensuring the test properly waits for the server to boot without timing out. + */ +export async function startTestServers({ + adjustTimeout, + settings = {}, +}: { + adjustTimeout: (timeout: number) => void; + settings: Record; +}) { + if (!adjustTimeout) { + throw new Error('adjustTimeout is required in order to avoid flaky tests'); + } + + const log = new ToolingLog({ + level: 'debug', + writeTo: process.stdout, + }); + + log.indent(6); + log.info('starting elasticsearch'); + log.indent(4); + + const es = createEsTestCluster({ log }); + + log.indent(-4); + + adjustTimeout(es.getStartTimeout()); + + await es.start(); + + const root = createRootWithCorePlugins(settings); + await root.start(); + + const kbnServer = getKbnServer(root); + await kbnServer.server.plugins.elasticsearch.waitUntilReady(); + + return { + kbnServer, + root, + es, + + async stop() { + await root.shutdown(); + await es.cleanup(); + }, + }; +} diff --git a/src/ui/__tests__/ui_exports_replace_injected_vars.js b/src/ui/__tests__/ui_exports_replace_injected_vars.js index b7762ef104b90..5cb05ac1dbeeb 100644 --- a/src/ui/__tests__/ui_exports_replace_injected_vars.js +++ b/src/ui/__tests__/ui_exports_replace_injected_vars.js @@ -25,10 +25,10 @@ import sinon from 'sinon'; import cheerio from 'cheerio'; import { noop } from 'lodash'; -import KbnServer from '../../server/kbn_server'; +import { createRoot, getKbnServer, request } from '../../test_utils/kbn_server'; const getInjectedVarsFromResponse = (resp) => { - const $ = cheerio.load(resp.payload); + const $ = cheerio.load(resp.text); const data = $('kbn-injected-metadata').attr('data'); return JSON.parse(data).legacyMetadata.vars; }; @@ -45,45 +45,46 @@ const injectReplacer = (kbnServer, replacer) => { }; describe('UiExports', function () { - describe('#replaceInjectedVars', function () { + let root; + let kbnServer; + before(async () => { this.slow(2000); - this.timeout(10000); - - let kbnServer; - beforeEach(async () => { - kbnServer = new KbnServer({ - server: { port: 0 }, // pick a random open port - logging: { silent: true }, // no logs - optimize: { enabled: false }, - plugins: { - paths: [resolve(__dirname, './fixtures/test_app')] // inject an app so we can hit /app/{id} - }, - }); + this.timeout(30000); - await kbnServer.ready(); - - // TODO: hopefully we can add better support for something - // like this in the new platform - kbnServer.server._requestor._decorations.getUiSettingsService = { - apply: undefined, - method() { - return { - getDefaults: noop, - getUserProvided: noop - }; - } - }; + root = root = createRoot({ + // inject an app so we can hit /app/{id} + plugins: { paths: [resolve(__dirname, './fixtures/test_app')] }, }); - afterEach(async () => { - await kbnServer.close(); - kbnServer = null; - }); + await root.start(); + + kbnServer = getKbnServer(root); + + // TODO: hopefully we can add better support for something + // like this in the new platform + kbnServer.server._requestor._decorations.getUiSettingsService = { + apply: undefined, + method: () => ({ getDefaults: noop, getUserProvided: noop }) + }; + }); + + after(async () => await root.shutdown()); + let originalInjectedVarsReplacers; + beforeEach(() => { + originalInjectedVarsReplacers = kbnServer.uiExports.injectedVarsReplacers; + }); + + afterEach(() => { + kbnServer.uiExports.injectedVarsReplacers = originalInjectedVarsReplacers; + }); + + describe('#replaceInjectedVars', function () { it('allows sync replacing of injected vars', async () => { injectReplacer(kbnServer, () => ({ a: 1 })); - const resp = await kbnServer.inject('/app/test_app'); + const resp = await request.get(root, '/app/test_app') + .expect(200); const injectedVars = getInjectedVarsFromResponse(resp); expect(injectedVars).to.eql({ a: 1 }); @@ -98,7 +99,8 @@ describe('UiExports', function () { }; }); - const resp = await kbnServer.inject('/app/test_app'); + const resp = await request.get(root, '/app/test_app') + .expect(200); const injectedVars = getInjectedVarsFromResponse(resp); expect(injectedVars).to.eql({ @@ -111,7 +113,8 @@ describe('UiExports', function () { injectReplacer(kbnServer, () => ({ foo: 'bar' })); injectReplacer(kbnServer, stub); - await kbnServer.inject('/app/test_app'); + await await request.get(root, '/app/test_app') + .expect(200); sinon.assert.calledOnce(stub); expect(stub.firstCall.args[0]).to.eql({ foo: 'bar' }); // originalInjectedVars @@ -126,7 +129,8 @@ describe('UiExports', function () { injectReplacer(kbnServer, orig => ({ name: orig.name + 'a' })); injectReplacer(kbnServer, orig => ({ name: orig.name + 'm' })); - const resp = await kbnServer.inject('/app/test_app'); + const resp = await request.get(root, '/app/test_app') + .expect(200); const injectedVars = getInjectedVarsFromResponse(resp); expect(injectedVars).to.eql({ name: 'sam' }); @@ -138,15 +142,17 @@ describe('UiExports', function () { throw new Error('replacer failed'); }); - const resp = await kbnServer.inject('/app/test_app'); - expect(resp).to.have.property('statusCode', 500); + await request.get(root, '/app/test_app') + .expect(500); }); it('starts off with the injected vars for the app merged with the default injected vars', async () => { const stub = sinon.stub(); injectReplacer(kbnServer, stub); - await kbnServer.inject('/app/test_app'); + await request.get(root, '/app/test_app') + .expect(200); + sinon.assert.calledOnce(stub); expect(stub.firstCall.args[0]).to.eql({ from_defaults: true, from_test_app: true }); }); diff --git a/src/ui/field_formats/__tests__/field_formats_mixin.js b/src/ui/field_formats/__tests__/field_formats_mixin.js index 58c61962c2414..3159705f3d8ed 100644 --- a/src/ui/field_formats/__tests__/field_formats_mixin.js +++ b/src/ui/field_formats/__tests__/field_formats_mixin.js @@ -22,31 +22,31 @@ import sinon from 'sinon'; import { FieldFormat } from '../field_format'; import * as FieldFormatsServiceNS from '../field_formats_service'; -import { createServer } from '../../../test_utils/kbn_server'; +import { fieldFormatsMixin } from '../field_formats_mixin'; describe('server.registerFieldFormat(createFormat)', () => { const sandbox = sinon.createSandbox(); - let kbnServer; + let registerFieldFormat; + let fieldFormatServiceFactory; + const serverMock = { decorate() {} }; beforeEach(async () => { - kbnServer = createServer(); - await kbnServer.ready(); + sandbox.stub(serverMock); + await fieldFormatsMixin({}, serverMock); + [[,, fieldFormatServiceFactory], [,, registerFieldFormat]] = serverMock.decorate.args; }); - afterEach(async () => { - sandbox.restore(); - await kbnServer.close(); - }); + afterEach(() => sandbox.restore()); it('throws if createFormat is not a function', () => { - expect(() => kbnServer.server.registerFieldFormat()).to.throwError(error => { + expect(() => registerFieldFormat()).to.throwError(error => { expect(error.message).to.match(/createFormat is not a function/i); }); }); it('calls the createFormat() function with the FieldFormat class', () => { const createFormat = sinon.stub(); - kbnServer.server.registerFieldFormat(createFormat); + registerFieldFormat(createFormat); sinon.assert.calledOnce(createFormat); sinon.assert.calledWithExactly(createFormat, sinon.match.same(FieldFormat)); }); @@ -61,9 +61,9 @@ describe('server.registerFieldFormat(createFormat)', () => { class FooFormat { static id = 'foo' } - kbnServer.server.registerFieldFormat(() => FooFormat); + registerFieldFormat(() => FooFormat); - const fieldFormats = await kbnServer.server.fieldFormatServiceFactory({ + const fieldFormats = await fieldFormatServiceFactory({ getAll: () => ({}), getDefaults: () => ({}) }); diff --git a/src/ui/public/agg_types/__tests__/metrics/top_hit.js b/src/ui/public/agg_types/__tests__/metrics/top_hit.js index a83053140f5cd..3e616391068d4 100644 --- a/src/ui/public/agg_types/__tests__/metrics/top_hit.js +++ b/src/ui/public/agg_types/__tests__/metrics/top_hit.js @@ -90,7 +90,7 @@ describe('Top hit metric', function () { it('requests both source and docvalues_fields for non-text aggregatable fields', function () { init({ field: 'bytes' }); expect(aggDsl.top_hits._source).to.be('bytes'); - expect(aggDsl.top_hits.docvalue_fields).to.eql([ 'bytes' ]); + expect(aggDsl.top_hits.docvalue_fields).to.eql([ { field: 'bytes', format: 'use_field_mapping' } ]); }); it('requests just source for aggregatable text fields', function () { diff --git a/src/ui/public/agg_types/controls/number_interval.html b/src/ui/public/agg_types/controls/number_interval.html index f0283d614cf16..a281875531d11 100644 --- a/src/ui/public/agg_types/controls/number_interval.html +++ b/src/ui/public/agg_types/controls/number_interval.html @@ -6,15 +6,6 @@ position="'right'" content="'Interval will be automatically scaled in the event that the provided value creates more buckets than specified by Advanced Setting\'s histogram:maxBars'" > - - +
+ {{editorConfig.interval.help}} +
diff --git a/src/ui/public/agg_types/controls/time_interval.html b/src/ui/public/agg_types/controls/time_interval.html index 1da3e3ddcd39b..4a980f39c727c 100644 --- a/src/ui/public/agg_types/controls/time_interval.html +++ b/src/ui/public/agg_types/controls/time_interval.html @@ -9,6 +9,7 @@ >