diff --git a/.i18nrc.json b/.i18nrc.json new file mode 100644 index 00000000000000..d45b944ff0348a --- /dev/null +++ b/.i18nrc.json @@ -0,0 +1,14 @@ +{ + "paths": { + "kbn": "src/core_plugins/kibana", + "common.server": "src/server", + "common.ui": "src/ui", + "xpack.idxMgmt": "xpack/plugins/index_management" + }, + "exclude": [ + "src/ui/ui_render/bootstrap/app_bootstrap.js", + "src/ui/ui_render/ui_render_mixin.js", + "x-pack/plugins/monitoring/public/components/cluster/overview/alerts_panel.js", + "x-pack/plugins/monitoring/public/directives/alerts/index.js" + ] +} diff --git a/docs/apm/getting-started.asciidoc b/docs/apm/getting-started.asciidoc index 2df3d5efa5f760..684c420c1e5f55 100644 --- a/docs/apm/getting-started.asciidoc +++ b/docs/apm/getting-started.asciidoc @@ -16,12 +16,7 @@ configuration is required. If you also use Elastic Stack for logging and server-level metrics, you can optionally import the APM dashboards that come with the APM Server. You can use these APM-specific visualizations to correlate APM data with other data sources. -To get the dashboards, run the following command on the APM server: - -[source,shell] ----------------------------------------------------------- -./apm-server setup ----------------------------------------------------------- +To get the dashboards, click the "Load Kibana objects" button at the bottom of the Getting Started guides for APM in Kibana. For more setup information, see {apm-get-started-ref}/index.html[Getting Started with APM]. diff --git a/docs/management/dashboard_only_mode/index.asciidoc b/docs/management/dashboard_only_mode/index.asciidoc index 55f24e72b109c4..e9acad28a85619 100644 --- a/docs/management/dashboard_only_mode/index.asciidoc +++ b/docs/management/dashboard_only_mode/index.asciidoc @@ -3,7 +3,11 @@ == Kibana Dashboard Only Mode If {security} is enabled, you can use the `kibana_dashboard_only_user` built-in role to limit -what users see when they log in to {kib}. +what users see when they log in to {kib}. The `kibana_dashboard_only_user` role is +preconfigured with read-only permissions to {kib}. + +IMPORTANT: You must also assign roles that grant the user appropriate access to the data indices. +For information on roles and privileges, see {xpack-ref}/authorization.html[User Authorization]. Users assigned this role are only able to see the Dashboard app in the navigation pane. When users open a dashboard, they will have a limited visual experience. @@ -13,12 +17,7 @@ All edit and create controls are hidden. image:management/dashboard_only_mode/images/view_only_dashboard.png["View Only Dashboard"] To assign this role, go to *Management > Security > Users*, add or edit -a user, and add the `kibana_dashboard_only_user` role. You must assign roles -that grant the user appropriate data access. For information on roles -and privileges, see {xpack-ref}/authorization.html[User Authorization]. - -The `kibana_dashboard_only_user` role is -preconfigured with read-only permissions to {kib}. +a user, and add the `kibana_dashboard_only_user` role. IMPORTANT: If you assign users the `kibana_dashboard_only_user` role, along with a role with write permissions to {kib}, they *will* have write access, diff --git a/docs/plugins/known-plugins.asciidoc b/docs/plugins/known-plugins.asciidoc index 179644060d5e31..9b05f58fd06bcc 100644 --- a/docs/plugins/known-plugins.asciidoc +++ b/docs/plugins/known-plugins.asciidoc @@ -28,6 +28,7 @@ This list of plugins is not guaranteed to work on your version of Kibana. Instea * https://github.com/JuanCarniglia/area3d_vis[3D Graph] (JuanCarniglia) * https://github.com/TrumanDu/bmap[Bmap](TrumanDu) - integrated echarts for map visualization * https://github.com/mstoyano/kbn_c3js_vis[C3JS Visualizations] (mstoyano) +* https://github.com/aaronoah/kibana_calendar_vis[Calendar Visualization] (aaronoah) * https://github.com/elo7/cohort[Cohort analysis] (elo7) * https://github.com/DeanF/health_metric_vis[Colored Metric Visualization] (deanf) * https://github.com/JuanCarniglia/dendrogram_vis[Dendrogram] (JuanCarniglia) diff --git a/docs/security/securing-kibana.asciidoc b/docs/security/securing-kibana.asciidoc index e3673a67537880..412d2f5bcf6eb8 100644 --- a/docs/security/securing-kibana.asciidoc +++ b/docs/security/securing-kibana.asciidoc @@ -85,8 +85,9 @@ You can manage privileges on the *Management / Security / Roles* page in {kib}. If you're using the native realm with Basic Authentication, you can assign roles using the *Management / Security / Users* page in {kib} or the -{ref}/security-api.html#security-user-apis[user management APIs]. For example, the following -creates a user named `jacknich` and assigns it the `kibana_user` role: +{ref}/security-api.html#security-user-apis[user management APIs]. For example, +the following creates a user named `jacknich` and assigns it the `kibana_user` +role: [source,js] -------------------------------------------------------------------------------- diff --git a/docs/settings/apm-settings.asciidoc b/docs/settings/apm-settings.asciidoc index b7e3d3c5181da7..fdbbee30ecad75 100644 --- a/docs/settings/apm-settings.asciidoc +++ b/docs/settings/apm-settings.asciidoc @@ -17,12 +17,12 @@ xpack.apm.enabled:: Set to `false` to disabled the APM plugin {kib}. Defaults to xpack.apm.ui.enabled:: Set to `false` to hide the APM plugin {kib} from the menu. Defaults to `true`. -apm_oss.indexPattern:: Index pattern is used for integrations with Machine Learning and Kuery Bar. It must match all apm indices. Defaults to `apm-*`. +apm_oss.indexPattern:: Index pattern is used for integrations with Machine Learning and Kuery Bar. It must match all apm indices. Defaults to `apm-*`. -apm_oss.errorIndices:: Matcher for indices containing error documents. Defaults to `apm-*-error-*`. +apm_oss.errorIndices:: Matcher for indices containing error documents. Defaults to `apm-\*-error-*`. -apm_oss.onboardingIndices:: Matcher for indices containing onboarding documents. Defaults to `apm-*-onboarding-*`. +apm_oss.onboardingIndices:: Matcher for indices containing onboarding documents. Defaults to `apm-\*-onboarding-*`. -apm_oss.spanIndices:: Matcher for indices containing span documents. Defaults to `apm-*-span-*`. +apm_oss.spanIndices:: Matcher for indices containing span documents. Defaults to `apm-\*-span-*`. -apm_oss.transactionIndices:: Matcher for indices containing transaction documents. Defaults to `apm-*-transaction-*`. +apm_oss.transactionIndices:: Matcher for indices containing transaction documents. Defaults to `apm-\*-transaction-*`. diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 0d82fa15d7b06a..1f09e5dc6e2a66 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -157,7 +157,7 @@ The minimum value is 100. `server.ssl.redirectHttpFromPort:`:: Kibana will bind to this port and redirect all http requests to https over the port configured as `server.port`. -`server.ssl.supportedProtocols:`:: *Default: TLSv1, TLSv1.1, TLSv1.2* Supported protocols with versions. Valid protocols: `TLSv1`, `TLSv1.1`, `TLSv1.2` +`server.ssl.supportedProtocols:`:: *Default: TLSv1, TLSv1.1, TLSv1.2* An array of supported protocols with versions. Valid protocols: `TLSv1`, `TLSv1.1`, `TLSv1.2` `status.allowAnonymous:`:: *Default: false* If authentication is enabled, setting this to `true` allows -unauthenticated users to access the Kibana server status API and status page. \ No newline at end of file +unauthenticated users to access the Kibana server status API and status page. diff --git a/package.json b/package.json index 2c8b6f5ee558eb..a34bf062fea0d9 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "url": "https://github.com/elastic/kibana.git" }, "dependencies": { - "@elastic/eui": "3.6.1", + "@elastic/eui": "3.7.0", "@elastic/filesaver": "1.1.2", "@elastic/numeral": "2.3.2", "@elastic/ui-ace": "0.2.3", @@ -310,10 +310,12 @@ "mutation-observer": "^1.0.3", "nock": "8.0.0", "node-sass": "^4.9.0", + "normalize-path": "^3.0.0", "pixelmatch": "4.0.2", "postcss": "^7.0.2", "prettier": "^1.14.0", "proxyquire": "1.7.11", + "regenerate": "^1.4.0", "simple-git": "1.37.0", "sinon": "^5.0.7", "strip-ansi": "^3.0.1", diff --git a/packages/kbn-dev-utils/index.d.ts b/packages/kbn-dev-utils/index.d.ts index efd26bfea07b6f..9d8fd8889b7e5d 100644 --- a/packages/kbn-dev-utils/index.d.ts +++ b/packages/kbn-dev-utils/index.d.ts @@ -17,21 +17,4 @@ * under the License. */ -import { Readable } from 'stream'; - -type LogLevel = 'silent' | 'error' | 'warning' | 'info' | 'debug' | 'verbose'; - -export class ToolingLog extends Readable { - public verbose(...args: any[]): void; - public debug(...args: any[]): void; - public info(...args: any[]): void; - public success(...args: any[]): void; - public warning(...args: any[]): void; - public error(errOrMsg: string | Error): void; - public write(...args: any[]): void; - public indent(spaces: number): void; - public getLevel(): LogLevel; - public setLevel(level: LogLevel): void; -} - -export function createToolingLog(level?: LogLevel): ToolingLog; +export * from './src/tooling_log'; diff --git a/packages/kbn-dev-utils/src/index.js b/packages/kbn-dev-utils/src/index.js index 56e74e59cef414..4e2d1b62a92f71 100644 --- a/packages/kbn-dev-utils/src/index.js +++ b/packages/kbn-dev-utils/src/index.js @@ -18,4 +18,4 @@ */ export { withProcRunner } from './proc_runner'; -export { createToolingLog, pickLevelFromFlags } from './tooling_log'; +export { ToolingLog, ToolingLogTextWriter, pickLevelFromFlags } from './tooling_log'; diff --git a/packages/kbn-dev-utils/src/proc_runner/__tests__/proc.sh b/packages/kbn-dev-utils/src/proc_runner/__tests__/proc.sh deleted file mode 100755 index 5c038cd76807d1..00000000000000 --- a/packages/kbn-dev-utils/src/proc_runner/__tests__/proc.sh +++ /dev/null @@ -1,3 +0,0 @@ -#! /usr/bin/node - -console.log(process.argv.join(' ')); diff --git a/packages/kbn-dev-utils/src/proc_runner/proc.js b/packages/kbn-dev-utils/src/proc_runner/proc.js index 4e2d59acf7c1c8..f473026418109b 100644 --- a/packages/kbn-dev-utils/src/proc_runner/proc.js +++ b/packages/kbn-dev-utils/src/proc_runner/proc.js @@ -82,7 +82,7 @@ export function createProc(name, { cmd, args, cwd, env, stdin, log }) { name = name; lines$ = Rx.merge(observeLines(childProcess.stdout), observeLines(childProcess.stderr)).pipe( - tap(line => log.write(` ${gray('proc')} [${gray(name)}] ${line}`)), + tap(line => log.write(` ${gray('proc')} [${gray(name)}] ${line}`)), share() ); diff --git a/packages/kbn-dev-utils/src/proc_runner/with_proc_runner.test.js b/packages/kbn-dev-utils/src/proc_runner/with_proc_runner.test.js new file mode 100644 index 00000000000000..4d0329de7c32ad --- /dev/null +++ b/packages/kbn-dev-utils/src/proc_runner/with_proc_runner.test.js @@ -0,0 +1,79 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES 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 '../tooling_log'; +import { withProcRunner } from './with_proc_runner'; +import { ProcRunner } from './proc_runner'; + +it('passes proc runner to a function', async () => { + await withProcRunner(new ToolingLog(), proc => { + expect(proc).toBeInstanceOf(ProcRunner); + }); +}); + +it('calls procRunner.teardown() if function returns synchronously', async () => { + let teardownSpy; + await withProcRunner(new ToolingLog(), proc => { + teardownSpy = jest.spyOn(proc, 'teardown'); + }); + + expect(teardownSpy).toHaveBeenCalled(); +}); + +it('calls procRunner.teardown() if function throw synchronous error, and rejects with the error', async () => { + const error = new Error('foo'); + let teardownSpy; + + await expect( + withProcRunner(new ToolingLog(), proc => { + teardownSpy = jest.spyOn(proc, 'teardown'); + throw error; + }) + ).rejects.toThrowError(error); + + expect(teardownSpy).toHaveBeenCalled(); +}); + +it('waits for promise to resolve before tearing down proc', async () => { + let teardownSpy; + + await withProcRunner(new ToolingLog(), async proc => { + await new Promise(resolve => setTimeout(resolve, 500)); + teardownSpy = jest.spyOn(proc, 'teardown'); + }); + + expect(teardownSpy).not.toBe(undefined); + expect(teardownSpy).toHaveBeenCalled(); +}); + +it('waits for promise to reject before tearing down proc and rejecting with the error', async () => { + const error = new Error('foo'); + let teardownSpy; + + await expect( + withProcRunner(new ToolingLog(), async proc => { + await new Promise(resolve => setTimeout(resolve, 500)); + teardownSpy = jest.spyOn(proc, 'teardown'); + throw error; + }) + ).rejects.toThrowError(error); + + expect(teardownSpy).not.toBe(undefined); + expect(teardownSpy).toHaveBeenCalled(); +}); diff --git a/packages/kbn-dev-utils/src/tooling_log/__snapshots__/log_levels.test.js.snap b/packages/kbn-dev-utils/src/tooling_log/__snapshots__/log_levels.test.js.snap new file mode 100644 index 00000000000000..56ad7de8588491 --- /dev/null +++ b/packages/kbn-dev-utils/src/tooling_log/__snapshots__/log_levels.test.js.snap @@ -0,0 +1,91 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`parses valid log levels correctly: debug 1`] = ` +Object { + "flags": Object { + "debug": true, + "error": true, + "info": true, + "silent": true, + "verbose": false, + "warning": true, + }, + "name": "debug", +} +`; + +exports[`parses valid log levels correctly: error 1`] = ` +Object { + "flags": Object { + "debug": false, + "error": true, + "info": false, + "silent": true, + "verbose": false, + "warning": false, + }, + "name": "error", +} +`; + +exports[`parses valid log levels correctly: info 1`] = ` +Object { + "flags": Object { + "debug": false, + "error": true, + "info": true, + "silent": true, + "verbose": false, + "warning": true, + }, + "name": "info", +} +`; + +exports[`parses valid log levels correctly: silent 1`] = ` +Object { + "flags": Object { + "debug": false, + "error": false, + "info": false, + "silent": true, + "verbose": false, + "warning": false, + }, + "name": "silent", +} +`; + +exports[`parses valid log levels correctly: verbose 1`] = ` +Object { + "flags": Object { + "debug": true, + "error": true, + "info": true, + "silent": true, + "verbose": true, + "warning": true, + }, + "name": "verbose", +} +`; + +exports[`parses valid log levels correctly: warning 1`] = ` +Object { + "flags": Object { + "debug": false, + "error": true, + "info": false, + "silent": true, + "verbose": false, + "warning": true, + }, + "name": "warning", +} +`; + +exports[`throws error for invalid levels: bar 1`] = `"Invalid log level \\"bar\\" (expected one of silent,error,warning,info,debug,verbose)"`; + +exports[`throws error for invalid levels: foo 1`] = `"Invalid log level \\"foo\\" (expected one of silent,error,warning,info,debug,verbose)"`; + +exports[`throws error for invalid levels: warn 1`] = `"Invalid log level \\"warn\\" (expected one of silent,error,warning,info,debug,verbose)"`; diff --git a/packages/kbn-dev-utils/src/tooling_log/__snapshots__/tooling_log.test.js.snap b/packages/kbn-dev-utils/src/tooling_log/__snapshots__/tooling_log.test.js.snap new file mode 100644 index 00000000000000..059e3d49c36882 --- /dev/null +++ b/packages/kbn-dev-utils/src/tooling_log/__snapshots__/tooling_log.test.js.snap @@ -0,0 +1,226 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`#debug() sends a msg of type "debug" to each writer with indent and arguments 1`] = ` +Array [ + Array [ + Object { + "args": Array [ + "foo", + "bar", + "baz", + ], + "indent": 0, + "type": "debug", + }, + ], +] +`; + +exports[`#error() sends a msg of type "error" to each writer with indent and arguments 1`] = ` +Array [ + Array [ + Object { + "args": Array [ + [Error: error message], + ], + "indent": 0, + "type": "error", + }, + ], + Array [ + Object { + "args": Array [ + "string message", + ], + "indent": 0, + "type": "error", + }, + ], +] +`; + +exports[`#getWritten$() does not emit msg if all writers return false 1`] = `Array []`; + +exports[`#getWritten$() does not emit msg when no writers 1`] = `Array []`; + +exports[`#getWritten$() emits msg if all writers return true 1`] = ` +Array [ + Object { + "args": Array [ + "foo", + ], + "indent": 0, + "type": "debug", + }, + Object { + "args": Array [ + "bar", + ], + "indent": 0, + "type": "info", + }, + Object { + "args": Array [ + "baz", + ], + "indent": 0, + "type": "verbose", + }, +] +`; + +exports[`#getWritten$() emits msg if some writers return true 1`] = ` +Array [ + Object { + "args": Array [ + "foo", + ], + "indent": 0, + "type": "debug", + }, + Object { + "args": Array [ + "bar", + ], + "indent": 0, + "type": "info", + }, + Object { + "args": Array [ + "baz", + ], + "indent": 0, + "type": "verbose", + }, +] +`; + +exports[`#indent() changes the indent on each written msg 1`] = ` +Array [ + Array [ + Object { + "args": Array [ + "foo", + ], + "indent": 1, + "type": "debug", + }, + ], + Array [ + Object { + "args": Array [ + "bar", + ], + "indent": 3, + "type": "debug", + }, + ], + Array [ + Object { + "args": Array [ + "baz", + ], + "indent": 6, + "type": "debug", + }, + ], + Array [ + Object { + "args": Array [ + "box", + ], + "indent": 4, + "type": "debug", + }, + ], + Array [ + Object { + "args": Array [ + "foo", + ], + "indent": 0, + "type": "debug", + }, + ], +] +`; + +exports[`#info() sends a msg of type "info" to each writer with indent and arguments 1`] = ` +Array [ + Array [ + Object { + "args": Array [ + "foo", + "bar", + "baz", + ], + "indent": 0, + "type": "info", + }, + ], +] +`; + +exports[`#success() sends a msg of type "success" to each writer with indent and arguments 1`] = ` +Array [ + Array [ + Object { + "args": Array [ + "foo", + "bar", + "baz", + ], + "indent": 0, + "type": "success", + }, + ], +] +`; + +exports[`#verbose() sends a msg of type "verbose" to each writer with indent and arguments 1`] = ` +Array [ + Array [ + Object { + "args": Array [ + "foo", + "bar", + "baz", + ], + "indent": 0, + "type": "verbose", + }, + ], +] +`; + +exports[`#warning() sends a msg of type "warning" to each writer with indent and arguments 1`] = ` +Array [ + Array [ + Object { + "args": Array [ + "foo", + "bar", + "baz", + ], + "indent": 0, + "type": "warning", + }, + ], +] +`; + +exports[`#write() sends a msg of type "write" to each writer with indent and arguments 1`] = ` +Array [ + Array [ + Object { + "args": Array [ + "foo", + "bar", + "baz", + ], + "indent": 0, + "type": "write", + }, + ], +] +`; diff --git a/packages/kbn-dev-utils/src/tooling_log/__snapshots__/tooling_log_text_writer.test.js.snap b/packages/kbn-dev-utils/src/tooling_log/__snapshots__/tooling_log_text_writer.test.js.snap new file mode 100644 index 00000000000000..76c018fdb36617 --- /dev/null +++ b/packages/kbn-dev-utils/src/tooling_log/__snapshots__/tooling_log_text_writer.test.js.snap @@ -0,0 +1,178 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`formats %s patterns and indents multi-line messages correctly 1`] = ` +" │ succ foo bar + │ { foo: { bar: { '1': [Array] } }, + │ bar: { bar: { '1': [Array] } } } + │ + │ Infinity +" +`; + +exports[`level:debug/type:debug snapshots: is written 1`] = `true`; + +exports[`level:debug/type:debug snapshots: output 1`] = ` +" debg foo +" +`; + +exports[`level:debug/type:error snapshots: is written 1`] = `true`; + +exports[`level:debug/type:error snapshots: output 1`] = ` +"ERROR foo +" +`; + +exports[`level:debug/type:info snapshots: is written 1`] = `true`; + +exports[`level:debug/type:info snapshots: output 1`] = ` +" info foo +" +`; + +exports[`level:debug/type:success snapshots: is written 1`] = `true`; + +exports[`level:debug/type:success snapshots: output 1`] = ` +" succ foo +" +`; + +exports[`level:debug/type:verbose snapshots: is written 1`] = `false`; + +exports[`level:debug/type:warning snapshots: is written 1`] = `true`; + +exports[`level:debug/type:warning snapshots: output 1`] = ` +" warn foo +" +`; + +exports[`level:error/type:debug snapshots: is written 1`] = `false`; + +exports[`level:error/type:error snapshots: is written 1`] = `true`; + +exports[`level:error/type:error snapshots: output 1`] = ` +"ERROR foo +" +`; + +exports[`level:error/type:info snapshots: is written 1`] = `false`; + +exports[`level:error/type:success snapshots: is written 1`] = `false`; + +exports[`level:error/type:verbose snapshots: is written 1`] = `false`; + +exports[`level:error/type:warning snapshots: is written 1`] = `false`; + +exports[`level:info/type:debug snapshots: is written 1`] = `false`; + +exports[`level:info/type:error snapshots: is written 1`] = `true`; + +exports[`level:info/type:error snapshots: output 1`] = ` +"ERROR foo +" +`; + +exports[`level:info/type:info snapshots: is written 1`] = `true`; + +exports[`level:info/type:info snapshots: output 1`] = ` +" info foo +" +`; + +exports[`level:info/type:success snapshots: is written 1`] = `true`; + +exports[`level:info/type:success snapshots: output 1`] = ` +" succ foo +" +`; + +exports[`level:info/type:verbose snapshots: is written 1`] = `false`; + +exports[`level:info/type:warning snapshots: is written 1`] = `true`; + +exports[`level:info/type:warning snapshots: output 1`] = ` +" warn foo +" +`; + +exports[`level:silent/type:debug snapshots: is written 1`] = `false`; + +exports[`level:silent/type:error snapshots: is written 1`] = `false`; + +exports[`level:silent/type:info snapshots: is written 1`] = `false`; + +exports[`level:silent/type:success snapshots: is written 1`] = `false`; + +exports[`level:silent/type:verbose snapshots: is written 1`] = `false`; + +exports[`level:silent/type:warning snapshots: is written 1`] = `false`; + +exports[`level:verbose/type:debug snapshots: is written 1`] = `true`; + +exports[`level:verbose/type:debug snapshots: output 1`] = ` +" debg foo +" +`; + +exports[`level:verbose/type:error snapshots: is written 1`] = `true`; + +exports[`level:verbose/type:error snapshots: output 1`] = ` +"ERROR foo +" +`; + +exports[`level:verbose/type:info snapshots: is written 1`] = `true`; + +exports[`level:verbose/type:info snapshots: output 1`] = ` +" info foo +" +`; + +exports[`level:verbose/type:success snapshots: is written 1`] = `true`; + +exports[`level:verbose/type:success snapshots: output 1`] = ` +" succ foo +" +`; + +exports[`level:verbose/type:verbose snapshots: is written 1`] = `true`; + +exports[`level:verbose/type:verbose snapshots: output 1`] = ` +" sill foo +" +`; + +exports[`level:verbose/type:warning snapshots: is written 1`] = `true`; + +exports[`level:verbose/type:warning snapshots: output 1`] = ` +" warn foo +" +`; + +exports[`level:warning/type:debug snapshots: is written 1`] = `false`; + +exports[`level:warning/type:error snapshots: is written 1`] = `true`; + +exports[`level:warning/type:error snapshots: output 1`] = ` +"ERROR foo +" +`; + +exports[`level:warning/type:info snapshots: is written 1`] = `false`; + +exports[`level:warning/type:success snapshots: is written 1`] = `false`; + +exports[`level:warning/type:verbose snapshots: is written 1`] = `false`; + +exports[`level:warning/type:warning snapshots: is written 1`] = `true`; + +exports[`level:warning/type:warning snapshots: output 1`] = ` +" warn foo +" +`; + +exports[`throws error if created with invalid level 1`] = `"Invalid log level \\"foo\\" (expected one of silent,error,warning,info,debug,verbose)"`; + +exports[`throws error if writeTo config is not defined or doesn't have a write method 1`] = `"ToolingLogTextWriter requires the \`writeTo\` option be set to a stream (like process.stdout)"`; + +exports[`throws error if writeTo config is not defined or doesn't have a write method 2`] = `"ToolingLogTextWriter requires the \`writeTo\` option be set to a stream (like process.stdout)"`; diff --git a/packages/kbn-dev-utils/src/tooling_log/__tests__/log.js b/packages/kbn-dev-utils/src/tooling_log/__tests__/log.js deleted file mode 100644 index 071b3c180d0c9f..00000000000000 --- a/packages/kbn-dev-utils/src/tooling_log/__tests__/log.js +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from 'expect.js'; -import Chance from 'chance'; - -import { createConcatStream, createPromiseFromStreams } from '../../streams'; - -import { createToolingLog } from '../tooling_log'; - -const chance = new Chance(); -const capture = (level, block) => { - const log = createToolingLog(level); - block(log); - log.end(); - return createPromiseFromStreams([log, createConcatStream('')]); -}; - -const nothingTest = (logLevel, method) => { - describe(`#${method}(...any)`, () => { - it('logs nothing', async () => { - const output = await capture(logLevel, log => log[method]('foo')); - expect(output).to.be(''); - }); - }); -}; - -const somethingTest = (logLevel, method) => { - describe(`#${method}(...any)`, () => { - it('logs to output stream', async () => { - const output = await capture(logLevel, log => log[method]('foo')); - expect(output).to.contain('foo'); - }); - }); -}; - -describe('utils: createToolingLog(logLevel, output)', () => { - it('is a readable stream', async () => { - const log = createToolingLog('debug'); - log.info('Foo'); - log.info('Bar'); - log.info('Baz'); - log.end(); - - const output = await createPromiseFromStreams([log, createConcatStream('')]); - - expect(output).to.contain('Foo'); - expect(output).to.contain('Bar'); - expect(output).to.contain('Baz'); - }); - - describe('log level', () => { - describe('logLevel=silent', () => { - nothingTest('silent', 'debug'); - nothingTest('silent', 'info'); - nothingTest('silent', 'error'); - }); - describe('logLevel=error', () => { - nothingTest('error', 'debug'); - nothingTest('error', 'info'); - somethingTest('error', 'error'); - }); - describe('logLevel=info', () => { - nothingTest('info', 'debug'); - somethingTest('info', 'info'); - somethingTest('info', 'error'); - }); - describe('logLevel=debug', () => { - somethingTest('debug', 'debug'); - somethingTest('debug', 'info'); - somethingTest('debug', 'error'); - }); - describe('invalid logLevel', () => { - it('throw error', () => { - // avoid the impossibility that a valid level is generated - // by specifying a long length - const level = chance.word({ length: 10 }); - - expect(() => createToolingLog(level)).to.throwError(level); - }); - }); - }); -}); diff --git a/packages/kbn-dev-utils/src/tooling_log/__tests__/log_levels.js b/packages/kbn-dev-utils/src/tooling_log/__tests__/log_levels.js deleted file mode 100644 index cedfde5261a5ba..00000000000000 --- a/packages/kbn-dev-utils/src/tooling_log/__tests__/log_levels.js +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from 'expect.js'; -import Chance from 'chance'; -import { parseLogLevel } from '../log_levels'; - -const chance = new Chance(); - -describe('parseLogLevel(logLevel).flags', () => { - describe('logLevel=silent', () => { - it('produces correct map', () => { - expect(parseLogLevel('silent').flags).to.eql({ - silent: true, - error: false, - warning: false, - info: false, - debug: false, - verbose: false, - }); - }); - }); - - describe('logLevel=error', () => { - it('produces correct map', () => { - expect(parseLogLevel('error').flags).to.eql({ - silent: true, - error: true, - warning: false, - info: false, - debug: false, - verbose: false, - }); - }); - }); - - describe('logLevel=warning', () => { - it('produces correct map', () => { - expect(parseLogLevel('warning').flags).to.eql({ - silent: true, - error: true, - warning: true, - info: false, - debug: false, - verbose: false, - }); - }); - }); - - describe('logLevel=info', () => { - it('produces correct map', () => { - expect(parseLogLevel('info').flags).to.eql({ - silent: true, - error: true, - warning: true, - info: true, - debug: false, - verbose: false, - }); - }); - }); - - describe('logLevel=debug', () => { - it('produces correct map', () => { - expect(parseLogLevel('debug').flags).to.eql({ - silent: true, - error: true, - warning: true, - info: true, - debug: true, - verbose: false, - }); - }); - }); - - describe('logLevel=verbose', () => { - it('produces correct map', () => { - expect(parseLogLevel('verbose').flags).to.eql({ - silent: true, - error: true, - warning: true, - info: true, - debug: true, - verbose: true, - }); - }); - }); - - describe('invalid logLevel', () => { - it('throws error', () => { - // avoid the impossibility that a valid level is generated - // by specifying a long length - const level = chance.word({ length: 10 }); - - expect(() => parseLogLevel(level)).to.throwError(level); - }); - }); -}); diff --git a/packages/kbn-dev-utils/src/tooling_log/index.d.ts b/packages/kbn-dev-utils/src/tooling_log/index.d.ts new file mode 100644 index 00000000000000..88efd6cf913bba --- /dev/null +++ b/packages/kbn-dev-utils/src/tooling_log/index.d.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { ToolingLog } from './tooling_log'; +export { ToolingLogTextWriter, WriterConfig } from './tooling_log_text_writer'; +export { pickLevelFromFlags, LogLevel } from './log_levels'; diff --git a/packages/kbn-dev-utils/src/tooling_log/index.js b/packages/kbn-dev-utils/src/tooling_log/index.js index 9d1f820e39a06d..bd394c9a1315aa 100644 --- a/packages/kbn-dev-utils/src/tooling_log/index.js +++ b/packages/kbn-dev-utils/src/tooling_log/index.js @@ -17,5 +17,6 @@ * under the License. */ -export { createToolingLog } from './tooling_log'; +export { ToolingLog } from './tooling_log'; +export { ToolingLogTextWriter } from './tooling_log_text_writer'; export { pickLevelFromFlags } from './log_levels'; diff --git a/src/core/server/legacy_compat/__tests__/legacy_kbn_server.test.ts b/packages/kbn-dev-utils/src/tooling_log/log_levels.d.ts similarity index 70% rename from src/core/server/legacy_compat/__tests__/legacy_kbn_server.test.ts rename to packages/kbn-dev-utils/src/tooling_log/log_levels.d.ts index 72780e1882023d..64786f8612ddaf 100644 --- a/src/core/server/legacy_compat/__tests__/legacy_kbn_server.test.ts +++ b/packages/kbn-dev-utils/src/tooling_log/log_levels.d.ts @@ -17,15 +17,13 @@ * under the License. */ -import { LegacyKbnServer } from '..'; +export type LogLevel = 'silent' | 'error' | 'warning' | 'info' | 'debug' | 'verbose'; -test('correctly returns `newPlatformProxyListener`.', () => { - const rawKbnServer = { - newPlatform: { - proxyListener: {}, - }, - }; +export interface ParsedLogLevel { + name: LogLevel; + flags: { [key in LogLevel]: boolean }; +} - const legacyKbnServer = new LegacyKbnServer(rawKbnServer); - expect(legacyKbnServer.newPlatformProxyListener).toBe(rawKbnServer.newPlatform.proxyListener); -}); +export function pickLevelFromFlags(flags: { [key: string]: any }): LogLevel; + +export function parseLogLevel(level: LogLevel): ParsedLogLevel; diff --git a/packages/kbn-dev-utils/src/proc_runner/__tests__/with_proc_runner.js b/packages/kbn-dev-utils/src/tooling_log/log_levels.test.js similarity index 52% rename from packages/kbn-dev-utils/src/proc_runner/__tests__/with_proc_runner.js rename to packages/kbn-dev-utils/src/tooling_log/log_levels.test.js index f65d76035bb879..eecc33cc90696d 100644 --- a/packages/kbn-dev-utils/src/proc_runner/__tests__/with_proc_runner.js +++ b/packages/kbn-dev-utils/src/tooling_log/log_levels.test.js @@ -17,26 +17,19 @@ * under the License. */ -import { createToolingLog } from '../../tooling_log'; -import { withProcRunner } from '../with_proc_runner'; +import { parseLogLevel } from './log_levels'; -describe('proc runner', () => { - function runProc({ thing = '', procs }) { - return new Promise(resolve => { - setTimeout(() => { - procs.run('proc', { - cmd: './proc', - args: ['these', 'are', 'words'], - }); - resolve(thing); - }, 500); - }); - } +it('parses valid log levels correctly', () => { + expect(parseLogLevel('silent')).toMatchSnapshot('silent'); + expect(parseLogLevel('error')).toMatchSnapshot('error'); + expect(parseLogLevel('warning')).toMatchSnapshot('warning'); + expect(parseLogLevel('info')).toMatchSnapshot('info'); + expect(parseLogLevel('debug')).toMatchSnapshot('debug'); + expect(parseLogLevel('verbose')).toMatchSnapshot('verbose'); +}); - it('passes procs to a function', async () => { - await withProcRunner(createToolingLog(), async procs => { - await runProc({ procs }); - await procs.stop('proc'); - }); - }); +it('throws error for invalid levels', () => { + expect(() => parseLogLevel('warn')).toThrowErrorMatchingSnapshot('warn'); + expect(() => parseLogLevel('foo')).toThrowErrorMatchingSnapshot('foo'); + expect(() => parseLogLevel('bar')).toThrowErrorMatchingSnapshot('bar'); }); diff --git a/packages/kbn-dev-utils/src/tooling_log/tooling_log.d.ts b/packages/kbn-dev-utils/src/tooling_log/tooling_log.d.ts new file mode 100644 index 00000000000000..e59524edbe0d94 --- /dev/null +++ b/packages/kbn-dev-utils/src/tooling_log/tooling_log.d.ts @@ -0,0 +1,45 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// tslint:disable max-classes-per-file + +import * as Rx from 'rxjs'; + +import { ToolingLogWriter, WriterConfig } from './tooling_log_text_writer'; + +export interface LogMessage { + type: 'verbose' | 'debug' | 'info' | 'success' | 'warning' | 'error' | 'write'; + indent: number; + args: any[]; +} + +export class ToolingLog { + constructor(config?: WriterConfig); + public verbose(...args: any[]): void; + public debug(...args: any[]): void; + public info(...args: any[]): void; + public success(...args: any[]): void; + public warning(...args: any[]): void; + public error(errOrMsg: string | Error): void; + public write(...args: any[]): void; + public indent(spaces: number): void; + public getWriters(): ToolingLogWriter[]; + public setWriters(reporters: ToolingLogWriter[]): void; + public getWritten$(): Rx.Observable; +} diff --git a/packages/kbn-dev-utils/src/tooling_log/tooling_log.js b/packages/kbn-dev-utils/src/tooling_log/tooling_log.js index d306dcfe76a5d6..1b309ac28fc3f6 100644 --- a/packages/kbn-dev-utils/src/tooling_log/tooling_log.js +++ b/packages/kbn-dev-utils/src/tooling_log/tooling_log.js @@ -17,87 +17,85 @@ * under the License. */ -import { format } from 'util'; -import { PassThrough } from 'stream'; +import * as Rx from 'rxjs'; +import { EventEmitter } from 'events'; + +import { ToolingLogTextWriter } from './tooling_log_text_writer'; + +export class ToolingLog extends EventEmitter { + /** + * Create a ToolingLog object + * @param {WriterConfig} writerConfig + */ + constructor(writerConfig) { + super(); + + this._indent = 0; + this._writers = writerConfig ? [new ToolingLogTextWriter(writerConfig)] : []; + this._written$ = new Rx.Subject(); + } -import { magentaBright, yellow, red, blue, green, dim } from 'chalk'; + indent(delta = 0) { + this._indent = Math.max(this._indent + delta, 0); + return this._indent; + } -import { parseLogLevel } from './log_levels'; + verbose(...args) { + this._write('verbose', args); + } -export function createToolingLog(initialLogLevelName = 'silent') { - // current log level (see logLevel.name and logLevel.flags) changed - // with ToolingLog#setLevel(newLogLevelName); - let logLevel = parseLogLevel(initialLogLevelName); + debug(...args) { + this._write('debug', args); + } - // current indentation level, changed with ToolingLog#indent(delta) - let indentString = ''; + info(...args) { + this._write('info', args); + } - class ToolingLog extends PassThrough { - constructor() { - super({ objectMode: true }); - } + success(...args) { + this._write('success', args); + } - verbose(...args) { - if (!logLevel.flags.verbose) return; - this.write(' %s ', magentaBright('sill'), format(...args)); - } + warning(...args) { + this._write('warning', args); + } - debug(...args) { - if (!logLevel.flags.debug) return; - this.write(' %s ', dim('debg'), format(...args)); - } + error(error) { + this._write('error', [error]); + } - info(...args) { - if (!logLevel.flags.info) return; - this.write(' %s ', blue('info'), format(...args)); - } + write(...args) { + this._write('write', args); + } - success(...args) { - if (!logLevel.flags.info) return; - this.write(' %s ', green('succ'), format(...args)); - } + getWriters() { + return this._writers.slice(0); + } - warning(...args) { - if (!logLevel.flags.warning) return; - this.write(' %s ', yellow('warn'), format(...args)); - } + setWriters(writers) { + this._writers = [...writers]; + } - error(err) { - if (!logLevel.flags.error) return; + getWritten$() { + return this._written$.asObservable(); + } - if (typeof err !== 'string' && !(err instanceof Error)) { - err = new Error(`"${err}" thrown`); + _write(type, args) { + const msg = { + type, + indent: this._indent, + args, + }; + + let written = false; + for (const writer of this._writers) { + if (writer.write(msg)) { + written = true; } - - this.write('%s ', red('ERROR'), err.stack || err.message || err); } - indent(delta = 0) { - const width = Math.max(0, indentString.length + delta); - indentString = ' '.repeat(width); - return indentString.length; - } - - getLevel() { - return logLevel.name; - } - - setLevel(newLogLevelName) { - logLevel = parseLogLevel(newLogLevelName); - } - - write(...args) { - format(...args) - .split('\n') - .forEach((line, i) => { - const subLineIndent = i === 0 ? '' : ' '; - const indent = !indentString - ? '' - : indentString.slice(0, -1) + (i === 0 && line[0] === '-' ? '└' : '│'); - super.write(`${indent}${subLineIndent}${line}\n`); - }); + if (written) { + this._written$.next(msg); } } - - return new ToolingLog(); } diff --git a/packages/kbn-dev-utils/src/tooling_log/tooling_log.test.js b/packages/kbn-dev-utils/src/tooling_log/tooling_log.test.js new file mode 100644 index 00000000000000..fb1095caeefa0d --- /dev/null +++ b/packages/kbn-dev-utils/src/tooling_log/tooling_log.test.js @@ -0,0 +1,143 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import * as Rx from 'rxjs'; +import { toArray, takeUntil } from 'rxjs/operators'; + +import { ToolingLog } from './tooling_log'; +import { ToolingLogTextWriter } from './tooling_log_text_writer'; + +it('creates zero writers without a config', () => { + const log = new ToolingLog(); + expect(log.getWriters()).toHaveLength(0); +}); + +it('creates a single writer with a single object', () => { + const log = new ToolingLog({ level: 'warning', writeTo: process.stdout }); + expect(log.getWriters()).toHaveLength(1); + const [writer] = log.getWriters(); + expect(writer.level).toHaveProperty('name', 'warning'); + expect(writer.writeTo).toBe(process.stdout); +}); + +describe('#get/setWriters()', () => { + it('returns/replaces the current writers', () => { + const log = new ToolingLog(); + expect(log.getWriters()).toHaveLength(0); + + log.setWriters([ + new ToolingLogTextWriter({ + level: 'verbose', + writeTo: process.stdout, + }), + new ToolingLogTextWriter({ + level: 'verbose', + writeTo: process.stdout, + }), + ]); + expect(log.getWriters()).toHaveLength(2); + + log.setWriters([]); + expect(log.getWriters()).toHaveLength(0); + }); +}); + +describe('#indent()', () => { + it('changes the indent on each written msg', () => { + const log = new ToolingLog(); + const write = jest.fn(); + log.setWriters([{ write }]); + + log.indent(1); + log.debug('foo'); + log.indent(2); + log.debug('bar'); + log.indent(3); + log.debug('baz'); + log.indent(-2); + log.debug('box'); + log.indent(-Infinity); + log.debug('foo'); + + expect(write.mock.calls).toMatchSnapshot(); + }); +}); + +['verbose', 'debug', 'info', 'success', 'warning', 'error', 'write'].forEach(method => { + describe(`#${method}()`, () => { + it(`sends a msg of type "${method}" to each writer with indent and arguments`, () => { + const log = new ToolingLog(); + const writeA = jest.fn(); + const writeB = jest.fn(); + + log.setWriters([{ write: writeA }, { write: writeB }]); + + if (method === 'error') { + const error = new Error('error message'); + error.stack = '... stack trace ...'; + log.error(error); + log.error('string message'); + } else { + log[method]('foo', 'bar', 'baz'); + } + + expect(writeA.mock.calls).toMatchSnapshot(); + expect(writeA.mock.calls).toEqual(writeB.mock.calls); + }); + }); +}); + +describe('#getWritten$()', () => { + async function testWrittenMsgs(writers) { + const log = new ToolingLog(); + log.setWriters(writers); + + const done$ = new Rx.Subject(); + const promise = log + .getWritten$() + .pipe( + takeUntil(done$), + toArray() + ) + .toPromise(); + + log.debug('foo'); + log.info('bar'); + log.verbose('baz'); + done$.next(); + + expect(await promise).toMatchSnapshot(); + } + + it('does not emit msg when no writers', async () => { + await testWrittenMsgs([]); + }); + + it('emits msg if all writers return true', async () => { + await testWrittenMsgs([{ write: jest.fn(() => true) }, { write: jest.fn(() => true) }]); + }); + + it('emits msg if some writers return true', async () => { + await testWrittenMsgs([{ write: jest.fn(() => true) }, { write: jest.fn(() => false) }]); + }); + + it('does not emit msg if all writers return false', async () => { + await testWrittenMsgs([{ write: jest.fn(() => false) }, { write: jest.fn(() => false) }]); + }); +}); diff --git a/packages/kbn-dev-utils/src/tooling_log/tooling_log_text_writer.d.ts b/packages/kbn-dev-utils/src/tooling_log/tooling_log_text_writer.d.ts new file mode 100644 index 00000000000000..67311c5ceeeecb --- /dev/null +++ b/packages/kbn-dev-utils/src/tooling_log/tooling_log_text_writer.d.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { LogLevel, ParsedLogLevel } from './log_levels'; +import { LogMessage } from './tooling_log'; + +export interface ToolingLogWriter { + write(msg: LogMessage): boolean; +} + +export interface WriteTarget { + write(chunk: string): void; +} + +export interface WriterConfig { + level: LogLevel; + writeTo: WriteTarget; +} + +export class ToolingLogTextWriter implements ToolingLogTextWriter { + public level: ParsedLogLevel; + public writeTo: WriteTarget; + + constructor(config: WriterConfig); + public write(msg: LogMessage): boolean; +} diff --git a/packages/kbn-dev-utils/src/tooling_log/tooling_log_text_writer.js b/packages/kbn-dev-utils/src/tooling_log/tooling_log_text_writer.js new file mode 100644 index 00000000000000..fa0db286874323 --- /dev/null +++ b/packages/kbn-dev-utils/src/tooling_log/tooling_log_text_writer.js @@ -0,0 +1,92 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { format } from 'util'; + +import { magentaBright, yellow, red, blue, green, dim } from 'chalk'; + +import { parseLogLevel } from './log_levels'; + +const PREFIX_INDENT = ' '.repeat(6); +const MSG_PREFIXES = { + verbose: ` ${magentaBright('sill')} `, + debug: ` ${dim('debg')} `, + info: ` ${blue('info')} `, + success: ` ${green('succ')} `, + warning: ` ${yellow('warn')} `, + error: `${red('ERROR')} `, +}; + +function shouldWriteType(level, type) { + if (type === 'write') { + return true; + } + + return Boolean(level.flags[type === 'success' ? 'info' : type]); +} + +function stringifyError(error) { + if (typeof error !== 'string' && !(error instanceof Error)) { + error = new Error(`"${error}" thrown`); + } + + return error.stack || error.message || error; +} + +export class ToolingLogTextWriter { + constructor(config) { + this.level = parseLogLevel(config.level); + this.writeTo = config.writeTo; + + if (!this.writeTo || typeof this.writeTo.write !== 'function') { + throw new Error( + 'ToolingLogTextWriter requires the `writeTo` option be set to a stream (like process.stdout)' + ); + } + } + + write({ type, indent, args }) { + if (!shouldWriteType(this.level, type)) { + return false; + } + + const txt = type === 'error' ? stringifyError(args[0]) : format(...args); + const prefix = MSG_PREFIXES[type] || ''; + + (prefix + txt).split('\n').forEach((line, i) => { + let lineIndent = ''; + + if (indent > 0) { + // if we are indenting write some spaces followed by a symbol + lineIndent += ' '.repeat(indent - 1); + lineIndent += line.startsWith('-') ? '└' : '│'; + } + + if (line && prefix && i > 0) { + // apply additional indentation to lines after + // the first if this message gets a prefix + lineIndent += PREFIX_INDENT; + } + + this.writeTo.write(`${lineIndent}${line}\n`); + }); + + return true; + } +} diff --git a/packages/kbn-dev-utils/src/tooling_log/tooling_log_text_writer.test.js b/packages/kbn-dev-utils/src/tooling_log/tooling_log_text_writer.test.js new file mode 100644 index 00000000000000..ed859757c0e76e --- /dev/null +++ b/packages/kbn-dev-utils/src/tooling_log/tooling_log_text_writer.test.js @@ -0,0 +1,98 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ToolingLogTextWriter } from './tooling_log_text_writer'; + +it('throws error if created with invalid level', () => { + expect( + () => + new ToolingLogTextWriter({ + level: 'foo', + }) + ).toThrowErrorMatchingSnapshot(); +}); + +it("throws error if writeTo config is not defined or doesn't have a write method", () => { + expect(() => { + new ToolingLogTextWriter({ + level: 'verbose', + writeTo: null, + }); + }).toThrowErrorMatchingSnapshot(); + + expect(() => { + new ToolingLogTextWriter({ + level: 'verbose', + writeTo: 'foo', + }); + }).toThrowErrorMatchingSnapshot(); +}); + +const levels = ['silent', 'verbose', 'debug', 'info', 'warning', 'error']; +const types = ['verbose', 'debug', 'info', 'warning', 'error', 'success']; +for (const level of levels) { + for (const type of types) { + it(`level:${level}/type:${type} snapshots`, () => { + const write = jest.fn(); + const writer = new ToolingLogTextWriter({ + level, + writeTo: { + write, + }, + }); + + const written = writer.write({ + type: type, + indent: 0, + args: ['foo'], + }); + + expect(written).toMatchSnapshot('is written'); + + if (written) { + const output = write.mock.calls.reduce((acc, chunk) => `${acc}${chunk}`, ''); + expect(output).toMatchSnapshot('output'); + } + }); + } +} + +it('formats %s patterns and indents multi-line messages correctly', () => { + const write = jest.fn(); + const writer = new ToolingLogTextWriter({ + level: 'debug', + writeTo: { + write, + }, + }); + + writer.write({ + type: 'success', + indent: 10, + args: [ + '%s\n%O\n\n%d', + 'foo bar', + { foo: { bar: { 1: [1, 2, 3] } }, bar: { bar: { 1: [1, 2, 3] } } }, + Infinity, + ], + }); + + const output = write.mock.calls.reduce((acc, chunk) => `${acc}${chunk}`, ''); + expect(output).toMatchSnapshot(); +}); diff --git a/packages/kbn-dev-utils/tsconfig.json b/packages/kbn-dev-utils/tsconfig.json index 3604f1004cf6c4..cb0d38840fffc1 100644 --- a/packages/kbn-dev-utils/tsconfig.json +++ b/packages/kbn-dev-utils/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.json", "include": [ - "index.d.ts" + "index.d.ts", + "src/**/*.d.ts" ], } diff --git a/packages/kbn-es/src/integration_tests/cluster.test.js b/packages/kbn-es/src/integration_tests/cluster.test.js index f13bf1954175e3..e74f94d92beee0 100644 --- a/packages/kbn-es/src/integration_tests/cluster.test.js +++ b/packages/kbn-es/src/integration_tests/cluster.test.js @@ -17,7 +17,7 @@ * under the License. */ -const { createToolingLog } = require('@kbn/dev-utils'); +const { ToolingLog } = require('@kbn/dev-utils'); const execa = require('execa'); const { Cluster } = require('../cluster'); const { installSource, installSnapshot, installArchive } = require('../install'); @@ -30,9 +30,7 @@ jest.mock('../install', () => ({ jest.mock('execa', () => jest.fn()); -const log = createToolingLog('verbose'); -log.onData = jest.fn(); -log.on('data', log.onData); +const log = new ToolingLog(); function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); diff --git a/packages/kbn-es/src/utils/log.js b/packages/kbn-es/src/utils/log.js index bb2e96b8c089c2..5c4bdbcb6c0da5 100644 --- a/packages/kbn-es/src/utils/log.js +++ b/packages/kbn-es/src/utils/log.js @@ -17,9 +17,11 @@ * under the License. */ -const { createToolingLog } = require('@kbn/dev-utils'); +const { ToolingLog } = require('@kbn/dev-utils'); -const log = createToolingLog('verbose'); -log.pipe(process.stdout); +const log = new ToolingLog({ + level: 'verbose', + writeTo: process.stdout, +}); exports.log = log; diff --git a/packages/kbn-test/src/functional_tests/cli/run_tests/args.js b/packages/kbn-test/src/functional_tests/cli/run_tests/args.js index 5fdc738c4517a4..c03649a1167ebf 100644 --- a/packages/kbn-test/src/functional_tests/cli/run_tests/args.js +++ b/packages/kbn-test/src/functional_tests/cli/run_tests/args.js @@ -18,7 +18,7 @@ */ import dedent from 'dedent'; -import { createToolingLog, pickLevelFromFlags } from '@kbn/dev-utils'; +import { ToolingLog, pickLevelFromFlags } from '@kbn/dev-utils'; const options = { help: { desc: 'Display this menu and exit.' }, @@ -99,9 +99,10 @@ export function processOptions(userOptions, defaultConfigPaths) { } function createLogger() { - const log = createToolingLog(pickLevelFromFlags(userOptions)); - log.pipe(process.stdout); - return log; + return new ToolingLog({ + level: pickLevelFromFlags(userOptions), + writeTo: process.stdout, + }); } return { diff --git a/packages/kbn-test/src/functional_tests/cli/start_servers/args.js b/packages/kbn-test/src/functional_tests/cli/start_servers/args.js index bc3d97ab0641f7..ca62aff2649f8a 100644 --- a/packages/kbn-test/src/functional_tests/cli/start_servers/args.js +++ b/packages/kbn-test/src/functional_tests/cli/start_servers/args.js @@ -18,7 +18,7 @@ */ import dedent from 'dedent'; -import { createToolingLog, pickLevelFromFlags } from '@kbn/dev-utils'; +import { ToolingLog, pickLevelFromFlags } from '@kbn/dev-utils'; const options = { help: { desc: 'Display this menu and exit.' }, @@ -86,9 +86,10 @@ export function processOptions(userOptions, defaultConfigPath) { } function createLogger() { - const log = createToolingLog(pickLevelFromFlags(userOptions)); - log.pipe(process.stdout); - return log; + return new ToolingLog({ + level: pickLevelFromFlags(userOptions), + writeTo: process.stdout, + }); } return { diff --git a/packages/kbn-test/src/functional_tests/lib/run_cli.js b/packages/kbn-test/src/functional_tests/lib/run_cli.js index 9bb2b7c05bb237..56f6f36f5388f7 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_cli.js +++ b/packages/kbn-test/src/functional_tests/lib/run_cli.js @@ -22,6 +22,14 @@ import { inspect } from 'util'; import chalk from 'chalk'; import getopts from 'getopts'; +export class CliError extends Error { + constructor(message, exitCode = 1) { + super(message); + this.exitCode = exitCode; + Error.captureStackTrace(this, CliError); + } +} + export async function runCli(getHelpText, run) { try { const userOptions = getopts(process.argv.slice(2)) || {}; @@ -39,20 +47,25 @@ export async function runCli(getHelpText, run) { console.log(); console.log(chalk.red(error.message)); - // first line in the stack trace is the message, skip it as we log it directly and color it red - if (error.stack) { - console.log( - error.stack - .split('\n') - .slice(1) - .join('\n') - ); - } else { - console.log(' (no stack trace)'); + // CliError is a special error class that indicates that the error is produced as a part + // of using the CLI, and does not need a stack trace to make sense, so we skip the stack + // trace logging if the error thrown is an instance of this class + if (!(error instanceof CliError)) { + // first line in the stack trace is the message, skip it as we log it directly and color it red + if (error.stack) { + console.log( + error.stack + .split('\n') + .slice(1) + .join('\n') + ); + } else { + console.log(' (no stack trace)'); + } } console.log(); - process.exit(1); + process.exit(error.exitCode || 1); } } diff --git a/packages/kbn-test/src/functional_tests/lib/run_ftr.js b/packages/kbn-test/src/functional_tests/lib/run_ftr.js index 90a3289f72028f..6fe02ec39d9a44 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_ftr.js +++ b/packages/kbn-test/src/functional_tests/lib/run_ftr.js @@ -17,33 +17,26 @@ * under the License. */ -import { KIBANA_FTR_SCRIPT, PROJECT_ROOT } from './paths'; +import { createFunctionalTestRunner } from '../../../../../src/functional_test_runner'; +import { CliError } from './run_cli'; -export async function runFtr({ - procs, - configPath, - cwd = PROJECT_ROOT, - options: { log, bail, grep, updateBaselines }, -}) { - const args = [KIBANA_FTR_SCRIPT]; - - if (getLogFlag(log)) args.push(`--${getLogFlag(log)}`); - if (bail) args.push('--bail'); - if (configPath) args.push('--config', configPath); - if (grep) args.push('--grep', grep); - if (updateBaselines) args.push('--updateBaselines'); - - await procs.run('ftr', { - cmd: 'node', - args, - cwd, - wait: true, +export async function runFtr({ configPath, options: { log, bail, grep, updateBaselines } }) { + const ftr = createFunctionalTestRunner({ + log, + configFile: configPath, + configOverrides: { + mochaOpts: { + bail: !!bail, + grep, + }, + updateBaselines, + }, }); -} - -function getLogFlag(log) { - const level = log.getLevel(); - if (level === 'info') return null; - return level === 'error' ? 'quiet' : level; + const failureCount = await ftr.run(); + if (failureCount > 0) { + throw new CliError( + `${failureCount} functional test ${failureCount === 1 ? 'failure' : 'failures'}` + ); + } } diff --git a/packages/kbn-test/src/functional_tests/lib/run_kibana_server.js b/packages/kbn-test/src/functional_tests/lib/run_kibana_server.js index e1cd151601497a..26a1509d5c4ecb 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_kibana_server.js +++ b/packages/kbn-test/src/functional_tests/lib/run_kibana_server.js @@ -77,7 +77,10 @@ function filterCliArgs(args) { // Check if original argv has a later setting that overrides // the current val. If so, skip this val. - if (findIndexFrom(args, ++ind, opt => opt.split('=')[0] === val.split('=')[0]) > -1) { + if ( + !allowsDuplicate(val) && + findIndexFrom(args, ++ind, opt => opt.split('=')[0] === val.split('=')[0]) > -1 + ) { return acc; } @@ -96,6 +99,14 @@ function pipe(arr, ...fns) { }, arr); } +/** + * Checks whether a specific parameter is allowed to appear multiple + * times in the Kibana parameters. + */ +function allowsDuplicate(val) { + return ['--plugin-path'].includes(val.split('=')[0]); +} + function isBasePathSettingOverridden(args, val, ind) { const key = val.split('=')[0]; const basePathKeys = ['--no-base-path', '--server.basePath']; diff --git a/packages/kbn-test/src/functional_tests/tasks.js b/packages/kbn-test/src/functional_tests/tasks.js index b6f44ff7e4cdbe..0cdcc77161a60f 100644 --- a/packages/kbn-test/src/functional_tests/tasks.js +++ b/packages/kbn-test/src/functional_tests/tasks.js @@ -91,7 +91,8 @@ export async function startServers(options) { } async function silence(milliseconds, { log }) { - await Rx.fromEvent(log, 'data') + await log + .getWritten$() .pipe( startWith(null), switchMap(() => Rx.timer(milliseconds)), @@ -115,15 +116,7 @@ async function runSingleConfig(configPath, options) { const es = await runElasticsearch({ config, options: opts }); await runKibanaServer({ procs, config, options: opts }); - - // Note: When solving how to incorporate functional_test_runner - // clean this up - await runFtr({ - procs, - configPath, - cwd: process.cwd(), - options: opts, - }); + await runFtr({ configPath, options: opts }); await procs.stop('kibana'); await es.cleanup(); diff --git a/packages/kbn-ui-framework/dist/ui_framework.css b/packages/kbn-ui-framework/dist/ui_framework.css index 409034cd9b6eac..928c41497a547a 100644 --- a/packages/kbn-ui-framework/dist/ui_framework.css +++ b/packages/kbn-ui-framework/dist/ui_framework.css @@ -59,7 +59,8 @@ /* 1 */ outline: none !important; /* 2 */ - box-shadow: 0 0 0 1px #ffffff, 0 0 0 2px #0079a5; + -webkit-box-shadow: 0 0 0 1px #ffffff, 0 0 0 2px #0079a5; + box-shadow: 0 0 0 1px #ffffff, 0 0 0 2px #0079a5; /* 3 */ } /** @@ -78,12 +79,15 @@ main { overflow: hidden; } .kuiActionItem { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; + -webkit-box-pack: justify; -webkit-justify-content: space-between; -ms-flex-pack: justify; justify-content: space-between; } @@ -102,12 +106,15 @@ main { background-color: rgba(0, 0, 0, 0.1); } .kuiBar { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; + -webkit-box-pack: justify; -webkit-justify-content: space-between; -ms-flex-pack: justify; justify-content: space-between; @@ -115,18 +122,22 @@ main { /* 1 */ } .kuiBarSection { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; + -webkit-box-flex: 1; -webkit-flex: 1 1 auto; -ms-flex: 1 1 auto; flex: 1 1 auto; margin-left: 25px; margin-right: 25px; } .kuiBarSection:not(:first-child):not(:last-child):not(:only-child) { + -webkit-box-pack: center; -webkit-justify-content: center; -ms-flex-pack: center; justify-content: center; @@ -135,10 +146,12 @@ main { margin-left: 0; } .kuiBarSection:last-child { margin-right: 0; + -webkit-box-flex: 0; -webkit-flex: 0 1 auto; -ms-flex: 0 1 auto; flex: 0 1 auto; /* 4 */ + -webkit-box-pack: end; -webkit-justify-content: flex-end; -ms-flex-pack: end; justify-content: flex-end; @@ -189,10 +202,12 @@ main { * 1. Solves whitespace problems introduced by inline elements. */ .kuiButton__inner { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; /* 1 */ + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; @@ -252,7 +267,8 @@ main { /* 1 */ outline: none !important; /* 2 */ - box-shadow: 0 0 0 1px #777777, 0 0 0 2px #0079a5; + -webkit-box-shadow: 0 0 0 1px #777777, 0 0 0 2px #0079a5; + box-shadow: 0 0 0 1px #777777, 0 0 0 2px #0079a5; /* 3 */ color: #FFF; } a.theme-dark .kuiButton--basic:not(.kuiButton-isDisabled):focus { @@ -261,7 +277,8 @@ main { /* 1 */ outline: none !important; /* 2 */ - box-shadow: 0 0 0 1px #777777, 0 0 0 2px #0079a5; + -webkit-box-shadow: 0 0 0 1px #777777, 0 0 0 2px #0079a5; + box-shadow: 0 0 0 1px #777777, 0 0 0 2px #0079a5; /* 3 */ color: #FFF; } .theme-dark .kuiButton--basic:enabled:hover { @@ -351,7 +368,8 @@ main { /* 1 */ outline: none !important; /* 2 */ - box-shadow: 0 0 0 1px #ffffff, 0 0 0 2px #ff523c; + -webkit-box-shadow: 0 0 0 1px #ffffff, 0 0 0 2px #ff523c; + box-shadow: 0 0 0 1px #ffffff, 0 0 0 2px #ff523c; /* 3 */ color: #A30000; } a.kuiButton--danger:not(.kuiButton-isDisabled):focus { @@ -360,7 +378,8 @@ main { /* 1 */ outline: none !important; /* 2 */ - box-shadow: 0 0 0 1px #ffffff, 0 0 0 2px #ff523c; + -webkit-box-shadow: 0 0 0 1px #ffffff, 0 0 0 2px #ff523c; + box-shadow: 0 0 0 1px #ffffff, 0 0 0 2px #ff523c; /* 3 */ color: #A30000; } .kuiButton--danger:enabled:hover { @@ -393,7 +412,8 @@ main { /* 1 */ outline: none !important; /* 2 */ - box-shadow: 0 0 0 1px #ffffff, 0 0 0 2px #ffa500; + -webkit-box-shadow: 0 0 0 1px #ffffff, 0 0 0 2px #ffa500; + box-shadow: 0 0 0 1px #ffffff, 0 0 0 2px #ffa500; /* 3 */ color: #FFF; } a.kuiButton--warning:not(.kuiButton-isDisabled):focus { @@ -402,7 +422,8 @@ main { /* 1 */ outline: none !important; /* 2 */ - box-shadow: 0 0 0 1px #ffffff, 0 0 0 2px #ffa500; + -webkit-box-shadow: 0 0 0 1px #ffffff, 0 0 0 2px #ffa500; + box-shadow: 0 0 0 1px #ffffff, 0 0 0 2px #ffa500; /* 3 */ color: #FFF; } .kuiButton--warning:enabled:hover { @@ -499,9 +520,11 @@ main { background-color: rgba(165, 231, 255, 0.5); } .kuiButtonGroup { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; } @@ -529,19 +552,28 @@ main { margin-left: 2px; } .kuiButtonGroup--fullWidth { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; } .kuiButtonGroup--fullWidth > .kuiButton { + -webkit-box-flex: 1; -webkit-flex: 1 1 auto; -ms-flex: 1 1 auto; flex: 1 1 auto; text-align: center; } +.eui-textCenter > .kuiButtonGroup, +.text-center > .kuiButtonGroup { + display: inline-block; } + .kuiCard { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; -webkit-flex-direction: column; -ms-flex-direction: column; flex-direction: column; @@ -551,15 +583,20 @@ main { line-height: 1.5; } .kuiCard__description { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; -webkit-flex-direction: column; -ms-flex-direction: column; flex-direction: column; + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; + -webkit-box-pack: start; -webkit-justify-content: flex-start; -ms-flex-pack: start; justify-content: flex-start; @@ -587,6 +624,7 @@ main { * 2. Offset the spacing between wrapped cards. */ .kuiCardGroup { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; @@ -601,6 +639,7 @@ main { * 2. Use an even margin all around the card so that the spacing is still even when wrapped. */ } .kuiCardGroup .kuiCard { + -webkit-box-flex: 1; -webkit-flex: 1 1 auto; -ms-flex: 1 1 auto; flex: 1 1 auto; @@ -608,6 +647,7 @@ main { margin: 15px; /* 2 */ } .kuiCardGroup .kuiCard .kuiCard__description { + -webkit-box-flex: 1; -webkit-flex: 1 1 auto; -ms-flex: 1 1 auto; flex: 1 1 auto; } @@ -658,15 +698,20 @@ main { right: 0; left: 0; background: rgba(255, 255, 255, 0.7); + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; -webkit-flex-direction: column; -ms-flex-direction: column; flex-direction: column; + -webkit-box-pack: center; -webkit-justify-content: center; -ms-flex-pack: center; justify-content: center; + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; @@ -702,9 +747,11 @@ main { cursor: pointer; } .kuiColorPicker__preview { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; } @@ -713,7 +760,8 @@ main { width: 20px; height: 20px; border-radius: 4px; - box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2); + -webkit-box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2); + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2); display: inline-block; } .kuiColorPicker__emptySwatch svg { @@ -835,6 +883,7 @@ main { width: 256px; position: relative; overflow: hidden; + -webkit-transition: height 150ms cubic-bezier(0.694, 0.0482, 0.335, 1); transition: height 150ms cubic-bezier(0.694, 0.0482, 0.335, 1); border-radius: 4px; } .kuiContextMenu .kuiContextMenu__content { @@ -854,9 +903,11 @@ main { margin-right: 8px; } .kuiContextMenu__itemLayout { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; } @@ -869,7 +920,8 @@ main { * 1. Override global focus style. */ } .kuiContextMenuPanel:focus { - box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; /* 1 */ } .kuiContextMenuPanel.kuiContextMenuPanel-txInLeft { pointer-events: none; @@ -928,7 +980,8 @@ main { .kuiContextMenuPanelTitle:hover .kuiContextMenu__text, .kuiContextMenuPanelTitle:focus .kuiContextMenu__text { text-decoration: underline; } .kuiContextMenuPanelTitle:focus { - box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; /* 1 */ } @-webkit-keyframes kuiContextMenuPanelTxInLeft { @@ -1025,7 +1078,8 @@ main { text-decoration: underline; } .kuiContextMenuItem:focus { background-color: rgba(63, 168, 199, 0.2); - box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; /* 1 */ } .theme-dark .kuiContextMenuItem:focus { background-color: transparent; } @@ -1033,11 +1087,13 @@ main { color: #ffffff; } .kuiContextMenuItem__inner { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; } .kuiContextMenuItem__text { + -webkit-box-flex: 1; -webkit-flex-grow: 1; -ms-flex-positive: 1; flex-grow: 1; } @@ -1056,11 +1112,13 @@ main { text-decoration: none; } .kuiEvent { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; } .kuiEventSymbol { + -webkit-box-flex: 0; -webkit-flex: 0 1 auto; -ms-flex: 0 1 auto; flex: 0 1 auto; @@ -1069,6 +1127,7 @@ main { padding-right: 8px; } .kuiEventBody { + -webkit-box-flex: 1; -webkit-flex: 1 1 0%; -ms-flex: 1 1 0%; flex: 1 1 0%; } @@ -1107,13 +1166,16 @@ main { border-bottom: solid 2px #00A69B; } .kuiFlexGroup { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-align: stretch; -webkit-align-items: stretch; -ms-flex-align: stretch; align-items: stretch; } .kuiFlexGroup .kuiFlexItem { + -webkit-box-flex: 1; -webkit-flex-grow: 1; -ms-flex-positive: 1; flex-grow: 1; } @@ -1139,11 +1201,13 @@ main { margin: 20px; } .kuiFlexGroup--justifyContentSpaceEvenly { + -webkit-box-pack: space-evenly; -webkit-justify-content: space-evenly; -ms-flex-pack: space-evenly; justify-content: space-evenly; } .kuiFlexGroup--justifyContentSpaceBetween { + -webkit-box-pack: justify; -webkit-justify-content: space-between; -ms-flex-pack: justify; justify-content: space-between; } @@ -1154,26 +1218,31 @@ main { justify-content: space-around; } .kuiFlexGroup--justifyContentCenter { + -webkit-box-pack: center; -webkit-justify-content: center; -ms-flex-pack: center; justify-content: center; } .kuiFlexGroup--justifyContentFlexEnd { + -webkit-box-pack: end; -webkit-justify-content: flex-end; -ms-flex-pack: end; justify-content: flex-end; } .kuiFlexGroup--alignItemsStart { + -webkit-box-align: start; -webkit-align-items: flex-start; -ms-flex-align: start; align-items: flex-start; } .kuiFlexGroup--alignItemsCenter { + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; } .kuiFlexGroup--alignItemsEnd { + -webkit-box-align: end; -webkit-align-items: flex-end; -ms-flex-align: end; align-items: flex-end; } @@ -1190,6 +1259,7 @@ main { flex-wrap: wrap; } } .kuiFlexGrid { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; @@ -1198,10 +1268,12 @@ main { flex-wrap: wrap; margin-bottom: 0; } .kuiFlexGrid > .kuiFlexItem { + -webkit-box-flex: 0; -webkit-flex-grow: 0; -ms-flex-positive: 0; flex-grow: 0; } .kuiFlexGrid > .kuiFlexItem.kuiFlexItem--flexGrowZero { + -webkit-box-flex: 0 !important; -webkit-flex-grow: 0 !important; -ms-flex-positive: 0 !important; flex-grow: 0 !important; @@ -1218,6 +1290,7 @@ main { .kuiFlexGrid--gutterSmall { margin: -4px; + -webkit-box-align: stretch; -webkit-align-items: stretch; -ms-flex-align: stretch; align-items: stretch; } @@ -1228,6 +1301,7 @@ main { .kuiFlexGrid--gutterSmall { margin: -4px; + -webkit-box-align: stretch; -webkit-align-items: stretch; -ms-flex-align: stretch; align-items: stretch; } @@ -1238,6 +1312,7 @@ main { .kuiFlexGrid--gutterSmall { margin: -4px; + -webkit-box-align: stretch; -webkit-align-items: stretch; -ms-flex-align: stretch; align-items: stretch; } @@ -1255,6 +1330,7 @@ main { .kuiFlexGrid--gutterMedium { margin: -8px; + -webkit-box-align: stretch; -webkit-align-items: stretch; -ms-flex-align: stretch; align-items: stretch; } @@ -1265,6 +1341,7 @@ main { .kuiFlexGrid--gutterMedium { margin: -8px; + -webkit-box-align: stretch; -webkit-align-items: stretch; -ms-flex-align: stretch; align-items: stretch; } @@ -1275,6 +1352,7 @@ main { .kuiFlexGrid--gutterMedium { margin: -8px; + -webkit-box-align: stretch; -webkit-align-items: stretch; -ms-flex-align: stretch; align-items: stretch; } @@ -1292,6 +1370,7 @@ main { .kuiFlexGrid--gutterLarge { margin: -12px; + -webkit-box-align: stretch; -webkit-align-items: stretch; -ms-flex-align: stretch; align-items: stretch; } @@ -1302,6 +1381,7 @@ main { .kuiFlexGrid--gutterLarge { margin: -12px; + -webkit-box-align: stretch; -webkit-align-items: stretch; -ms-flex-align: stretch; align-items: stretch; } @@ -1312,6 +1392,7 @@ main { .kuiFlexGrid--gutterLarge { margin: -12px; + -webkit-box-align: stretch; -webkit-align-items: stretch; -ms-flex-align: stretch; align-items: stretch; } @@ -1329,6 +1410,7 @@ main { .kuiFlexGrid--gutterXLarge { margin: -16px; + -webkit-box-align: stretch; -webkit-align-items: stretch; -ms-flex-align: stretch; align-items: stretch; } @@ -1339,6 +1421,7 @@ main { .kuiFlexGrid--gutterXLarge { margin: -16px; + -webkit-box-align: stretch; -webkit-align-items: stretch; -ms-flex-align: stretch; align-items: stretch; } @@ -1349,6 +1432,7 @@ main { .kuiFlexGrid--gutterXLarge { margin: -16px; + -webkit-box-align: stretch; -webkit-align-items: stretch; -ms-flex-align: stretch; align-items: stretch; } @@ -1361,10 +1445,13 @@ main { * 1. Allow KuiPanels to expand to fill the item. */ .kuiFlexItem { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; /* 1 */ + -webkit-box-orient: vertical; + -webkit-box-direction: normal; -webkit-flex-direction: column; -ms-flex-direction: column; flex-direction: column; @@ -1375,6 +1462,7 @@ main { */ } .kuiFlexItem.kuiFlexItem--flexGrowZero { /* 1 */ + -webkit-box-flex: 0; -webkit-flex-grow: 0; -ms-flex-positive: 0; flex-grow: 0; @@ -1384,42 +1472,52 @@ main { flex-basis: auto; /* 2 */ } .kuiFlexItem.kuiFlexItem--flexGrow1 { + -webkit-box-flex: 1; -webkit-flex-grow: 1; -ms-flex-positive: 1; flex-grow: 1; } .kuiFlexItem.kuiFlexItem--flexGrow2 { + -webkit-box-flex: 2; -webkit-flex-grow: 2; -ms-flex-positive: 2; flex-grow: 2; } .kuiFlexItem.kuiFlexItem--flexGrow3 { + -webkit-box-flex: 3; -webkit-flex-grow: 3; -ms-flex-positive: 3; flex-grow: 3; } .kuiFlexItem.kuiFlexItem--flexGrow4 { + -webkit-box-flex: 4; -webkit-flex-grow: 4; -ms-flex-positive: 4; flex-grow: 4; } .kuiFlexItem.kuiFlexItem--flexGrow5 { + -webkit-box-flex: 5; -webkit-flex-grow: 5; -ms-flex-positive: 5; flex-grow: 5; } .kuiFlexItem.kuiFlexItem--flexGrow6 { + -webkit-box-flex: 6; -webkit-flex-grow: 6; -ms-flex-positive: 6; flex-grow: 6; } .kuiFlexItem.kuiFlexItem--flexGrow7 { + -webkit-box-flex: 7; -webkit-flex-grow: 7; -ms-flex-positive: 7; flex-grow: 7; } .kuiFlexItem.kuiFlexItem--flexGrow8 { + -webkit-box-flex: 8; -webkit-flex-grow: 8; -ms-flex-positive: 8; flex-grow: 8; } .kuiFlexItem.kuiFlexItem--flexGrow9 { + -webkit-box-flex: 9; -webkit-flex-grow: 9; -ms-flex-positive: 9; flex-grow: 9; } .kuiFlexItem.kuiFlexItem--flexGrow10 { + -webkit-box-flex: 10; -webkit-flex-grow: 10; -ms-flex-positive: 10; flex-grow: 10; } @@ -1477,6 +1575,7 @@ main { /* 2 */ font-size: 10px !important; /* 2 */ + -webkit-transition: background-color 0.1s linear; transition: background-color 0.1s linear; } .kuiCheckBox:before { position: relative; @@ -1486,6 +1585,7 @@ main { font-size: 1em; opacity: 0; color: #FFF; + -webkit-transition: opacity 0.1s linear; transition: opacity 0.1s linear; } .kuiCheckBox:checked { border-color: #0079a5; @@ -1497,7 +1597,8 @@ main { /* 1 */ outline: none !important; /* 2 */ - box-shadow: 0 0 0 1px #ffffff, 0 0 0 2px #0079a5; + -webkit-box-shadow: 0 0 0 1px #ffffff, 0 0 0 2px #0079a5; + box-shadow: 0 0 0 1px #ffffff, 0 0 0 2px #0079a5; /* 3 */ } .kuiCheckBox:disabled { background-color: #D9D9D9 !important; @@ -1510,9 +1611,11 @@ main { background-color: #0079a5; } .kuiCheckBoxLabel { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; @@ -1566,6 +1669,7 @@ main { background-color: #ffffff; border: 1px solid #DEDEDE; border-radius: 4px; + -webkit-transition: border-color 0.1s linear; transition: border-color 0.1s linear; min-height: 30px; /* 1 */ @@ -1612,6 +1716,7 @@ main { background-color: #ffffff; border: 1px solid #DEDEDE; border-radius: 4px; + -webkit-transition: border-color 0.1s linear; transition: border-color 0.1s linear; min-height: 30px; /* 1 */ @@ -1648,7 +1753,8 @@ main { .kuiSelect.kuiSelect-isInvalid { border-color: #A30000; } .kuiSelect:focus { - box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; outline: none; border-color: #0079a5; } @@ -1695,6 +1801,7 @@ main { background-color: #ffffff; border: 1px solid #DEDEDE; border-radius: 4px; + -webkit-transition: border-color 0.1s linear; transition: border-color 0.1s linear; min-height: 30px; /* 1 */ } @@ -1715,7 +1822,8 @@ main { outline: none; border-color: #0079a5; } .kuiTextArea:focus { - box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; outline: none; border-color: #0079a5; } .kuiTextArea.kuiTextArea-isInvalid { @@ -1744,6 +1852,7 @@ main { background-color: #ffffff; border: 1px solid #DEDEDE; border-radius: 4px; + -webkit-transition: border-color 0.1s linear; transition: border-color 0.1s linear; min-height: 30px; /* 1 */ } @@ -1776,15 +1885,18 @@ main { * 1. We may want to put elements in here which have different heights. */ .kuiFieldGroup { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; /* 1 */ } .kuiFieldGroup--alignTop { + -webkit-box-align: start; -webkit-align-items: flex-start; -ms-flex-align: start; align-items: flex-start; } @@ -1795,6 +1907,7 @@ main { margin-left: 10px; } .kuiFieldGroupSection--wide { + -webkit-box-flex: 1; -webkit-flex: 1 1 auto; -ms-flex: 1 1 auto; flex: 1 1 auto; } @@ -1802,6 +1915,7 @@ main { width: 100%; } .kuiGallery { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; @@ -1810,15 +1924,20 @@ main { flex-wrap: wrap; } .kuiGalleryItem { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; -webkit-flex-direction: column; -ms-flex-direction: column; flex-direction: column; + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; + -webkit-box-pack: center; -webkit-justify-content: center; -ms-flex-pack: center; justify-content: center; @@ -1837,12 +1956,15 @@ main { border-color: #00A6FF; } .kuiGalleryItem__image { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; + -webkit-box-pack: center; -webkit-justify-content: center; -ms-flex-pack: center; justify-content: center; @@ -1873,12 +1995,15 @@ main { color: #666; } .kuiHeaderBar { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; + -webkit-box-pack: justify; -webkit-justify-content: space-between; -ms-flex-pack: justify; justify-content: space-between; @@ -1890,18 +2015,22 @@ main { * 1. Align a single section to the left by default. */ .kuiHeaderBarSection { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; + -webkit-box-flex: 1; -webkit-flex: 1 1 auto; -ms-flex: 1 1 auto; flex: 1 1 auto; margin-left: 25px; margin-right: 25px; } .kuiHeaderBarSection:not(:first-child):not(:last-child):not(:only-child) { + -webkit-box-pack: center; -webkit-justify-content: center; -ms-flex-pack: center; justify-content: center; @@ -1910,10 +2039,12 @@ main { margin-left: 0; } .kuiHeaderBarSection:last-child { margin-right: 0; + -webkit-box-flex: 0; -webkit-flex: 0 1 auto; -ms-flex: 0 1 auto; flex: 0 1 auto; /* 4 */ + -webkit-box-pack: end; -webkit-justify-content: flex-end; -ms-flex-pack: end; justify-content: flex-end; @@ -2012,9 +2143,11 @@ main { * 1. Align with first line of title text if it wraps. */ .kuiInfoPanelHeader { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-align: baseline; -webkit-align-items: baseline; -ms-flex-align: baseline; align-items: baseline; @@ -2071,9 +2204,11 @@ main { * a bit. */ .kuiLocalBreadcrumbs { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; @@ -2143,12 +2278,15 @@ main { padding: 0; } .kuiDatePickerNavigation { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; + -webkit-box-pack: justify; -webkit-justify-content: space-between; -ms-flex-pack: justify; justify-content: space-between; @@ -2176,7 +2314,8 @@ main { /* 1 */ outline: none !important; /* 2 */ - box-shadow: 0 0 0 1px #F5F5F5, 0 0 0 2px #0079a5; + -webkit-box-shadow: 0 0 0 1px #F5F5F5, 0 0 0 2px #0079a5; + box-shadow: 0 0 0 1px #F5F5F5, 0 0 0 2px #0079a5; /* 3 */ color: #2d2d2d; /* 1 */ } @@ -2190,7 +2329,8 @@ main { /* 1 */ outline: none !important; /* 2 */ - box-shadow: 0 0 0 1px #525252, 0 0 0 2px #0079a5; + -webkit-box-shadow: 0 0 0 1px #525252, 0 0 0 2px #0079a5; + box-shadow: 0 0 0 1px #525252, 0 0 0 2px #0079a5; /* 3 */ color: #dedede; /* 1 */ } @@ -2238,7 +2378,8 @@ main { /* 1 */ outline: none !important; /* 2 */ - box-shadow: 0 0 0 1px #F5F5F5, 0 0 0 2px #0079a5; + -webkit-box-shadow: 0 0 0 1px #F5F5F5, 0 0 0 2px #0079a5; + box-shadow: 0 0 0 1px #F5F5F5, 0 0 0 2px #0079a5; /* 3 */ color: #2d2d2d; /* 1 */ } @@ -2264,7 +2405,8 @@ main { /* 1 */ outline: none !important; /* 2 */ - box-shadow: 0 0 0 1px #525252, 0 0 0 2px #0079a5; + -webkit-box-shadow: 0 0 0 1px #525252, 0 0 0 2px #0079a5; + box-shadow: 0 0 0 1px #525252, 0 0 0 2px #0079a5; /* 3 */ } .theme-dark .kuiDatePickerRowCellContent.kuiDatePickerRowCellContent-isCurrent { color: #b7e2ea; } @@ -2312,11 +2454,13 @@ main { /* 1 */ } .kuiLocalDropdownPanels { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; } .kuiLocalDropdownPanel { + -webkit-box-flex: 1; -webkit-flex: 1 1 0%; -ms-flex: 1 1 0%; flex: 1 1 0%; } @@ -2345,12 +2489,15 @@ main { margin-bottom: 0; } .kuiLocalDropdownHeader { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; + -webkit-box-pack: justify; -webkit-justify-content: space-between; -ms-flex-pack: justify; justify-content: space-between; @@ -2369,6 +2516,7 @@ main { color: #cecece; } .kuiLocalDropdownHeader__actions { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; } @@ -2430,17 +2578,21 @@ main { color: #9e9e9e; } .kuiLocalMenu { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-align: stretch; -webkit-align-items: stretch; -ms-flex-align: stretch; align-items: stretch; } .kuiLocalMenuItem { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; @@ -2492,12 +2644,16 @@ main { * dropdown. */ .kuiLocalNav { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; -webkit-flex-direction: column; -ms-flex-direction: column; flex-direction: column; + -webkit-box-pack: justify; -webkit-justify-content: space-between; -ms-flex-pack: justify; justify-content: space-between; @@ -2516,12 +2672,15 @@ main { * 1. Allow row to expand if the content is so long that it wraps. */ .kuiLocalNavRow { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-align: stretch; -webkit-align-items: stretch; -ms-flex-align: stretch; align-items: stretch; + -webkit-box-pack: justify; -webkit-justify-content: space-between; -ms-flex-pack: justify; justify-content: space-between; @@ -2529,9 +2688,11 @@ main { /* 1 */ } .kuiLocalNavRow__section { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-align: stretch; -webkit-align-items: stretch; -ms-flex-align: stretch; align-items: stretch; } @@ -2546,12 +2707,14 @@ main { .kuiLocalNavRow--secondary { padding: 0 10px; /* 1 */ + -webkit-box-align: start; -webkit-align-items: flex-start; -ms-flex-align: start; align-items: flex-start; /* 1 */ } .kuiLocalSearch { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; @@ -2571,9 +2734,11 @@ main { background-color: #ffffff; border: 1px solid #DEDEDE; border-radius: 4px; + -webkit-transition: border-color 0.1s linear; transition: border-color 0.1s linear; min-height: 30px; /* 1 */ + -webkit-box-flex: 1; -webkit-flex: 1 1 100%; -ms-flex: 1 1 100%; flex: 1 1 100%; @@ -2597,12 +2762,14 @@ main { outline: none; border-color: #0079a5; } .kuiLocalSearchInput:focus { - box-shadow: none; } + -webkit-box-shadow: none; + box-shadow: none; } .kuiLocalSearchInput.kuiLocalSearchInput-isInvalid { border-color: #e74C3c; } .kuiLocalSearchInput--secondary { height: 30px; + -webkit-box-flex: 0; -webkit-flex: 0 0 auto; -ms-flex: 0 0 auto; flex: 0 0 auto; @@ -2614,9 +2781,11 @@ main { border-right-color: #333333; } .kuiLocalSearchAssistedInput { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-flex: 1; -webkit-flex: 1 1 100%; -ms-flex: 1 1 100%; flex: 1 1 100%; @@ -2648,6 +2817,7 @@ main { background-color: #ffffff; border: 1px solid #DEDEDE; border-radius: 4px; + -webkit-transition: border-color 0.1s linear; transition: border-color 0.1s linear; min-height: 30px; /* 1 */ @@ -2702,7 +2872,8 @@ main { /* 1 */ outline: none !important; /* 2 */ - box-shadow: 0 0 0 1px #D9D9D9, 0 0 0 2px #0079a5; + -webkit-box-shadow: 0 0 0 1px #D9D9D9, 0 0 0 2px #0079a5; + box-shadow: 0 0 0 1px #D9D9D9, 0 0 0 2px #0079a5; /* 3 */ } .theme-dark .kuiLocalSearchButton { color: #ffffff; @@ -2712,16 +2883,19 @@ main { /* 1 */ outline: none !important; /* 2 */ - box-shadow: 0 0 0 1px #333333, 0 0 0 2px #0079a5; + -webkit-box-shadow: 0 0 0 1px #333333, 0 0 0 2px #0079a5; + box-shadow: 0 0 0 1px #333333, 0 0 0 2px #0079a5; /* 3 */ } /** * 1. We want the bottom border on selected tabs to be flush with the bottom of the container. */ .kuiLocalTabs { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-align: end; -webkit-align-items: flex-end; -ms-flex-align: end; align-items: flex-end; @@ -2766,9 +2940,11 @@ main { color: #dedede; } .kuiLocalTitle { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; @@ -2832,7 +3008,8 @@ main { /* 1 */ outline: none !important; /* 2 */ - box-shadow: 0 0 0 1px #ffffff, 0 0 0 2px #0079a5; + -webkit-box-shadow: 0 0 0 1px #ffffff, 0 0 0 2px #0079a5; + box-shadow: 0 0 0 1px #ffffff, 0 0 0 2px #0079a5; /* 3 */ } .kuiMenuButton--iconText .kuiMenuButton__icon:first-child { @@ -2896,10 +3073,12 @@ main { /* 1 */ outline: none !important; /* 2 */ - box-shadow: 0 0 0 1px #ffffff, 0 0 0 2px #ff523c; + -webkit-box-shadow: 0 0 0 1px #ffffff, 0 0 0 2px #ff523c; + box-shadow: 0 0 0 1px #ffffff, 0 0 0 2px #ff523c; /* 3 */ } .kuiMenuButtonGroup { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; } @@ -2907,6 +3086,7 @@ main { margin-left: 4px; } .kuiMenuButtonGroup--alignRight { + -webkit-box-pack: end; -webkit-justify-content: flex-end; -ms-flex-pack: end; justify-content: flex-end; } @@ -2929,6 +3109,7 @@ main { color: #191E23; } .kuiMicroButtonGroup { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; } @@ -2942,12 +3123,15 @@ main { left: 0; right: 0; bottom: 0; + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; + -webkit-box-pack: center; -webkit-justify-content: center; -ms-flex-pack: center; justify-content: center; @@ -2955,12 +3139,14 @@ main { background-color: rgba(0, 0, 0, 0.5); } .kuiModal { - box-shadow: 0 16px 16px -8px rgba(0, 0, 0, 0.1); + -webkit-box-shadow: 0 16px 16px -8px rgba(0, 0, 0, 0.1); + box-shadow: 0 16px 16px -8px rgba(0, 0, 0, 0.1); line-height: 1.5; background-color: #FFF; border: 1px solid #D9D9D9; border-radius: 4px; - box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.1); + -webkit-box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.1); + box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.1); z-index: 1001; -webkit-animation: kuiModal 350ms cubic-bezier(0.34, 1.61, 0.7, 1); animation: kuiModal 350ms cubic-bezier(0.34, 1.61, 0.7, 1); } @@ -2973,12 +3159,15 @@ main { min-width: auto; } .kuiModalHeader { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-pack: justify; -webkit-justify-content: space-between; -ms-flex-pack: justify; justify-content: space-between; + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; @@ -3019,9 +3208,11 @@ main { color: #cecece; } .kuiModalFooter { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-pack: end; -webkit-justify-content: flex-end; -ms-flex-pack: end; justify-content: flex-end; @@ -3060,9 +3251,11 @@ main { * 1. Put 10px of space between each child. */ .kuiPager { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; } @@ -3082,16 +3275,21 @@ main { border-radius: 4px; } .kuiPanel--prompt { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; -webkit-flex-direction: column; -ms-flex-direction: column; flex-direction: column; + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; text-align: center; + -webkit-box-pack: center; -webkit-justify-content: center; -ms-flex-pack: center; justify-content: center; @@ -3108,23 +3306,29 @@ main { border-radius: 0; } .kuiPanel--centered { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-pack: center; -webkit-justify-content: center; -ms-flex-pack: center; justify-content: center; + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; } .kuiPanelHeader { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; + -webkit-box-pack: justify; -webkit-justify-content: space-between; -ms-flex-pack: justify; justify-content: space-between; @@ -3138,7 +3342,8 @@ main { /* 1 */ outline: none !important; /* 2 */ - box-shadow: 0 0 0 1px #D9D9D9, 0 0 0 2px #0079a5; + -webkit-box-shadow: 0 0 0 1px #D9D9D9, 0 0 0 2px #0079a5; + box-shadow: 0 0 0 1px #D9D9D9, 0 0 0 2px #0079a5; /* 3 */ } a.kuiPanelHeader .kuiButton:not(.kuiButton-isDisabled):focus { /* 1 */ @@ -3146,14 +3351,16 @@ main { /* 1 */ outline: none !important; /* 2 */ - box-shadow: 0 0 0 1px #D9D9D9, 0 0 0 2px #0079a5; + -webkit-box-shadow: 0 0 0 1px #D9D9D9, 0 0 0 2px #0079a5; + box-shadow: 0 0 0 1px #D9D9D9, 0 0 0 2px #0079a5; /* 3 */ } .kuiPanelHeader .kuiButton--danger:not(a):enabled:focus { z-index: 1; /* 1 */ outline: none !important; /* 2 */ - box-shadow: 0 0 0 1px #D9D9D9, 0 0 0 2px #ff523c; + -webkit-box-shadow: 0 0 0 1px #D9D9D9, 0 0 0 2px #ff523c; + box-shadow: 0 0 0 1px #D9D9D9, 0 0 0 2px #ff523c; /* 3 */ } a.kuiPanelHeader .kuiButton--danger:not(.kuiButton-isDisabled):focus { /* 1 */ @@ -3161,7 +3368,8 @@ main { /* 1 */ outline: none !important; /* 2 */ - box-shadow: 0 0 0 1px #D9D9D9, 0 0 0 2px #ff523c; + -webkit-box-shadow: 0 0 0 1px #D9D9D9, 0 0 0 2px #ff523c; + box-shadow: 0 0 0 1px #D9D9D9, 0 0 0 2px #ff523c; /* 3 */ } .kuiPanelHeader .kuiSelect { border-color: #ffffff; } @@ -3186,18 +3394,22 @@ main { * 1. Undo what barSection mixin does. */ .kuiPanelHeaderSection { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; + -webkit-box-flex: 1; -webkit-flex: 1 1 auto; -ms-flex: 1 1 auto; flex: 1 1 auto; margin-left: 25px; margin-right: 25px; } .kuiPanelHeaderSection:not(:first-child):not(:last-child):not(:only-child) { + -webkit-box-pack: center; -webkit-justify-content: center; -ms-flex-pack: center; justify-content: center; @@ -3206,10 +3418,12 @@ main { margin-left: 0; } .kuiPanelHeaderSection:last-child { margin-right: 0; + -webkit-box-flex: 0; -webkit-flex: 0 1 auto; -ms-flex: 0 1 auto; flex: 0 1 auto; /* 4 */ + -webkit-box-pack: end; -webkit-justify-content: flex-end; -ms-flex-pack: end; justify-content: flex-end; @@ -3230,10 +3444,12 @@ main { padding: 10px; } .kuiPanelSimple { - box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.1); + -webkit-box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.1); + box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.1); background-color: #FFF; border: 1px solid #D9D9D9; border-radius: 4px; + -webkit-box-flex: 1; -webkit-flex-grow: 1; -ms-flex-positive: 1; flex-grow: 1; } @@ -3244,8 +3460,10 @@ main { .kuiPanelSimple.kuiPanelSimple--paddingLarge { padding: 24px; } .kuiPanelSimple.kuiPanelSimple--shadow { - box-shadow: 0 16px 16px -8px rgba(0, 0, 0, 0.1); } + -webkit-box-shadow: 0 16px 16px -8px rgba(0, 0, 0, 0.1); + box-shadow: 0 16px 16px -8px rgba(0, 0, 0, 0.1); } .kuiPanelSimple.kuiPanelSimple--flexGrowZero { + -webkit-box-flex: 0; -webkit-flex-grow: 0; -ms-flex-positive: 0; flex-grow: 0; } @@ -3271,6 +3489,7 @@ main { transform: translateX(-50%) translateY(8px) translateZ(0); -webkit-backface-visibility: hidden; backface-visibility: hidden; + -webkit-transition: opacity cubic-bezier(0.34, 1.61, 0.7, 1) 350ms, visibility cubic-bezier(0.34, 1.61, 0.7, 1) 350ms, margin-top cubic-bezier(0.34, 1.61, 0.7, 1) 350ms; transition: opacity cubic-bezier(0.34, 1.61, 0.7, 1) 350ms, visibility cubic-bezier(0.34, 1.61, 0.7, 1) 350ms, margin-top cubic-bezier(0.34, 1.61, 0.7, 1) 350ms; -webkit-transform-origin: center top; transform-origin: center top; @@ -3339,12 +3558,16 @@ main { color: #ffffff; } .kuiEmptyTablePrompt { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; -webkit-flex-direction: column; -ms-flex-direction: column; flex-direction: column; + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; @@ -3359,9 +3582,11 @@ main { margin-top: 10px; } .kuiStatusText { + display: -webkit-inline-box; display: -webkit-inline-flex; display: -ms-inline-flexbox; display: inline-flex; + -webkit-box-align: baseline; -webkit-align-items: baseline; -ms-flex-align: baseline; align-items: baseline; } @@ -3477,9 +3702,11 @@ main { display: block; opacity: 1; } .kuiTableHeaderCellButton .kuiTableHeaderCell__liner { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; } @@ -3580,6 +3807,7 @@ main { line-height: 1.5; } .kuiTabs { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; @@ -3622,7 +3850,8 @@ main { .kuiTab:active { outline: none !important; /* 1 */ - box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; /* 1 */ } .kuiTab:focus { outline: none; @@ -3703,12 +3932,15 @@ main { /* 1 */ } .kuiToolBar { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; + -webkit-box-pack: justify; -webkit-justify-content: space-between; -ms-flex-pack: justify; justify-content: space-between; @@ -3723,7 +3955,8 @@ main { /* 1 */ outline: none !important; /* 2 */ - box-shadow: 0 0 0 1px #D9D9D9, 0 0 0 2px #0079a5; + -webkit-box-shadow: 0 0 0 1px #D9D9D9, 0 0 0 2px #0079a5; + box-shadow: 0 0 0 1px #D9D9D9, 0 0 0 2px #0079a5; /* 3 */ } a.kuiToolBar .kuiButton:not(.kuiButton-isDisabled):focus { /* 1 */ @@ -3731,14 +3964,16 @@ main { /* 1 */ outline: none !important; /* 2 */ - box-shadow: 0 0 0 1px #D9D9D9, 0 0 0 2px #0079a5; + -webkit-box-shadow: 0 0 0 1px #D9D9D9, 0 0 0 2px #0079a5; + box-shadow: 0 0 0 1px #D9D9D9, 0 0 0 2px #0079a5; /* 3 */ } .kuiToolBar .kuiButton--danger:not(a):enabled:focus { z-index: 1; /* 1 */ outline: none !important; /* 2 */ - box-shadow: 0 0 0 1px #D9D9D9, 0 0 0 2px #ff523c; + -webkit-box-shadow: 0 0 0 1px #D9D9D9, 0 0 0 2px #ff523c; + box-shadow: 0 0 0 1px #D9D9D9, 0 0 0 2px #ff523c; /* 3 */ } a.kuiToolBar .kuiButton--danger:not(.kuiButton-isDisabled):focus { /* 1 */ @@ -3746,7 +3981,8 @@ main { /* 1 */ outline: none !important; /* 2 */ - box-shadow: 0 0 0 1px #D9D9D9, 0 0 0 2px #ff523c; + -webkit-box-shadow: 0 0 0 1px #D9D9D9, 0 0 0 2px #ff523c; + box-shadow: 0 0 0 1px #D9D9D9, 0 0 0 2px #ff523c; /* 3 */ } .kuiToolBar .kuiSelect { border-color: #ffffff; } @@ -3759,18 +3995,22 @@ main { border-color: #0079a5; } .kuiToolBarSection { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; + -webkit-box-flex: 1; -webkit-flex: 1 1 auto; -ms-flex: 1 1 auto; flex: 1 1 auto; margin-left: 25px; margin-right: 25px; } .kuiToolBarSection:not(:first-child):not(:last-child):not(:only-child) { + -webkit-box-pack: center; -webkit-justify-content: center; -ms-flex-pack: center; justify-content: center; @@ -3779,10 +4019,12 @@ main { margin-left: 0; } .kuiToolBarSection:last-child { margin-right: 0; + -webkit-box-flex: 0; -webkit-flex: 0 1 auto; -ms-flex: 0 1 auto; flex: 0 1 auto; /* 4 */ + -webkit-box-pack: end; -webkit-justify-content: flex-end; -ms-flex-pack: end; justify-content: flex-end; @@ -3802,12 +4044,15 @@ main { /* 1 */ } .kuiToolBarFooter { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; + -webkit-box-pack: justify; -webkit-justify-content: space-between; -ms-flex-pack: justify; justify-content: space-between; @@ -3819,18 +4064,22 @@ main { border: 1px solid #D9D9D9; } .kuiToolBarFooterSection { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; + -webkit-box-flex: 1; -webkit-flex: 1 1 auto; -ms-flex: 1 1 auto; flex: 1 1 auto; margin-left: 25px; margin-right: 25px; } .kuiToolBarFooterSection:not(:first-child):not(:last-child):not(:only-child) { + -webkit-box-pack: center; -webkit-justify-content: center; -ms-flex-pack: center; justify-content: center; @@ -3839,10 +4088,12 @@ main { margin-left: 0; } .kuiToolBarFooterSection:last-child { margin-right: 0; + -webkit-box-flex: 0; -webkit-flex: 0 1 auto; -ms-flex: 0 1 auto; flex: 0 1 auto; /* 4 */ + -webkit-box-pack: end; -webkit-justify-content: flex-end; -ms-flex-pack: end; justify-content: flex-end; @@ -3860,14 +4111,17 @@ main { * kuiToolBarSection sibling. */ .kuiToolBarSearch { + display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; margin-left: 25px; margin-right: 25px; + -webkit-box-flex: 1; -webkit-flex: 1 1 auto; -ms-flex: 1 1 auto; flex: 1 1 auto; @@ -3883,6 +4137,7 @@ main { /* 1 */ } .kuiToolBarSearchBox { + -webkit-box-flex: 1; -webkit-flex: 1 1 auto; -ms-flex: 1 1 auto; flex: 1 1 auto; @@ -3913,6 +4168,7 @@ main { border: 1px solid #D9D9D9; line-height: normal; /* 1 */ + -webkit-transition: border-color 0.1s linear; transition: border-color 0.1s linear; } .kuiToolBarSearchBox__input:focus { outline: none; @@ -4005,6 +4261,7 @@ main { .kuiView { background-color: #FFF; + -webkit-box-flex: 1; -webkit-flex: 1 1 auto; -ms-flex: 1 1 auto; flex: 1 1 auto; } diff --git a/packages/kbn-ui-framework/src/components/button/button_group/_button_group.scss b/packages/kbn-ui-framework/src/components/button/button_group/_button_group.scss index 254bf6025b48df..3b7d4fb82fd585 100644 --- a/packages/kbn-ui-framework/src/components/button/button_group/_button_group.scss +++ b/packages/kbn-ui-framework/src/components/button/button_group/_button_group.scss @@ -42,3 +42,10 @@ text-align: center; } } + +// KuiButtonGroup doesn't pass down className, so can't create custom classname +// So this little gem was being applied in Home/components/tutorial +.eui-textCenter > .kuiButtonGroup, +.text-center > .kuiButtonGroup { + display: inline-block; +} diff --git a/scripts/functional_tests.js b/scripts/functional_tests.js index d02b73c97d6f82..9d73923704ce6d 100644 --- a/scripts/functional_tests.js +++ b/scripts/functional_tests.js @@ -21,5 +21,5 @@ require('../src/setup_node_env'); require('@kbn/test').runTestsCli([ require.resolve('../test/functional/config.js'), require.resolve('../test/api_integration/config.js'), - require.resolve('../test/panel_actions/config.js'), + require.resolve('../test/plugin_functional/config.js'), ]); diff --git a/src/core/server/config/__tests__/__mocks__/env.ts b/src/core/server/config/__tests__/__mocks__/env.ts index e86cd9aab77cda..fe33fd32f46489 100644 --- a/src/core/server/config/__tests__/__mocks__/env.ts +++ b/src/core/server/config/__tests__/__mocks__/env.ts @@ -21,33 +21,14 @@ import { EnvOptions } from '../../env'; -interface MockEnvOptions { - config?: string; - kbnServer?: any; - mode?: EnvOptions['mode']['name']; - packageInfo?: Partial; -} - -export function getEnvOptions({ - config, - kbnServer, - mode = 'development', - packageInfo = {}, -}: MockEnvOptions = {}): EnvOptions { +export function getEnvOptions(options: Partial = {}): EnvOptions { return { - config, - kbnServer, - mode: { - dev: mode === 'development', - name: mode, - prod: mode === 'production', - }, - packageInfo: { - branch: 'some-branch', - buildNum: 1, - buildSha: 'some-sha-256', - version: 'some-version', - ...packageInfo, + configs: options.configs || [], + cliArgs: { + dev: true, + ...(options.cliArgs || {}), }, + isDevClusterMaster: + options.isDevClusterMaster !== undefined ? options.isDevClusterMaster : false, }; } diff --git a/src/core/server/config/__tests__/__snapshots__/env.test.ts.snap b/src/core/server/config/__tests__/__snapshots__/env.test.ts.snap new file mode 100644 index 00000000000000..db2917da5406f0 --- /dev/null +++ b/src/core/server/config/__tests__/__snapshots__/env.test.ts.snap @@ -0,0 +1,149 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`correctly creates default environment in dev mode.: env properties 1`] = ` +Env { + "binDir": "/test/cwd/bin", + "cliArgs": Object { + "dev": true, + "someArg": 1, + "someOtherArg": "2", + }, + "configDir": "/test/cwd/config", + "configs": Array [ + "/test/cwd/config/kibana.yml", + ], + "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, + "name": "development", + "prod": false, + }, + "packageInfo": Object { + "branch": "some-branch", + "buildNum": 9007199254740991, + "buildSha": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "version": "some-version", + }, + "staticFilesDir": "/test/cwd/ui", +} +`; + +exports[`correctly creates default environment in prod distributable mode.: env properties 1`] = ` +Env { + "binDir": "/test/cwd/bin", + "cliArgs": Object { + "dev": false, + "someArg": 1, + "someOtherArg": "2", + }, + "configDir": "/test/cwd/config", + "configs": Array [ + "/some/other/path/some-kibana.yml", + ], + "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, + "name": "production", + "prod": true, + }, + "packageInfo": Object { + "branch": "feature-v1", + "buildNum": 100, + "buildSha": "feature-v1-build-sha", + "version": "v1", + }, + "staticFilesDir": "/test/cwd/ui", +} +`; + +exports[`correctly creates default environment in prod non-distributable mode.: env properties 1`] = ` +Env { + "binDir": "/test/cwd/bin", + "cliArgs": Object { + "dev": false, + "someArg": 1, + "someOtherArg": "2", + }, + "configDir": "/test/cwd/config", + "configs": Array [ + "/some/other/path/some-kibana.yml", + ], + "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, + "name": "production", + "prod": true, + }, + "packageInfo": Object { + "branch": "feature-v1", + "buildNum": 9007199254740991, + "buildSha": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "version": "v1", + }, + "staticFilesDir": "/test/cwd/ui", +} +`; + +exports[`correctly creates environment with constructor.: env properties 1`] = ` +Env { + "binDir": "/some/home/dir/bin", + "cliArgs": Object { + "dev": false, + "someArg": 1, + "someOtherArg": "2", + }, + "configDir": "/some/home/dir/config", + "configs": Array [ + "/some/other/path/some-kibana.yml", + ], + "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, + "name": "production", + "prod": true, + }, + "packageInfo": Object { + "branch": "feature-v1", + "buildNum": 100, + "buildSha": "feature-v1-build-sha", + "version": "v1", + }, + "staticFilesDir": "/some/home/dir/ui", +} +`; diff --git a/src/core/server/config/__tests__/config_service.test.ts b/src/core/server/config/__tests__/config_service.test.ts index 1cac0d4ef8d0e3..2dbf99587f3349 100644 --- a/src/core/server/config/__tests__/config_service.test.ts +++ b/src/core/server/config/__tests__/config_service.test.ts @@ -18,8 +18,13 @@ */ /* tslint:disable max-classes-per-file */ + 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 })); + import { schema, Type, TypeOf } from '../schema'; import { ConfigService, ObjectToRawConfigAdapter } from '..'; @@ -161,21 +166,19 @@ test('tracks unhandled paths', async () => { }); test('correctly passes context', async () => { - const config$ = new BehaviorSubject(new ObjectToRawConfigAdapter({ foo: {} })); + mockPackage.raw = { + branch: 'feature-v1', + version: 'v1', + build: { + distributable: true, + number: 100, + sha: 'feature-v1-build-sha', + }, + }; - const env = new Env( - '/kibana', - getEnvOptions({ - mode: 'development', - packageInfo: { - branch: 'feature-v1', - buildNum: 100, - buildSha: 'feature-v1-build-sha', - version: 'v1', - }, - }) - ); + const env = new Env('/kibana', getEnvOptions()); + const config$ = new BehaviorSubject(new ObjectToRawConfigAdapter({ foo: {} })); const configService = new ConfigService(config$, env, logger); const configs = configService.atPath( 'foo', diff --git a/src/core/server/config/__tests__/env.test.ts b/src/core/server/config/__tests__/env.test.ts index 8707bb2f2a2f71..26163c82c84642 100644 --- a/src/core/server/config/__tests__/env.test.ts +++ b/src/core/server/config/__tests__/env.test.ts @@ -29,76 +29,82 @@ jest.mock('path', () => ({ }, })); +const mockPackage = new Proxy({ raw: {} as any }, { get: (obj, prop) => obj.raw[prop] }); +jest.mock('../../../../utils/package_json', () => ({ pkg: mockPackage })); + import { Env } from '../env'; -import { getEnvOptions } from './__mocks__/env'; - -test('correctly creates default environment with empty options.', () => { - const envOptions = getEnvOptions(); - const defaultEnv = Env.createDefault(envOptions); - - expect(defaultEnv.homeDir).toEqual('/test/cwd'); - expect(defaultEnv.configDir).toEqual('/test/cwd/config'); - expect(defaultEnv.corePluginsDir).toEqual('/test/cwd/core_plugins'); - expect(defaultEnv.binDir).toEqual('/test/cwd/bin'); - expect(defaultEnv.logDir).toEqual('/test/cwd/log'); - expect(defaultEnv.staticFilesDir).toEqual('/test/cwd/ui'); - - expect(defaultEnv.getConfigFile()).toEqual('/test/cwd/config/kibana.yml'); - expect(defaultEnv.getLegacyKbnServer()).toBeUndefined(); - expect(defaultEnv.getMode()).toEqual(envOptions.mode); - expect(defaultEnv.getPackageInfo()).toEqual(envOptions.packageInfo); + +test('correctly creates default environment in dev mode.', () => { + mockPackage.raw = { + branch: 'some-branch', + version: 'some-version', + }; + + const defaultEnv = Env.createDefault({ + cliArgs: { dev: true, someArg: 1, someOtherArg: '2' }, + configs: ['/test/cwd/config/kibana.yml'], + isDevClusterMaster: true, + }); + + expect(defaultEnv).toMatchSnapshot('env properties'); }); -test('correctly creates default environment with options overrides.', () => { - const mockEnvOptions = getEnvOptions({ - config: '/some/other/path/some-kibana.yml', - kbnServer: {}, - mode: 'production', - packageInfo: { - branch: 'feature-v1', - buildNum: 100, - buildSha: 'feature-v1-build-sha', - version: 'v1', +test('correctly creates default environment in prod distributable mode.', () => { + mockPackage.raw = { + branch: 'feature-v1', + version: 'v1', + build: { + distributable: true, + number: 100, + sha: 'feature-v1-build-sha', }, + }; + + const defaultEnv = Env.createDefault({ + cliArgs: { dev: false, someArg: 1, someOtherArg: '2' }, + configs: ['/some/other/path/some-kibana.yml'], + isDevClusterMaster: false, }); - const defaultEnv = Env.createDefault(mockEnvOptions); - - expect(defaultEnv.homeDir).toEqual('/test/cwd'); - expect(defaultEnv.configDir).toEqual('/test/cwd/config'); - expect(defaultEnv.corePluginsDir).toEqual('/test/cwd/core_plugins'); - expect(defaultEnv.binDir).toEqual('/test/cwd/bin'); - expect(defaultEnv.logDir).toEqual('/test/cwd/log'); - expect(defaultEnv.staticFilesDir).toEqual('/test/cwd/ui'); - - expect(defaultEnv.getConfigFile()).toEqual(mockEnvOptions.config); - expect(defaultEnv.getLegacyKbnServer()).toBe(mockEnvOptions.kbnServer); - expect(defaultEnv.getMode()).toEqual(mockEnvOptions.mode); - expect(defaultEnv.getPackageInfo()).toEqual(mockEnvOptions.packageInfo); + + expect(defaultEnv).toMatchSnapshot('env properties'); }); -test('correctly creates environment with constructor.', () => { - const mockEnvOptions = getEnvOptions({ - config: '/some/other/path/some-kibana.yml', - mode: 'production', - packageInfo: { - branch: 'feature-v1', - buildNum: 100, - buildSha: 'feature-v1-build-sha', - version: 'v1', +test('correctly creates default environment in prod non-distributable mode.', () => { + mockPackage.raw = { + branch: 'feature-v1', + version: 'v1', + build: { + distributable: false, + number: 100, + sha: 'feature-v1-build-sha', }, + }; + + const defaultEnv = Env.createDefault({ + cliArgs: { dev: false, someArg: 1, someOtherArg: '2' }, + configs: ['/some/other/path/some-kibana.yml'], + isDevClusterMaster: false, }); - const defaultEnv = new Env('/some/home/dir', mockEnvOptions); + expect(defaultEnv).toMatchSnapshot('env properties'); +}); - expect(defaultEnv.homeDir).toEqual('/some/home/dir'); - expect(defaultEnv.configDir).toEqual('/some/home/dir/config'); - expect(defaultEnv.corePluginsDir).toEqual('/some/home/dir/core_plugins'); - expect(defaultEnv.binDir).toEqual('/some/home/dir/bin'); - expect(defaultEnv.logDir).toEqual('/some/home/dir/log'); - expect(defaultEnv.staticFilesDir).toEqual('/some/home/dir/ui'); +test('correctly creates environment with constructor.', () => { + mockPackage.raw = { + branch: 'feature-v1', + version: 'v1', + build: { + distributable: true, + number: 100, + sha: 'feature-v1-build-sha', + }, + }; + + const env = new Env('/some/home/dir', { + cliArgs: { dev: false, someArg: 1, someOtherArg: '2' }, + configs: ['/some/other/path/some-kibana.yml'], + isDevClusterMaster: false, + }); - expect(defaultEnv.getConfigFile()).toEqual(mockEnvOptions.config); - expect(defaultEnv.getLegacyKbnServer()).toBeUndefined(); - expect(defaultEnv.getMode()).toEqual(mockEnvOptions.mode); - expect(defaultEnv.getPackageInfo()).toEqual(mockEnvOptions.packageInfo); + expect(env).toMatchSnapshot('env properties'); }); diff --git a/src/core/server/config/config_service.ts b/src/core/server/config/config_service.ts index a918bb588e94c4..bd839e51cbcf56 100644 --- a/src/core/server/config/config_service.ts +++ b/src/core/server/config/config_service.ts @@ -138,13 +138,12 @@ export class ConfigService { ); } - const environmentMode = this.env.getMode(); const config = ConfigClass.schema.validate( rawConfig, { - dev: environmentMode.dev, - prod: environmentMode.prod, - ...this.env.getPackageInfo(), + dev: this.env.mode.dev, + prod: this.env.mode.prod, + ...this.env.packageInfo, }, namespace ); diff --git a/src/core/server/config/env.ts b/src/core/server/config/env.ts index 87e4b6567120b0..56d6c1ae94a0ca 100644 --- a/src/core/server/config/env.ts +++ b/src/core/server/config/env.ts @@ -17,10 +17,11 @@ * under the License. */ +import { EventEmitter } from 'events'; import { resolve } from 'path'; import process from 'process'; -import { LegacyKbnServer } from '../legacy_compat'; +import { pkg } from '../../../utils/package_json'; interface PackageInfo { version: string; @@ -36,11 +37,9 @@ interface EnvironmentMode { } export interface EnvOptions { - config?: string; - kbnServer?: any; - packageInfo: PackageInfo; - mode: EnvironmentMode; - [key: string]: any; + configs: string[]; + cliArgs: Record; + isDevClusterMaster: boolean; } export class Env { @@ -58,43 +57,63 @@ export class Env { public readonly staticFilesDir: string; /** - * @internal + * Information about Kibana package (version, build number etc.). */ - constructor(readonly homeDir: string, private readonly options: EnvOptions) { - this.configDir = resolve(this.homeDir, 'config'); - this.corePluginsDir = resolve(this.homeDir, 'core_plugins'); - this.binDir = resolve(this.homeDir, 'bin'); - this.logDir = resolve(this.homeDir, 'log'); - this.staticFilesDir = resolve(this.homeDir, 'ui'); - } + public readonly packageInfo: Readonly; - public getConfigFile() { - const defaultConfigFile = this.getDefaultConfigFile(); - return this.options.config === undefined ? defaultConfigFile : this.options.config; - } + /** + * Mode Kibana currently run in (development or production). + */ + public readonly mode: Readonly; /** * @internal */ - public getLegacyKbnServer(): LegacyKbnServer | undefined { - return this.options.kbnServer; - } + public readonly legacy: EventEmitter; /** - * Gets information about Kibana package (version, build number etc.). + * Arguments provided through command line. */ - public getPackageInfo() { - return this.options.packageInfo; - } + public readonly cliArgs: Readonly>; /** - * Gets mode Kibana currently run in (development or production). + * Paths to the configuration files. */ - public getMode() { - return this.options.mode; - } + public readonly configs: ReadonlyArray; + + /** + * Indicates that this Kibana instance is run as development Node Cluster master. + */ + public readonly isDevClusterMaster: boolean; + + /** + * @internal + */ + constructor(readonly homeDir: string, options: EnvOptions) { + this.configDir = resolve(this.homeDir, 'config'); + this.corePluginsDir = resolve(this.homeDir, 'core_plugins'); + this.binDir = resolve(this.homeDir, 'bin'); + this.logDir = resolve(this.homeDir, 'log'); + this.staticFilesDir = resolve(this.homeDir, 'ui'); + + this.cliArgs = Object.freeze(options.cliArgs); + this.configs = Object.freeze(options.configs); + this.isDevClusterMaster = options.isDevClusterMaster; + + this.mode = Object.freeze({ + dev: this.cliArgs.dev, + name: this.cliArgs.dev ? 'development' : 'production', + prod: !this.cliArgs.dev, + }); + + const isKibanaDistributable = pkg.build && pkg.build.distributable === true; + this.packageInfo = Object.freeze({ + branch: pkg.branch, + buildNum: isKibanaDistributable ? pkg.build.number : Number.MAX_SAFE_INTEGER, + buildSha: isKibanaDistributable ? pkg.build.sha : 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', + version: pkg.version, + }); - private getDefaultConfigFile() { - return resolve(this.configDir, 'kibana.yml'); + this.legacy = new EventEmitter(); } } diff --git a/src/core/server/http/__tests__/__snapshots__/http_server.test.ts.snap b/src/core/server/http/__tests__/__snapshots__/http_server.test.ts.snap new file mode 100644 index 00000000000000..3060d7b4689603 --- /dev/null +++ b/src/core/server/http/__tests__/__snapshots__/http_server.test.ts.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`broadcasts server and connection options to the legacy "channel" 1`] = ` +Object { + "host": "127.0.0.1", + "port": 12345, + "routes": Object { + "cors": undefined, + "payload": Object { + "maxBytes": 1024, + }, + "validate": Object { + "options": Object { + "abortEarly": false, + }, + }, + }, + "state": Object { + "strictHeader": false, + }, +} +`; diff --git a/src/core/server/http/__tests__/http_server.test.ts b/src/core/server/http/__tests__/http_server.test.ts index ed07d8220141bd..7f49d153163a9b 100644 --- a/src/core/server/http/__tests__/http_server.test.ts +++ b/src/core/server/http/__tests__/http_server.test.ts @@ -24,7 +24,6 @@ jest.mock('fs', () => ({ })); import Chance from 'chance'; -import http from 'http'; import supertest from 'supertest'; import { Env } from '../../config'; @@ -36,6 +35,7 @@ import { Router } from '../router'; const chance = new Chance(); +let env: Env; let server: HttpServer; let config: HttpConfig; @@ -51,7 +51,8 @@ beforeEach(() => { ssl: {}, } as HttpConfig; - server = new HttpServer(logger.get(), new Env('/kibana', getEnvOptions())); + env = new Env('/kibana', getEnvOptions()); + server = new HttpServer(logger.get(), env); }); afterEach(async () => { @@ -563,99 +564,21 @@ describe('with defined `redirectHttpFromPort`', () => { }); }); -describe('when run within legacy platform', () => { - let newPlatformProxyListenerMock: any; - beforeEach(() => { - newPlatformProxyListenerMock = { - bind: jest.fn(), - proxy: jest.fn(), - }; - - const kbnServerMock = { - newPlatformProxyListener: newPlatformProxyListenerMock, - }; - - server = new HttpServer( - logger.get(), - new Env('/kibana', getEnvOptions({ kbnServer: kbnServerMock })) - ); - - const router = new Router('/new'); - router.get({ path: '/', validate: false }, async (req, res) => { - return res.ok({ key: 'new-platform' }); - }); - - server.registerRouter(router); - - newPlatformProxyListenerMock.proxy.mockImplementation( - (req: http.IncomingMessage, res: http.ServerResponse) => { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ key: `legacy-platform:${req.url}` })); - } - ); - }); - - test('binds proxy listener to server.', async () => { - expect(newPlatformProxyListenerMock.bind).not.toHaveBeenCalled(); - - await server.start(config); - - expect(newPlatformProxyListenerMock.bind).toHaveBeenCalledTimes(1); - expect(newPlatformProxyListenerMock.bind).toHaveBeenCalledWith( - expect.any((http as any).Server) - ); - expect(newPlatformProxyListenerMock.bind.mock.calls[0][0]).toBe(getServerListener(server)); - }); - - test('forwards request to legacy platform if new one cannot handle it', async () => { - await server.start(config); - - await supertest(getServerListener(server)) - .get('/legacy') - .expect(200) - .then(res => { - expect(res.body).toEqual({ key: 'legacy-platform:/legacy' }); - expect(newPlatformProxyListenerMock.proxy).toHaveBeenCalledTimes(1); - expect(newPlatformProxyListenerMock.proxy).toHaveBeenCalledWith( - expect.any((http as any).IncomingMessage), - expect.any((http as any).ServerResponse) - ); - }); - }); +test('broadcasts server and connection options to the legacy "channel"', async () => { + const onConnectionListener = jest.fn(); + env.legacy.on('connection', onConnectionListener); - test('forwards request to legacy platform and rewrites base path if needed', async () => { - await server.start({ - ...config, - basePath: '/bar', - rewriteBasePath: true, - }); - - await supertest(getServerListener(server)) - .get('/legacy') - .expect(404); + expect(onConnectionListener).not.toHaveBeenCalled(); - await supertest(getServerListener(server)) - .get('/bar/legacy') - .expect(200) - .then(res => { - expect(res.body).toEqual({ key: 'legacy-platform:/legacy' }); - expect(newPlatformProxyListenerMock.proxy).toHaveBeenCalledTimes(1); - expect(newPlatformProxyListenerMock.proxy).toHaveBeenCalledWith( - expect.any((http as any).IncomingMessage), - expect.any((http as any).ServerResponse) - ); - }); + await server.start({ + ...config, + port: 12345, }); - test('do not forward request to legacy platform if new one can handle it', async () => { - await server.start(config); + expect(onConnectionListener).toHaveBeenCalledTimes(1); - await supertest(getServerListener(server)) - .get('/new/') - .expect(200) - .then(res => { - expect(res.body).toEqual({ key: 'new-platform' }); - expect(newPlatformProxyListenerMock.proxy).not.toHaveBeenCalled(); - }); - }); + const [[{ options, server: rawServer }]] = onConnectionListener.mock.calls; + expect(rawServer).toBeDefined(); + expect(rawServer).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 ae02018c435456..21cde147b8ea24 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -45,7 +45,10 @@ export class HttpServer { } public async start(config: HttpConfig) { - this.server = createServer(getServerOptions(config)); + this.log.debug('starting http server'); + + const serverOptions = getServerOptions(config); + this.server = createServer(serverOptions); this.setupBasePathRewrite(this.server, config); @@ -59,32 +62,13 @@ export class HttpServer { } } - const legacyKbnServer = this.env.getLegacyKbnServer(); - if (legacyKbnServer !== undefined) { - legacyKbnServer.newPlatformProxyListener.bind(this.server.listener); - - // We register Kibana proxy middleware right before we start server to allow - // all new platform plugins register their routes, so that `legacyKbnServer` - // handles only requests that aren't handled by the new platform. - this.server.route({ - handler: ({ raw: { req, res } }, responseToolkit) => { - legacyKbnServer.newPlatformProxyListener.proxy(req, res); - return responseToolkit.abandon; - }, - 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, - }, - }, - path: '/{p*}', - }); - } + // 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(); @@ -96,12 +80,13 @@ export class HttpServer { } public async stop() { - this.log.info('stopping http server'); - - if (this.server !== undefined) { - await this.server.stop(); - this.server = undefined; + if (this.server === undefined) { + return; } + + this.log.debug('stopping http server'); + await this.server.stop(); + this.server = undefined; } private setupBasePathRewrite(server: Server, config: HttpConfig) { 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 index 41d10685923dfb..eb58ca8cbc5fdb 100644 --- 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 @@ -1,3 +1,21 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`correctly unbinds from the previous server. 1`] = `"Unhandled \\"error\\" event. (Error: Some error)"`; +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/__tests__/legacy_platform_proxifier.test.ts b/src/core/server/legacy_compat/__tests__/legacy_platform_proxifier.test.ts index a441b81bd171e7..27db835a0ecf3a 100644 --- a/src/core/server/legacy_compat/__tests__/legacy_platform_proxifier.test.ts +++ b/src/core/server/legacy_compat/__tests__/legacy_platform_proxifier.test.ts @@ -17,131 +17,74 @@ * under the License. */ -import { EventEmitter } from 'events'; -import { IncomingMessage, ServerResponse } from 'http'; - -class MockNetServer extends EventEmitter { - public address() { - return { port: 1234, family: 'test-family', address: 'test-address' }; - } - - public getConnections(callback: (error: Error | null, count: number) => void) { - callback(null, 100500); - } -} - -function mockNetServer() { - return new MockNetServer(); -} - -jest.mock('net', () => ({ - createServer: jest.fn(() => mockNetServer()), -})); - -import { createServer } from 'net'; +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__'; +let server: jest.Mocked; +let mockHapiServer: jest.Mocked; let root: any; let proxifier: LegacyPlatformProxifier; beforeEach(() => { + server = { + addListener: jest.fn(), + address: jest + .fn() + .mockReturnValue({ port: 1234, family: 'test-family', address: 'test-address' }), + getConnections: jest.fn(), + } as any; + + mockHapiServer = { listener: server, route: jest.fn() } as any; + root = { - logger: { - get: jest.fn(() => ({ - debug: jest.fn(), - info: jest.fn(), - })), - }, + logger, shutdown: jest.fn(), start: jest.fn(), } as any; - proxifier = new LegacyPlatformProxifier(root); + 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.', () => { - const server = createServer(); - jest.spyOn(server, 'addListener'); - proxifier.bind(server); - - expect(server.addListener).toHaveBeenCalledTimes(4); - for (const eventName of ['listening', 'error', 'clientError', 'connection']) { + 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)); } }); -test('correctly binds to the server and redirects its events.', () => { - const server = createServer(); - proxifier.bind(server); - - const eventsAndListeners = new Map( - ['listening', 'error', 'clientError', 'connection'].map(eventName => { - const listener = jest.fn(); - proxifier.addListener(eventName, listener); - - return [eventName, listener] as [string, () => void]; - }) - ); +test('correctly redirects server events.', () => { + for (const eventName of ['clientError', 'close', 'connection', 'error', 'listening', 'upgrade']) { + expect(server.addListener).toHaveBeenCalledWith(eventName, expect.any(Function)); - for (const [eventName, listener] of eventsAndListeners) { - expect(listener).not.toHaveBeenCalled(); + const listener = jest.fn(); + proxifier.addListener(eventName, listener); // Emit several events, to make sure that server is not being listened with `once`. - server.emit(eventName, 1, 2, 3, 4); - server.emit(eventName, 5, 6, 7, 8); + const [, serverListener] = server.addListener.mock.calls.find( + ([serverEventName]) => serverEventName === eventName + )!; + + serverListener(1, 2, 3, 4); + serverListener(5, 6, 7, 8); expect(listener).toHaveBeenCalledTimes(2); expect(listener).toHaveBeenCalledWith(1, 2, 3, 4); expect(listener).toHaveBeenCalledWith(5, 6, 7, 8); - } -}); - -test('correctly unbinds from the previous server.', () => { - const previousServer = createServer(); - proxifier.bind(previousServer); - - const currentServer = createServer(); - proxifier.bind(currentServer); - - const eventsAndListeners = new Map( - ['listening', 'error', 'clientError', 'connection'].map(eventName => { - const listener = jest.fn(); - proxifier.addListener(eventName, listener); - - return [eventName, listener] as [string, () => void]; - }) - ); - - // Any events from the previous server should not be forwarded. - for (const [eventName, listener] of eventsAndListeners) { - // `error` event is a special case in node, if `error` is emitted, but - // there is no listener for it error will be thrown. - if (eventName === 'error') { - expect(() => - previousServer.emit(eventName, new Error('Some error')) - ).toThrowErrorMatchingSnapshot(); - } else { - previousServer.emit(eventName, 1, 2, 3, 4); - } - - expect(listener).not.toHaveBeenCalled(); - } - - // Only events from the last server should be forwarded. - for (const [eventName, listener] of eventsAndListeners) { - expect(listener).not.toHaveBeenCalled(); - - currentServer.emit(eventName, 1, 2, 3, 4); - expect(listener).toHaveBeenCalledTimes(1); - expect(listener).toHaveBeenCalledWith(1, 2, 3, 4); + proxifier.removeListener(eventName, listener); } }); test('returns `address` from the underlying server.', () => { - expect(proxifier.address()).toBeUndefined(); - - proxifier.bind(createServer()); - expect(proxifier.address()).toEqual({ address: 'test-address', family: 'test-family', @@ -168,33 +111,35 @@ test('`close` shuts down the `root`.', async () => { }); test('returns connection count from the underlying server.', () => { + server.getConnections.mockImplementation(callback => callback(null, 0)); const onGetConnectionsComplete = jest.fn(); - proxifier.getConnections(onGetConnectionsComplete); expect(onGetConnectionsComplete).toHaveBeenCalledTimes(1); expect(onGetConnectionsComplete).toHaveBeenCalledWith(null, 0); onGetConnectionsComplete.mockReset(); - proxifier.bind(createServer()); + server.getConnections.mockImplementation(callback => callback(null, 100500)); proxifier.getConnections(onGetConnectionsComplete); expect(onGetConnectionsComplete).toHaveBeenCalledTimes(1); expect(onGetConnectionsComplete).toHaveBeenCalledWith(null, 100500); }); -test('correctly proxies request and response objects.', () => { +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 request = {} as IncomingMessage; - const response = {} as ServerResponse; - proxifier.proxy(request, response); + const [[{ handler }]] = mockHapiServer.route.mock.calls; + const response = await handler(mockRequest, mockResponseToolkit); - expect(onRequest).toHaveBeenCalledTimes(1); - expect(onRequest).toHaveBeenCalledWith(request, response); + expect(response).toBe(mockResponseToolkit.abandon); + expect(mockResponseToolkit.response).not.toHaveBeenCalled(); - // Check that exactly same objects were passed as event arguments. - expect(onRequest.mock.calls[0][0]).toBe(request); - expect(onRequest.mock.calls[0][1]).toBe(response); + // 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/index.ts b/src/core/server/legacy_compat/index.ts index feed0001ee2161..dcc5c31fbb87d9 100644 --- a/src/core/server/legacy_compat/index.ts +++ b/src/core/server/legacy_compat/index.ts @@ -19,24 +19,18 @@ import { BehaviorSubject } from 'rxjs'; import { map } from 'rxjs/operators'; + /** @internal */ export { LegacyPlatformProxifier } from './legacy_platform_proxifier'; /** @internal */ export { LegacyConfigToRawConfigAdapter, LegacyConfig } from './legacy_platform_config'; -/** @internal */ -export { LegacyKbnServer } from './legacy_kbn_server'; -import { - LegacyConfig, - LegacyConfigToRawConfigAdapter, - LegacyKbnServer, - LegacyPlatformProxifier, -} from '.'; +import { LegacyConfig, LegacyConfigToRawConfigAdapter, LegacyPlatformProxifier } from '.'; import { Env } from '../config'; import { Root } from '../root'; import { BasePathProxyRoot } from '../root/base_path_proxy_root'; -function initEnvironment(rawKbnServer: any) { +function initEnvironment(rawKbnServer: any, isDevClusterMaster = false) { const config: LegacyConfig = rawKbnServer.config; const legacyConfig$ = new BehaviorSubject(config); @@ -45,12 +39,12 @@ function initEnvironment(rawKbnServer: any) { ); const env = Env.createDefault({ - kbnServer: new LegacyKbnServer(rawKbnServer), - // The defaults for the following parameters are retrieved by the legacy - // platform from the command line or from `package.json` and stored in the - // config, so we can borrow these parameters and avoid double parsing. - mode: config.get('env'), - packageInfo: config.get('pkg'), + // 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: config.get('env.dev') }, + isDevClusterMaster, }); return { @@ -71,12 +65,12 @@ export const injectIntoKbnServer = (rawKbnServer: any) => { rawKbnServer.newPlatform = { // Custom HTTP Listener that will be used within legacy platform by HapiJS server. - proxyListener: new LegacyPlatformProxifier(new Root(config$, env)), + proxyListener: new LegacyPlatformProxifier(new Root(config$, env), env), updateConfig, }; }; export const createBasePathProxy = (rawKbnServer: any) => { - const { env, config$ } = initEnvironment(rawKbnServer); + 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 index 8e9198799988a9..8baa156266ef03 100644 --- a/src/core/server/legacy_compat/legacy_platform_proxifier.ts +++ b/src/core/server/legacy_compat/legacy_platform_proxifier.ts @@ -18,16 +18,29 @@ */ import { EventEmitter } from 'events'; -import { IncomingMessage, ServerResponse } from 'http'; 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 = ['listening', 'error', 'clientError', 'connection']; +const ServerEventsToForward = [ + 'clientError', + 'close', + 'connection', + 'error', + 'listening', + 'upgrade', +]; /** * Represents "proxy" between legacy and current platform. @@ -38,7 +51,7 @@ export class LegacyPlatformProxifier extends EventEmitter { private readonly log: Logger; private server?: Server; - constructor(private readonly root: Root) { + constructor(private readonly root: Root, private readonly env: Env) { super(); this.log = root.logger.get('legacy-platform-proxifier'); @@ -56,6 +69,14 @@ export class LegacyPlatformProxifier extends EventEmitter { ] 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) + ); } /** @@ -116,31 +137,36 @@ export class LegacyPlatformProxifier extends EventEmitter { } } - /** - * Binds Http/Https server to the LegacyPlatformProxifier. - * @param server Server to bind to. - */ - public bind(server: Server) { - const oldServer = this.server; - this.server = server; + private onConnection({ server }: ConnectionInfo) { + this.server = server.listener; for (const [eventName, eventHandler] of this.eventHandlers) { - if (oldServer !== undefined) { - oldServer.removeListener(eventName, eventHandler); - } - this.server.addListener(eventName, eventHandler); } - } - /** - * Forwards request and response objects to the legacy platform. - * This method is used whenever new platform doesn't know how to handle the request. - * @param request Native Node request object instance. - * @param response Native Node response object instance. - */ - public proxy(request: IncomingMessage, response: ServerResponse) { - this.log.debug(`Request will be handled by proxy ${request.method}:${request.url}.`); - this.emit('request', request, response); + // 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_plugins/kibana/common/tutorials/filebeat_instructions.js b/src/core_plugins/kibana/common/tutorials/filebeat_instructions.js index 6ba8f0ec697cf2..692074d2ce897b 100644 --- a/src/core_plugins/kibana/common/tutorials/filebeat_instructions.js +++ b/src/core_plugins/kibana/common/tutorials/filebeat_instructions.js @@ -18,98 +18,97 @@ */ import { INSTRUCTION_VARIANT } from './instruction_variant'; -import { - TRYCLOUD_OPTION1, - TRYCLOUD_OPTION2 -} from './onprem_cloud_instructions'; +import { createTrycloudOption1, createTrycloudOption2 } from './onprem_cloud_instructions'; -export const FILEBEAT_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).', + textPre: + 'First time using Filebeat? See the [Getting Started Guide]' + + '({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', 'cd filebeat-{config.kibana.version}-darwin-x86_64/', - ] + ], }, DEB: { title: 'Download and install Filebeat', - textPre: 'First time using Filebeat? See the [Getting Started Guide]' + - '({config.docs.beats.filebeat}/filebeat-getting-started.html).', + textPre: + 'First time using Filebeat? See the [Getting Started Guide]' + + '({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' + '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: + 'Looking for the 32-bit packages? See the [Download page](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).', + textPre: + 'First time using Filebeat? See the [Getting Started Guide]' + + '({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' + '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: + 'Looking for the 32-bit packages? See the [Download page](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.', + 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.', commands: [ 'PS > cd C:\\Program Files\\Filebeat', - 'PS C:\\Program Files\\Filebeat> .\\install-service-filebeat.ps1' + '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: + 'Modify the settings under `output.elasticsearch` in the ' + + '`C:\\Program Files\\Filebeat\\filebeat.yml` file to point to your Elasticsearch installation.', + }, }, 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', - ] + textPre: + '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', - ] + 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'], }, 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', - ], + 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'], }, WINDOWS: { title: 'Start Filebeat', - textPre: 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, ' + - 'omit this command.', + textPre: + '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', - ] - } + ], + }, }, CONFIG: { OSX: { @@ -121,10 +120,11 @@ export const FILEBEAT_INSTRUCTIONS = { ' username: "elastic"', ' password: ""', 'setup.kibana:', - ' host: ""' + ' host: ""', ], - textPost: 'Where `` is the password of the `elastic` user, ' + - '`` is the URL of Elasticsearch, and `` is the URL of Kibana.' + textPost: + 'Where `` is the password of the `elastic` user, ' + + '`` is the URL of Elasticsearch, and `` is the URL of Kibana.', }, DEB: { title: 'Edit the configuration', @@ -135,10 +135,11 @@ export const FILEBEAT_INSTRUCTIONS = { ' username: "elastic"', ' password: ""', 'setup.kibana:', - ' host: ""' + ' host: ""', ], - textPost: 'Where `` is the password of the `elastic` user, ' + - '`` is the URL of Elasticsearch, and `` is the URL of Kibana.' + textPost: + 'Where `` is the password of the `elastic` user, ' + + '`` is the URL of Elasticsearch, and `` is the URL of Kibana.', }, RPM: { title: 'Edit the configuration', @@ -149,120 +150,106 @@ export const FILEBEAT_INSTRUCTIONS = { ' username: "elastic"', ' password: ""', 'setup.kibana:', - ' host: ""' + ' host: ""', ], - textPost: 'Where `` is the password of the `elastic` user, ' + - '`` is the URL of Elasticsearch, and `` is the URL of Kibana.' + textPost: + 'Where `` is the password of the `elastic` user, ' + + '`` is the URL of Elasticsearch, and `` is the URL of Kibana.', }, WINDOWS: { title: 'Edit the configuration', - textPre: 'Modify `C:\\Program Files\\Filebeat\\filebeat.yml` to set the connection information:', + textPre: + 'Modify `C:\\Program Files\\Filebeat\\filebeat.yml` to set the connection information:', commands: [ 'output.elasticsearch:', ' hosts: [""]', ' username: "elastic"', ' password: ""', 'setup.kibana:', - ' host: ""' + ' host: ""', ], - textPost: 'Where `` is the password of the `elastic` user, ' + - '`` is the URL of Elasticsearch, and `` is the URL of Kibana.' - } + textPost: + 'Where `` is the password of the `elastic` user, ' + + '`` is the URL of Elasticsearch, and `` is the URL of Kibana.', + }, }, 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:', + textPre: + 'This module requires two Elasticsearch plugins that are not ' + + 'installed by default.\n\nFrom the Elasticsearch installation folder, run:', commands: [ 'bin/elasticsearch-plugin install ingest-geoip', - 'bin/elasticsearch-plugin install ingest-user-agent' - ] + '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' - ] - } - } -}; + 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'], + }, + }, +}); -export const FILEBEAT_CLOUD_INSTRUCTIONS = { +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.' + commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], + textPost: 'Where `` is the password of the `elastic` user.', }, 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.' + 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.', }, 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.' + 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.', }, 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.' - } - } -}; + 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.', + }, + }, +}); 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.' + commands: ['./filebeat modules enable ' + moduleName], + textPost: 'Modify the settings in the `modules.d/' + moduleName + '.yml` file.', }, 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.' + commands: ['sudo filebeat modules enable ' + moduleName], + textPost: 'Modify the settings in the `/etc/filebeat/modules.d/' + moduleName + '.yml` file.', }, 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.' + commands: ['sudo filebeat modules enable ' + moduleName], + textPost: 'Modify the settings in the `/etc/filebeat/modules.d/' + moduleName + '.yml` file.', }, 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.' - } + commands: ['PS C:\\Program Files\\Filebeat> filebeat.exe modules enable ' + moduleName], + textPost: 'Modify the settings in the `modules.d/' + moduleName + '.yml` file.', + }, }; } @@ -279,16 +266,18 @@ export function filebeatStatusCheck(moduleName) { bool: { filter: { term: { - 'fileset.module': moduleName - } - } - } - } - } + 'fileset.module': moduleName, + }, + }, + }, + }, + }, }; } export function onPremInstructions(moduleName, platforms, geoipRequired, uaRequired) { + const FILEBEAT_INSTRUCTIONS = createFilebeatInstructions(); + const variants = []; for (let i = 0; i < platforms.length; i++) { const platform = platforms[i]; @@ -304,7 +293,7 @@ export function onPremInstructions(moduleName, platforms, geoipRequired, uaRequi instructions.push(FILEBEAT_INSTRUCTIONS.START[platform]); variants.push({ id: INSTRUCTION_VARIANT[platform], - instructions: instructions + instructions: instructions, }); } return { @@ -312,13 +301,17 @@ export function onPremInstructions(moduleName, platforms, geoipRequired, uaRequi { title: 'Getting Started', instructionVariants: variants, - statusCheck: filebeatStatusCheck(moduleName) - } - ] + statusCheck: filebeatStatusCheck(moduleName), + }, + ], }; } export function onPremCloudInstructions(moduleName, platforms) { + const FILEBEAT_INSTRUCTIONS = createFilebeatInstructions(); + const TRYCLOUD_OPTION1 = createTrycloudOption1(); + const TRYCLOUD_OPTION2 = createTrycloudOption2(); + const variants = []; for (let i = 0; i < platforms.length; i++) { const platform = platforms[i]; @@ -330,8 +323,8 @@ export function onPremCloudInstructions(moduleName, platforms) { FILEBEAT_INSTRUCTIONS.INSTALL[platform], FILEBEAT_INSTRUCTIONS.CONFIG[platform], filebeatEnableInstructions(moduleName)[platform], - FILEBEAT_INSTRUCTIONS.START[platform] - ] + FILEBEAT_INSTRUCTIONS.START[platform], + ], }); } @@ -340,13 +333,16 @@ export function onPremCloudInstructions(moduleName, platforms) { { title: 'Getting Started', instructionVariants: variants, - statusCheck: filebeatStatusCheck(moduleName) - } - ] + statusCheck: filebeatStatusCheck(moduleName), + }, + ], }; } export function cloudInstructions(moduleName, platforms) { + const FILEBEAT_INSTRUCTIONS = createFilebeatInstructions(); + const FILEBEAT_CLOUD_INSTRUCTIONS = createFilebeatCloudInstructions(); + const variants = []; for (let i = 0; i < platforms.length; i++) { const platform = platforms[i]; @@ -356,8 +352,8 @@ export function cloudInstructions(moduleName, platforms) { FILEBEAT_INSTRUCTIONS.INSTALL[platform], FILEBEAT_CLOUD_INSTRUCTIONS.CONFIG[platform], filebeatEnableInstructions(moduleName)[platform], - FILEBEAT_INSTRUCTIONS.START[platform] - ] + FILEBEAT_INSTRUCTIONS.START[platform], + ], }); } @@ -366,8 +362,8 @@ export function cloudInstructions(moduleName, platforms) { { title: 'Getting Started', instructionVariants: variants, - statusCheck: filebeatStatusCheck(moduleName) - } - ] + 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 c231fda0a6152c..4c52314236d101 100644 --- a/src/core_plugins/kibana/common/tutorials/logstash_instructions.js +++ b/src/core_plugins/kibana/common/tutorials/logstash_instructions.js @@ -17,35 +17,39 @@ * under the License. */ -export const LOGSTASH_INSTRUCTIONS = { +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).' + textPre: + 'Follow the installation instructions [here](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 ' + + textPre: + 'First time using Logstash? See the ' + '[Getting Started Guide]({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' - ] - } + 'tar xzvf logstash-{config.kibana.version}.tar.gz', + ], + }, ], 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).' + textPre: + 'Follow the installation instructions [here](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 ' + + 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.' - } + ' 2. Extract the contents of the zip file.', + }, ], - } -}; + }, +}); diff --git a/src/core_plugins/kibana/common/tutorials/metricbeat_instructions.js b/src/core_plugins/kibana/common/tutorials/metricbeat_instructions.js index 13a9aabb5a60b9..8e1b9b95bcb477 100644 --- a/src/core_plugins/kibana/common/tutorials/metricbeat_instructions.js +++ b/src/core_plugins/kibana/common/tutorials/metricbeat_instructions.js @@ -18,95 +18,97 @@ */ import { INSTRUCTION_VARIANT } from './instruction_variant'; -import { TRYCLOUD_OPTION1, TRYCLOUD_OPTION2 } from './onprem_cloud_instructions'; +import { createTrycloudOption1, createTrycloudOption2 } from './onprem_cloud_instructions'; -export const METRICBEAT_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).', + textPre: + 'First time using Metricbeat? See the [Getting Started Guide]' + + '({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', 'cd metricbeat-{config.kibana.version}-darwin-x86_64/', - ] + ], }, DEB: { title: 'Download and install Metricbeat', - textPre: 'First time using Metricbeat? See the [Getting Started Guide]' + - '({config.docs.beats.metricbeat}/metricbeat-getting-started.html).', + textPre: + 'First time using Metricbeat? See the [Getting Started Guide]' + + '({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' + '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: + 'Looking for the 32-bit packages? See the [Download page](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).', + textPre: + 'First time using Metricbeat? See the [Getting Started Guide]' + + '({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' + '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: + 'Looking for the 32-bit packages? See the [Download page](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.', + 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.', commands: [ 'PS > cd C:\\Program Files\\Metricbeat', - 'PS C:\\Program Files\\Metricbeat> .\\install-service-metricbeat.ps1' + '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: + 'Modify the settings under `output.elasticsearch` in the ' + + '`C:\\Program Files\\Metricbeat\\metricbeat.yml` file to point to your Elasticsearch installation.', + }, }, 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', - ] + textPre: + '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', - ] + 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'], }, 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', - ], + 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'], }, WINDOWS: { title: 'Start Metricbeat', - textPre: 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, ' + - 'omit this command.', + textPre: + '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', - ] - } + ], + }, }, CONFIG: { OSX: { @@ -118,10 +120,11 @@ export const METRICBEAT_INSTRUCTIONS = { ' username: "elastic"', ' password: ""', 'setup.kibana:', - ' host: ""' + ' host: ""', ], - textPost: 'Where `` is the password of the `elastic` user, ' + - '`` is the URL of Elasticsearch, and `` is the URL of Kibana.' + textPost: + 'Where `` is the password of the `elastic` user, ' + + '`` is the URL of Elasticsearch, and `` is the URL of Kibana.', }, DEB: { title: 'Edit the configuration', @@ -132,10 +135,11 @@ export const METRICBEAT_INSTRUCTIONS = { ' username: "elastic"', ' password: ""', 'setup.kibana:', - ' host: ""' + ' host: ""', ], - textPost: 'Where `` is the password of the `elastic` user, ' + - '`` is the URL of Elasticsearch, and `` is the URL of Kibana.' + textPost: + 'Where `` is the password of the `elastic` user, ' + + '`` is the URL of Elasticsearch, and `` is the URL of Kibana.', }, RPM: { title: 'Edit the configuration', @@ -146,101 +150,89 @@ export const METRICBEAT_INSTRUCTIONS = { ' username: "elastic"', ' password: ""', 'setup.kibana:', - ' host: ""' + ' host: ""', ], - textPost: 'Where `` is the password of the `elastic` user, ' + - '`` is the URL of Elasticsearch, and `` is the URL of Kibana.' + textPost: + 'Where `` is the password of the `elastic` user, ' + + '`` is the URL of Elasticsearch, and `` is the URL of Kibana.', }, WINDOWS: { title: 'Edit the configuration', - textPre: 'Modify `C:\\Program Files\\Metricbeat\\metricbeat.yml` to set the connection information:', + textPre: + 'Modify `C:\\Program Files\\Metricbeat\\metricbeat.yml` to set the connection information:', commands: [ 'output.elasticsearch:', ' hosts: [""]', ' username: "elastic"', ' password: ""', 'setup.kibana:', - ' host: ""' + ' host: ""', ], - textPost: 'Where `` is the password of the `elastic` user, ' + - '`` is the URL of Elasticsearch, and `` is the URL of Kibana.' - } - } -}; + textPost: + 'Where `` is the password of the `elastic` user, ' + + '`` is the URL of Elasticsearch, and `` is the URL of Kibana.', + }, + }, +}); -export const METRICBEAT_CLOUD_INSTRUCTIONS = { +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.' + commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], + textPost: 'Where `` is the password of the `elastic` user.', }, 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.' + 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.', }, 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.' + 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.', }, 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.' - } - } -}; + 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.', + }, + }, +}); 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.' + commands: ['./metricbeat modules enable ' + moduleName], + textPost: 'Modify the settings in the `modules.d/' + moduleName + '.yml` file.', }, 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.' + commands: ['sudo metricbeat modules enable ' + moduleName], + textPost: + 'Modify the settings in the `/etc/metricbeat/modules.d/' + moduleName + '.yml` file.', }, 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.' + commands: ['sudo metricbeat modules enable ' + moduleName], + textPost: + 'Modify the settings in the `/etc/metricbeat/modules.d/' + moduleName + '.yml` file.', }, 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.' - } + commands: ['PS C:\\Program Files\\Metricbeat> metricbeat.exe modules enable ' + moduleName], + textPost: 'Modify the settings in the `modules.d/' + moduleName + '.yml` file.', + }, }; } @@ -257,16 +249,18 @@ export function metricbeatStatusCheck(moduleName) { bool: { filter: { term: { - 'metricset.module': moduleName - } - } - } - } - } + 'metricset.module': moduleName, + }, + }, + }, + }, + }, }; } export function onPremInstructions(moduleName) { + const METRICBEAT_INSTRUCTIONS = createMetricbeatInstructions(); + return { instructionSets: [ { @@ -278,8 +272,8 @@ export function onPremInstructions(moduleName) { METRICBEAT_INSTRUCTIONS.INSTALL.OSX, METRICBEAT_INSTRUCTIONS.CONFIG.OSX, metricbeatEnableInstructions(moduleName).OSX, - METRICBEAT_INSTRUCTIONS.START.OSX - ] + METRICBEAT_INSTRUCTIONS.START.OSX, + ], }, { id: INSTRUCTION_VARIANT.DEB, @@ -287,8 +281,8 @@ export function onPremInstructions(moduleName) { METRICBEAT_INSTRUCTIONS.INSTALL.DEB, METRICBEAT_INSTRUCTIONS.CONFIG.DEB, metricbeatEnableInstructions(moduleName).DEB, - METRICBEAT_INSTRUCTIONS.START.DEB - ] + METRICBEAT_INSTRUCTIONS.START.DEB, + ], }, { id: INSTRUCTION_VARIANT.RPM, @@ -296,8 +290,8 @@ export function onPremInstructions(moduleName) { METRICBEAT_INSTRUCTIONS.INSTALL.RPM, METRICBEAT_INSTRUCTIONS.CONFIG.RPM, metricbeatEnableInstructions(moduleName).RPM, - METRICBEAT_INSTRUCTIONS.START.RPM - ] + METRICBEAT_INSTRUCTIONS.START.RPM, + ], }, { id: INSTRUCTION_VARIANT.WINDOWS, @@ -305,17 +299,21 @@ export function onPremInstructions(moduleName) { METRICBEAT_INSTRUCTIONS.INSTALL.WINDOWS, METRICBEAT_INSTRUCTIONS.CONFIG.WINDOWS, metricbeatEnableInstructions(moduleName).WINDOWS, - METRICBEAT_INSTRUCTIONS.START.WINDOWS - ] - } + METRICBEAT_INSTRUCTIONS.START.WINDOWS, + ], + }, ], - statusCheck: metricbeatStatusCheck(moduleName) - } - ] + statusCheck: metricbeatStatusCheck(moduleName), + }, + ], }; } export function onPremCloudInstructions(moduleName) { + const TRYCLOUD_OPTION1 = createTrycloudOption1(); + const TRYCLOUD_OPTION2 = createTrycloudOption2(); + const METRICBEAT_INSTRUCTIONS = createMetricbeatInstructions(); + return { instructionSets: [ { @@ -329,8 +327,8 @@ export function onPremCloudInstructions(moduleName) { METRICBEAT_INSTRUCTIONS.INSTALL.OSX, METRICBEAT_INSTRUCTIONS.CONFIG.OSX, metricbeatEnableInstructions(moduleName).OSX, - METRICBEAT_INSTRUCTIONS.START.OSX - ] + METRICBEAT_INSTRUCTIONS.START.OSX, + ], }, { id: INSTRUCTION_VARIANT.DEB, @@ -340,8 +338,8 @@ export function onPremCloudInstructions(moduleName) { METRICBEAT_INSTRUCTIONS.INSTALL.DEB, METRICBEAT_INSTRUCTIONS.CONFIG.DEB, metricbeatEnableInstructions(moduleName).DEB, - METRICBEAT_INSTRUCTIONS.START.DEB - ] + METRICBEAT_INSTRUCTIONS.START.DEB, + ], }, { id: INSTRUCTION_VARIANT.RPM, @@ -351,8 +349,8 @@ export function onPremCloudInstructions(moduleName) { METRICBEAT_INSTRUCTIONS.INSTALL.RPM, METRICBEAT_INSTRUCTIONS.CONFIG.RPM, metricbeatEnableInstructions(moduleName).RPM, - METRICBEAT_INSTRUCTIONS.START.RPM - ] + METRICBEAT_INSTRUCTIONS.START.RPM, + ], }, { id: INSTRUCTION_VARIANT.WINDOWS, @@ -362,17 +360,20 @@ export function onPremCloudInstructions(moduleName) { METRICBEAT_INSTRUCTIONS.INSTALL.WINDOWS, METRICBEAT_INSTRUCTIONS.CONFIG.WINDOWS, metricbeatEnableInstructions(moduleName).WINDOWS, - METRICBEAT_INSTRUCTIONS.START.WINDOWS - ] - } + METRICBEAT_INSTRUCTIONS.START.WINDOWS, + ], + }, ], - statusCheck: metricbeatStatusCheck(moduleName) - } - ] + statusCheck: metricbeatStatusCheck(moduleName), + }, + ], }; } export function cloudInstructions(moduleName) { + const METRICBEAT_INSTRUCTIONS = createMetricbeatInstructions(); + const METRICBEAT_CLOUD_INSTRUCTIONS = createMetricbeatCloudInstructions(); + return { instructionSets: [ { @@ -384,8 +385,8 @@ export function cloudInstructions(moduleName) { METRICBEAT_INSTRUCTIONS.INSTALL.OSX, METRICBEAT_CLOUD_INSTRUCTIONS.CONFIG.OSX, metricbeatEnableInstructions(moduleName).OSX, - METRICBEAT_INSTRUCTIONS.START.OSX - ] + METRICBEAT_INSTRUCTIONS.START.OSX, + ], }, { id: INSTRUCTION_VARIANT.DEB, @@ -393,8 +394,8 @@ export function cloudInstructions(moduleName) { METRICBEAT_INSTRUCTIONS.INSTALL.DEB, METRICBEAT_CLOUD_INSTRUCTIONS.CONFIG.DEB, metricbeatEnableInstructions(moduleName).DEB, - METRICBEAT_INSTRUCTIONS.START.DEB - ] + METRICBEAT_INSTRUCTIONS.START.DEB, + ], }, { id: INSTRUCTION_VARIANT.RPM, @@ -402,8 +403,8 @@ export function cloudInstructions(moduleName) { METRICBEAT_INSTRUCTIONS.INSTALL.RPM, METRICBEAT_CLOUD_INSTRUCTIONS.CONFIG.RPM, metricbeatEnableInstructions(moduleName).RPM, - METRICBEAT_INSTRUCTIONS.START.RPM - ] + METRICBEAT_INSTRUCTIONS.START.RPM, + ], }, { id: INSTRUCTION_VARIANT.WINDOWS, @@ -411,12 +412,12 @@ export function cloudInstructions(moduleName) { METRICBEAT_INSTRUCTIONS.INSTALL.WINDOWS, METRICBEAT_CLOUD_INSTRUCTIONS.CONFIG.WINDOWS, metricbeatEnableInstructions(moduleName).WINDOWS, - METRICBEAT_INSTRUCTIONS.START.WINDOWS - ] - } + METRICBEAT_INSTRUCTIONS.START.WINDOWS, + ], + }, ], - statusCheck: metricbeatStatusCheck(moduleName) - } - ] + statusCheck: metricbeatStatusCheck(moduleName), + }, + ], }; } 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 9b0326a1b647a2..eec1848c06a931 100644 --- a/src/core_plugins/kibana/common/tutorials/onprem_cloud_instructions.js +++ b/src/core_plugins/kibana/common/tutorials/onprem_cloud_instructions.js @@ -17,25 +17,24 @@ * under the License. */ -export const TRYCLOUD_OPTION1 = { +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' + + 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', +}); - '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' - -}; - -export const TRYCLOUD_OPTION2 = { +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' -}; + 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', +}); diff --git a/src/core_plugins/kibana/public/dashboard/dashboard_constants.js b/src/core_plugins/kibana/public/dashboard/dashboard_constants.ts similarity index 95% rename from src/core_plugins/kibana/public/dashboard/dashboard_constants.js rename to src/core_plugins/kibana/public/dashboard/dashboard_constants.ts index 9bdecc4253d765..2d0995179860ea 100644 --- a/src/core_plugins/kibana/public/dashboard/dashboard_constants.js +++ b/src/core_plugins/kibana/public/dashboard/dashboard_constants.ts @@ -28,6 +28,6 @@ export const DASHBOARD_GRID_HEIGHT = 20; export const DEFAULT_PANEL_WIDTH = DASHBOARD_GRID_COLUMN_COUNT / 2; export const DEFAULT_PANEL_HEIGHT = 15; -export function createDashboardEditUrl(id) { +export function createDashboardEditUrl(id: string) { return `/dashboard/${id}`; } diff --git a/src/core_plugins/kibana/public/dashboard/panel/_dashboard_panel.scss b/src/core_plugins/kibana/public/dashboard/panel/_dashboard_panel.scss index e0b1e6e3a315be..36130cd1837476 100644 --- a/src/core_plugins/kibana/public/dashboard/panel/_dashboard_panel.scss +++ b/src/core_plugins/kibana/public/dashboard/panel/_dashboard_panel.scss @@ -12,6 +12,7 @@ flex: 1 1 100%; height: auto; z-index: 1; + background-color: inherit; } // SASSTODO: Pretty sure this doesn't do anything since the flex-basis 100%, @@ -103,7 +104,7 @@ color: $euiColorDarkShade; transition: color $euiAnimSpeedFast $euiAnimSlightResistance; flex: 1 1 auto; - @include truncate; + @include euiTextTruncate; // flexbox fix for IE10 // http://stackoverflow.com/questions/22008135/internet-explorer-10-does-not-apply-flexbox-on-inline-elements diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_error.js b/src/core_plugins/kibana/public/dashboard/panel/panel_error.tsx similarity index 83% rename from src/core_plugins/kibana/public/dashboard/panel/panel_error.js rename to src/core_plugins/kibana/public/dashboard/panel/panel_error.tsx index 6b9d1d689e25c4..4901c7d9287f1a 100644 --- a/src/core_plugins/kibana/public/dashboard/panel/panel_error.js +++ b/src/core_plugins/kibana/public/dashboard/panel/panel_error.tsx @@ -18,21 +18,16 @@ */ import React from 'react'; -import PropTypes from 'prop-types'; -export function PanelError({ error }) { +export interface PanelErrorProps { + error: string | React.ReactNode; +} + +export function PanelError({ error }: PanelErrorProps) { return (
-
); } - -PanelError.propTypes = { - error: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.node - ]), -}; - diff --git a/src/core_plugins/kibana/public/discover/_index.scss b/src/core_plugins/kibana/public/discover/_index.scss index b45ab8781566d6..e42c4e90938292 100644 --- a/src/core_plugins/kibana/public/discover/_index.scss +++ b/src/core_plugins/kibana/public/discover/_index.scss @@ -1,7 +1,7 @@ @import 'components/fetch_error/index'; @import 'directives/index'; -@import '../../../../ui/public/styles/local_search'; +@import 'ui/public/styles/local_search'; @import 'hacks'; diff --git a/src/core_plugins/kibana/public/home/_home.scss b/src/core_plugins/kibana/public/home/_home.scss new file mode 100644 index 00000000000000..48c41254ff0415 --- /dev/null +++ b/src/core_plugins/kibana/public/home/_home.scss @@ -0,0 +1,13 @@ +home-app { + background-color: $euiColorLightestShade; +} + +.homPage { + min-height: 100vh; + max-width: 1200px; + margin: auto; +} + +.homSampleDataSetCard { + flex-grow: 0; // IE FIX +} diff --git a/src/core_plugins/kibana/public/home/_index.scss b/src/core_plugins/kibana/public/home/_index.scss new file mode 100644 index 00000000000000..f972db1e85917d --- /dev/null +++ b/src/core_plugins/kibana/public/home/_index.scss @@ -0,0 +1,2 @@ +@import './home'; +@import './components/index'; diff --git a/src/core_plugins/kibana/public/home/components/__snapshots__/add_data.test.js.snap b/src/core_plugins/kibana/public/home/components/__snapshots__/add_data.test.js.snap index ed3bbe797db964..51d080ee62c4c4 100644 --- a/src/core_plugins/kibana/public/home/components/__snapshots__/add_data.test.js.snap +++ b/src/core_plugins/kibana/public/home/components/__snapshots__/add_data.test.js.snap @@ -52,11 +52,11 @@ exports[`apmUiEnabled 1`] = ` grow={true} > @@ -84,11 +84,11 @@ exports[`apmUiEnabled 1`] = ` grow={true} > @@ -116,11 +116,11 @@ exports[`apmUiEnabled 1`] = ` grow={true} > @@ -148,11 +148,11 @@ exports[`apmUiEnabled 1`] = ` grow={true} > @@ -190,7 +190,7 @@ exports[`apmUiEnabled 1`] = ` wrap={false} > @@ -221,7 +221,7 @@ exports[`apmUiEnabled 1`] = ` @@ -307,11 +307,11 @@ exports[`isNewKibanaInstance 1`] = ` grow={true} > @@ -339,11 +339,11 @@ exports[`isNewKibanaInstance 1`] = ` grow={true} > @@ -371,11 +371,11 @@ exports[`isNewKibanaInstance 1`] = ` grow={true} > @@ -413,7 +413,7 @@ exports[`isNewKibanaInstance 1`] = ` wrap={false} > @@ -444,7 +444,7 @@ exports[`isNewKibanaInstance 1`] = ` @@ -530,11 +530,11 @@ exports[`render 1`] = ` grow={true} > @@ -562,11 +562,11 @@ exports[`render 1`] = ` grow={true} > @@ -594,11 +594,11 @@ exports[`render 1`] = ` grow={true} > @@ -636,7 +636,7 @@ exports[`render 1`] = ` wrap={false} > @@ -667,7 +667,7 @@ exports[`render 1`] = ` diff --git a/src/core_plugins/kibana/public/home/components/__snapshots__/home.test.js.snap b/src/core_plugins/kibana/public/home/components/__snapshots__/home.test.js.snap index fca2261462277e..4f245a6cc8ef7d 100644 --- a/src/core_plugins/kibana/public/home/components/__snapshots__/home.test.js.snap +++ b/src/core_plugins/kibana/public/home/components/__snapshots__/home.test.js.snap @@ -2,7 +2,7 @@ exports[`home directories should not render directory entry when showOnHomePage is false 1`] = ` @@ -48,7 +48,7 @@ exports[`render 1`] = ` key="0" > @@ -91,7 +92,7 @@ exports[`render 1`] = ` ( } + className="homAddData__card" + icon={} title="APM" description="APM automatically collects in-depth performance metrics and errors from inside your applications." footer={ Add APM @@ -65,13 +64,13 @@ export function AddData({ apmUiEnabled, isNewKibanaInstance }) { } + className="homAddData__card" + icon={} title="Logging" description="Ingest logs from popular data sources and easily visualize in preconfigured dashboards." footer={ Add log data @@ -82,13 +81,13 @@ export function AddData({ apmUiEnabled, isNewKibanaInstance }) { } + className="homAddData__card" + icon={} title="Metrics" description="Collect metrics from the operating system and services running on your servers." footer={ Add metric data @@ -99,13 +98,13 @@ export function AddData({ apmUiEnabled, isNewKibanaInstance }) { } + className="homAddData__card" + icon={} title="Security Analytics" description="Centralize security events for interactive investigation in ready-to-go visualizations." footer={ Add security events @@ -117,7 +116,7 @@ export function AddData({ apmUiEnabled, isNewKibanaInstance }) { ); }; - const footerItemClasses = classNames('addDataFooterItem', { 'addDataFooterItem_highlight': isNewKibanaInstance }); + const footerItemClasses = classNames('homAddData__footerItem', { 'homAddData__footerItem--highlight': isNewKibanaInstance }); return ( diff --git a/src/core_plugins/kibana/public/home/components/add_data.less b/src/core_plugins/kibana/public/home/components/add_data.less deleted file mode 100644 index 0dcb5f46b99cb4..00000000000000 --- a/src/core_plugins/kibana/public/home/components/add_data.less +++ /dev/null @@ -1,25 +0,0 @@ -.addDataCard { - width: 250px; - min-width: 200px; - border: none; - box-shadow: none; -} - -.addDataIcon { - width: 64px; - height: 64px; -} - -.addDataButton { - line-height: normal; -} - -.addDataFooterItem_highlight { - background-color: #e6f2f6; - padding-top: 16px; - padding-bottom: 16px; -} - -.addDataFooterItem { - text-align: center; -} diff --git a/src/core_plugins/kibana/public/home/components/feature_directory.js b/src/core_plugins/kibana/public/home/components/feature_directory.js index fff2c58659ef6f..ce7371fbff092f 100644 --- a/src/core_plugins/kibana/public/home/components/feature_directory.js +++ b/src/core_plugins/kibana/public/home/components/feature_directory.js @@ -76,7 +76,6 @@ export class FeatureDirectory extends React.Component { renderTabs = () => { return this.tabs.map((tab, index) => ( this.onSelectedTabChanged(tab.id)} isSelected={tab.id === this.state.selectedTabId} key={index} @@ -114,7 +113,7 @@ export class FeatureDirectory extends React.Component { render() { return ( - +

diff --git a/src/core_plugins/kibana/public/home/components/home.js b/src/core_plugins/kibana/public/home/components/home.js index 1d3c195bdaf1d4..5cf09eb239d2a0 100644 --- a/src/core_plugins/kibana/public/home/components/home.js +++ b/src/core_plugins/kibana/public/home/components/home.js @@ -141,7 +141,7 @@ export class Home extends Component { } return ( - + {recentlyAccessedPanel} diff --git a/src/core_plugins/kibana/public/home/components/recently_accessed.js b/src/core_plugins/kibana/public/home/components/recently_accessed.js index 14e0f496d10aff..b0dbfb888d0bf5 100644 --- a/src/core_plugins/kibana/public/home/components/recently_accessed.js +++ b/src/core_plugins/kibana/public/home/components/recently_accessed.js @@ -17,7 +17,6 @@ * under the License. */ -import './recently_accessed.less'; import React, { Component } from 'react'; import PropTypes from 'prop-types'; @@ -74,7 +73,7 @@ export class RecentlyAccessed extends Component { data-test-subj={`moreRecentlyAccessedItem${this.props.recentlyAccessed[i].id}`} > {this.props.recentlyAccessed[i].label} @@ -90,7 +89,7 @@ export class RecentlyAccessed extends Component { data-test-subj="openMoreRecentlyAccessedPopover" > {`${dropdownLinks.length} more`} @@ -127,7 +126,7 @@ export class RecentlyAccessed extends Component { let separator; if (includeSeparator) { separator = ( - + {separator} {recentlyAccessedItem.label} @@ -205,7 +205,7 @@ export class RecentlyAccessed extends Component { - + {this.renderRecentlyAccessed()} diff --git a/src/core_plugins/kibana/public/home/components/recently_accessed.less b/src/core_plugins/kibana/public/home/components/recently_accessed.less deleted file mode 100644 index df491d95a53e30..00000000000000 --- a/src/core_plugins/kibana/public/home/components/recently_accessed.less +++ /dev/null @@ -1,35 +0,0 @@ -@media only screen and (max-width: 768px) { - .recentlyAccessedSeparator { - display: none; - } -} - -.recentlyAccessedItem { - overflow: hidden; - max-width: 300px; - - .euiToolTipAnchor { - overflow: hidden; - text-overflow: ellipsis; - } -} - -.recentlyAccessedLongLink { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - line-height: normal; -} - -.recentlyAccessedFlexItem { - max-width: 1000px; -} - -.recentlyAccessedDropwdownLink { - white-space: nowrap; - height: 18px; -} - -.recentlyAccessedDropdownLabel { - white-space: nowrap; -} diff --git a/src/core_plugins/kibana/public/home/components/sample_data_set_card.js b/src/core_plugins/kibana/public/home/components/sample_data_set_card.js index 101861f0dd486a..a19eaf1c5dd8a8 100644 --- a/src/core_plugins/kibana/public/home/components/sample_data_set_card.js +++ b/src/core_plugins/kibana/public/home/components/sample_data_set_card.js @@ -28,70 +28,40 @@ import { EuiToolTip, } from '@elastic/eui'; -import { - installSampleDataSet, - uninstallSampleDataSet -} from '../sample_data_sets'; +export const INSTALLED_STATUS = 'installed'; +export const UNINSTALLED_STATUS = 'not_installed'; export class SampleDataSetCard extends React.Component { - constructor(props) { - super(props); - - this.state = { - isProcessingRequest: false, - }; - } - - startRequest = async () => { - const { - getConfig, - setConfig, - id, - name, - onRequestComplete, - defaultIndex, - clearIndexPatternsCache, - } = this.props; - - this.setState({ - isProcessingRequest: true, - }); - - if (this.isInstalled()) { - await uninstallSampleDataSet(id, name, defaultIndex, getConfig, setConfig, clearIndexPatternsCache); - } else { - await installSampleDataSet(id, name, defaultIndex, getConfig, setConfig, clearIndexPatternsCache); - } - - onRequestComplete(); - - this.setState({ - isProcessingRequest: false, - }); - } - isInstalled = () => { - if (this.props.status === 'installed') { + if (this.props.status === INSTALLED_STATUS) { return true; } return false; } + install = () => { + this.props.onInstall(this.props.id); + } + + uninstall = () => { + this.props.onUninstall(this.props.id); + } + renderBtn = () => { switch (this.props.status) { - case 'installed': + case INSTALLED_STATUS: return ( - {this.state.isProcessingRequest ? 'Removing' : 'Remove'} + {this.props.isProcessing ? 'Removing' : 'Remove'} @@ -105,16 +75,16 @@ export class SampleDataSetCard extends React.Component { ); - case 'not_installed': + case UNINSTALLED_STATUS: return ( - {this.state.isProcessingRequest ? 'Adding' : 'Add'} + {this.props.isProcessing ? 'Adding' : 'Add'} @@ -145,7 +115,7 @@ export class SampleDataSetCard extends React.Component { render() { return ( { + let sampleDataSets; + try { + sampleDataSets = await listSampleDataSets(); + } catch (fetchError) { + toastNotifications.addDanger({ + title: `Unable to load sample data sets list`, + text: `${fetchError.message}`, + }); + sampleDataSets = []; + } + + if (!this._isMounted) { + return; + } + + this.setState({ + sampleDataSets: sampleDataSets + .sort((a, b) => { + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); + }), + processingStatus: {}, + }); + } + + install = async (id) => { + const { + getConfig, + setConfig, + clearIndexPatternsCache, + } = this.props; + + const targetSampleDataSet = this.state.sampleDataSets.find((sampleDataSet) => { + return sampleDataSet.id === id; + }); + + this.setState((prevState) => ({ + processingStatus: { ...prevState.processingStatus, [id]: true } + })); + + try { + await installSampleDataSet(id, targetSampleDataSet.defaultIndex, getConfig, setConfig, clearIndexPatternsCache); + } catch (fetchError) { + if (this._isMounted) { + this.setState((prevState) => ({ + processingStatus: { ...prevState.processingStatus, [id]: false } + })); + } + toastNotifications.addDanger({ + title: `Unable to install sample data set: ${targetSampleDataSet.name}`, + text: `${fetchError.message}`, + }); + return; + } + + this.setState((prevState) => ({ + processingStatus: { ...prevState.processingStatus, [id]: false }, + sampleDataSets: prevState.sampleDataSets.map(sampleDataSet => { + if (sampleDataSet.id === id) { + sampleDataSet.status = INSTALLED_STATUS; + } + return sampleDataSet; + }), + })); + toastNotifications.addSuccess({ + title: `${targetSampleDataSet.name} installed`, + ['data-test-subj']: 'sampleDataSetInstallToast' + }); + } + + uninstall = async (id) => { + const { + getConfig, + setConfig, + clearIndexPatternsCache, + } = this.props; + + const targetSampleDataSet = this.state.sampleDataSets.find((sampleDataSet) => { + return sampleDataSet.id === id; + }); + + this.setState((prevState) => ({ + processingStatus: { ...prevState.processingStatus, [id]: true } + })); + + try { + await uninstallSampleDataSet(id, targetSampleDataSet.defaultIndex, getConfig, setConfig, clearIndexPatternsCache); + } catch (fetchError) { + if (this._isMounted) { + this.setState((prevState) => ({ + processingStatus: { ...prevState.processingStatus, [id]: false } + })); + } + toastNotifications.addDanger({ + title: `Unable to uninstall sample data set: ${targetSampleDataSet.name}`, + text: `${fetchError.message}`, + }); + return; + } + + this.setState((prevState) => ({ + processingStatus: { ...prevState.processingStatus, [id]: false }, + sampleDataSets: prevState.sampleDataSets.map(sampleDataSet => { + if (sampleDataSet.id === id) { + sampleDataSet.status = UNINSTALLED_STATUS; + } + return sampleDataSet; + }), + })); + toastNotifications.addSuccess({ + title: `${targetSampleDataSet.name} uninstalled`, + ['data-test-subj']: 'sampleDataSetUninstallToast' + }); + } + + render() { + return ( + + { + this.state.sampleDataSets.map(sampleDataSet => { + return ( + + + + ); + }) + } + + ); + } +} + +SampleDataSetCards.propTypes = { + getConfig: PropTypes.func.isRequired, + setConfig: PropTypes.func.isRequired, + clearIndexPatternsCache: PropTypes.func.isRequired, + addBasePath: PropTypes.func.isRequired, +}; diff --git a/src/core_plugins/kibana/public/home/components/synopsis.js b/src/core_plugins/kibana/public/home/components/synopsis.js index e52d9b98d92cb8..ddcee7e3cfcf1f 100644 --- a/src/core_plugins/kibana/public/home/components/synopsis.js +++ b/src/core_plugins/kibana/public/home/components/synopsis.js @@ -17,7 +17,6 @@ * under the License. */ -import './synopsis.less'; import React from 'react'; import PropTypes from 'prop-types'; import { diff --git a/src/core_plugins/kibana/public/home/components/synopsis.less b/src/core_plugins/kibana/public/home/components/synopsis.less deleted file mode 100644 index bcc2e891baaa29..00000000000000 --- a/src/core_plugins/kibana/public/home/components/synopsis.less +++ /dev/null @@ -1,36 +0,0 @@ -@import (reference) "~ui/styles/variables.less"; - -.synopsis { - display: flex; - flex-grow: 1; - cursor: pointer; -} - -.synopsis:hover { - text-decoration: none; - .synopsisTitle { - text-decoration: underline; - } -} - -.synopsis:focus { - text-decoration: none; - - .synopsisPanel { - border: solid 1px @globalColorBlue; - } - - .synopsisBody { - text-decoration: none; - } -} - -.synopsisTitle { - font-size: 16px; - font-weight: normal; - color: @globalColorBlue; -} - -.synopsisIcon { - padding-top: 8px; -} diff --git a/src/core_plugins/kibana/public/home/components/tutorial/__snapshots__/content.test.js.snap b/src/core_plugins/kibana/public/home/components/tutorial/__snapshots__/content.test.js.snap index e5e0481bb3eec4..690875dc96900a 100644 --- a/src/core_plugins/kibana/public/home/components/tutorial/__snapshots__/content.test.js.snap +++ b/src/core_plugins/kibana/public/home/components/tutorial/__snapshots__/content.test.js.snap @@ -2,7 +2,7 @@ exports[`should render content with markdown 1`] = ` - -

- custom status check description -

-
+
- -

- custom status check description -

-
+
, "key": "checkStatusStep", - "status": "complete", + "status": "danger", "title": "custom title", }, ] @@ -436,13 +428,9 @@ exports[`statusCheckState failed status check - no data 1`] = ` component="div" grow={true} > - -

- custom status check description -

-
+
, "key": "checkStatusStep", - "status": "complete", + "status": "warning", "title": "custom title", }, ] @@ -570,13 +558,9 @@ exports[`statusCheckState initial state - no check has been attempted 1`] = ` component="div" grow={true} > - -

- custom status check description -

-
+
- -

- custom status check description -

-
+

Great tutorial +  

@@ -113,6 +114,7 @@ exports[`props iconType 1`] = ` >

Great tutorial +  

@@ -164,19 +166,14 @@ exports[`props isBeta 1`] = ` >

Great tutorial +   +

- - -

Great tutorial +  

@@ -285,6 +283,7 @@ exports[`render 1`] = ` >

Great tutorial +  

diff --git a/src/core_plugins/kibana/public/home/components/tutorial/__snapshots__/tutorial.test.js.snap b/src/core_plugins/kibana/public/home/components/tutorial/__snapshots__/tutorial.test.js.snap index 2fa3552180e1e2..debe5319704b8c 100644 --- a/src/core_plugins/kibana/public/home/components/tutorial/__snapshots__/tutorial.test.js.snap +++ b/src/core_plugins/kibana/public/home/components/tutorial/__snapshots__/tutorial.test.js.snap @@ -2,7 +2,7 @@ exports[`isCloudEnabled is false should not render instruction toggle when ON_PREM_ELASTIC_CLOUD instructions are not provided 1`] = `
- + + + + +
+
diff --git a/src/core_plugins/kibana/public/home/components/tutorial/instruction_set.js b/src/core_plugins/kibana/public/home/components/tutorial/instruction_set.js index a923dee56f4553..9d5a4cbc5f0bf7 100644 --- a/src/core_plugins/kibana/public/home/components/tutorial/instruction_set.js +++ b/src/core_plugins/kibana/public/home/components/tutorial/instruction_set.js @@ -26,6 +26,7 @@ import { } from '@kbn/ui-framework/components'; import { Instruction } from './instruction'; import { ParameterForm } from './parameter_form'; +import { Content } from './content'; import { getDisplayText } from '../../../../common/tutorials/instruction_variant'; import { EuiTabs, @@ -34,7 +35,6 @@ import { EuiSteps, EuiFlexGroup, EuiFlexItem, - EuiText, EuiButton, EuiCallOut, } from '@elastic/eui'; @@ -110,17 +110,32 @@ export class InstructionSet extends React.Component { ); } + getStepStatus(statusCheckState) { + switch (statusCheckState) { + case undefined: + case StatusCheckStates.NOT_CHECKED: + case StatusCheckStates.FETCHING: + return 'incomplete'; + case StatusCheckStates.HAS_DATA: + return 'complete'; + case StatusCheckStates.NO_DATA: + return 'warning'; + case StatusCheckStates.ERROR: + return 'danger'; + default: + throw new Error(`Unexpected status check state ${statusCheckState}`); + } + } + renderStatusCheck() { const { statusCheckState, statusCheckConfig, onStatusCheck } = this.props; const checkStatusStep = ( - -

- {statusCheckConfig.text} -

-
+
); - const stepStatus = statusCheckState === StatusCheckStates.NOT_CHECKED || - statusCheckState === StatusCheckStates.FETCHING ? 'incomplete' : 'complete'; return { title: statusCheckConfig.title || 'Status Check', - status: stepStatus, + status: this.getStepStatus(statusCheckState), children: checkStatusStep, key: 'checkStatusStep' }; diff --git a/src/core_plugins/kibana/public/home/components/tutorial/introduction.js b/src/core_plugins/kibana/public/home/components/tutorial/introduction.js index 0d7650e3771d25..1b3e06e35b2942 100644 --- a/src/core_plugins/kibana/public/home/components/tutorial/introduction.js +++ b/src/core_plugins/kibana/public/home/components/tutorial/introduction.js @@ -73,9 +73,7 @@ export function Introduction({ description, previewUrl, title, exportedFieldsUrl let betaBadge; if (isBeta) { betaBadge = ( - - - + ); } return ( @@ -90,11 +88,11 @@ export function Introduction({ description, previewUrl, title, exportedFieldsUrl

- {title} + {title}   + {betaBadge}

- {betaBadge}
diff --git a/src/core_plugins/kibana/public/home/components/tutorial/radio_button_group.js b/src/core_plugins/kibana/public/home/components/tutorial/radio_button_group.js deleted file mode 100644 index 1f73afcb2df9e2..00000000000000 --- a/src/core_plugins/kibana/public/home/components/tutorial/radio_button_group.js +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import './radio_button_group.less'; -import React from 'react'; -import PropTypes from 'prop-types'; -import { - KuiButtonGroup, - KuiButton -} from '@kbn/ui-framework/components'; - -export class RadioButtonGroup extends React.Component { - - constructor(props) { - super(props); - - this.state = {}; - - if (props.buttons.length > 0) { - const matchingButton = props.buttons.find(button => { - return props.selectedBtnLabel === button.label; - }); - if (matchingButton) { - this.state.selectedBtnLabel = props.selectedBtnLabel; - } else { - this.state.selectedBtnLabel = props.buttons[0].label; - } - } - } - - renderButtons = () => { - return this.props.buttons.map((button, index) => { - const handleOnClick = () => { - this.setState({ - selectedBtnLabel: button.label - }); - button.onClick(); - }; - - let buttonType = 'secondary'; - if (button.label === this.state.selectedBtnLabel) { - buttonType = 'primary'; - } - return ( - - {button.label} - - ); - }); - } - - render = () => { - return ( - - {this.renderButtons()} - - ); - } -} - -RadioButtonGroup.propTypes = { - buttons: PropTypes.arrayOf(PropTypes.shape({ - onClick: PropTypes.func.isRequired, - label: PropTypes.string.isRequired, - dataTestSubj: PropTypes.string - })).isRequired, - selectedBtnLabel: PropTypes.string -}; diff --git a/src/core_plugins/kibana/public/home/components/tutorial/radio_button_group.less b/src/core_plugins/kibana/public/home/components/tutorial/radio_button_group.less deleted file mode 100644 index 8630f2caf2570a..00000000000000 --- a/src/core_plugins/kibana/public/home/components/tutorial/radio_button_group.less +++ /dev/null @@ -1,10 +0,0 @@ - -// remove space between buttons -.kuiRadioButton { - margin-left: 0px !important; -} - -// give primary button same border as secondary button so they are even heights when placed side-by-side -.kuiRadioButton.kuiButton--primary { - border: solid 1px #0079a5; -} diff --git a/src/core_plugins/kibana/public/home/components/tutorial/tutorial.js b/src/core_plugins/kibana/public/home/components/tutorial/tutorial.js index ff34d3f616a0c2..f42dab9c16ff63 100644 --- a/src/core_plugins/kibana/public/home/components/tutorial/tutorial.js +++ b/src/core_plugins/kibana/public/home/components/tutorial/tutorial.js @@ -17,16 +17,24 @@ * under the License. */ -import './tutorial.less'; import _ from 'lodash'; import React from 'react'; import PropTypes from 'prop-types'; import { Footer } from './footer'; import { Introduction } from './introduction'; import { InstructionSet } from './instruction_set'; -import { RadioButtonGroup } from './radio_button_group'; import { SavedObjectsInstaller } from './saved_objects_installer'; -import { EuiSpacer, EuiPage, EuiPanel, EuiLink, EuiText, EuiPageBody } from '@elastic/eui'; +import { + EuiSpacer, + EuiPage, + EuiPanel, + EuiLink, + EuiText, + EuiPageBody, + EuiButtonGroup, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; import * as StatusCheckStates from './status_check_states'; const INSTRUCTIONS_TYPE = { @@ -177,25 +185,29 @@ export class Tutorial extends React.Component { return numHits === 0 ? StatusCheckStates.NO_DATA : StatusCheckStates.HAS_DATA; }; - onPrem = () => { - this.setVisibleInstructions(INSTRUCTIONS_TYPE.ON_PREM); - }; - - onPremElasticCloud = () => { - this.setVisibleInstructions(INSTRUCTIONS_TYPE.ON_PREM_ELASTIC_CLOUD); - }; - renderInstructionSetsToggle = () => { if (!this.props.isCloudEnabled && this.state.tutorial.onPremElasticCloud) { const radioButtons = [ - { onClick: this.onPrem, label: 'Self managed', dataTestSubj: 'onPremBtn' }, - { onClick: this.onPremElasticCloud, label: 'Elastic Cloud', dataTestSubj: 'onPremElasticCloudBtn' }, + { + id: INSTRUCTIONS_TYPE.ON_PREM, + label: 'Self managed', + }, + { + id: INSTRUCTIONS_TYPE.ON_PREM_ELASTIC_CLOUD, + label: 'Elastic Cloud', + } ]; return ( - + + + + + ); } }; @@ -281,7 +293,7 @@ export class Tutorial extends React.Component { let content; if (this.state.notFound) { content = ( -
+

Unable to find tutorial {this.props.tutorialId} @@ -315,7 +327,7 @@ export class Tutorial extends React.Component { /> -

+
{this.renderInstructionSetsToggle()}
@@ -329,7 +341,7 @@ export class Tutorial extends React.Component { ); } return ( - +
diff --git a/src/core_plugins/kibana/public/home/components/tutorial/tutorial.less b/src/core_plugins/kibana/public/home/components/tutorial/tutorial.less deleted file mode 100644 index 855a6a0f977463..00000000000000 --- a/src/core_plugins/kibana/public/home/components/tutorial/tutorial.less +++ /dev/null @@ -1,8 +0,0 @@ -.text-center > .kuiButtonGroup { - display: inline-block !important; -} - -.homePanel { - background: white; - padding: 24px; -} diff --git a/src/core_plugins/kibana/public/home/components/tutorial_directory.js b/src/core_plugins/kibana/public/home/components/tutorial_directory.js index 4ed5bd4e5327dc..d1835e5546c43b 100644 --- a/src/core_plugins/kibana/public/home/components/tutorial_directory.js +++ b/src/core_plugins/kibana/public/home/components/tutorial_directory.js @@ -21,7 +21,7 @@ import _ from 'lodash'; import React from 'react'; import PropTypes from 'prop-types'; import { Synopsis } from './synopsis'; -import { SampleDataSetCard } from './sample_data_set_card'; +import { SampleDataSetCards } from './sample_data_set_cards'; import { EuiPage, @@ -36,7 +36,6 @@ import { import { getTutorials } from '../load_tutorials'; -import { listSampleDataSets } from '../sample_data_sets'; const ALL_TAB_ID = 'all'; const SAMPLE_DATA_TAB_ID = 'sampleData'; @@ -70,7 +69,6 @@ export class TutorialDirectory extends React.Component { this.state = { selectedTabId: openTab, tutorialCards: [], - sampleDataSets: [], }; } @@ -81,8 +79,6 @@ export class TutorialDirectory extends React.Component { async componentDidMount() { this._isMounted = true; - this.loadSampleDataSets(); - const tutorialConfigs = await getTutorials(); if (!this._isMounted) { @@ -126,20 +122,6 @@ export class TutorialDirectory extends React.Component { }); } - loadSampleDataSets = async () => { - const sampleDataSets = await listSampleDataSets(); - - if (!this._isMounted) { - return; - } - - this.setState({ - sampleDataSets: sampleDataSets.sort((a, b) => { - return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); - }), - }); - } - onSelectedTabChanged = id => { this.setState({ selectedTabId: id, @@ -149,7 +131,6 @@ export class TutorialDirectory extends React.Component { renderTabs = () => { return this.tabs.map((tab, index) => ( this.onSelectedTabChanged(tab.id)} isSelected={tab.id === this.state.selectedTabId} key={index} @@ -159,62 +140,48 @@ export class TutorialDirectory extends React.Component { )); } - renderTab = () => { + renderTabContent = () => { if (this.state.selectedTabId === SAMPLE_DATA_TAB_ID) { - return this.renderSampleDataSetsTab(); - } - - return this.renderTutorialsTab(); - } - - renderTutorialsTab = () => { - return this.state.tutorialCards - .filter((tutorial) => { - return this.state.selectedTabId === ALL_TAB_ID || this.state.selectedTabId === tutorial.category; - }) - .map((tutorial) => { - return ( - - - - ); - }); - }; - - renderSampleDataSetsTab = () => { - return this.state.sampleDataSets.map(sampleDataSet => { return ( - - - + ); - }); + } + + return ( + + { + this.state.tutorialCards + .filter((tutorial) => { + return this.state.selectedTabId === ALL_TAB_ID || this.state.selectedTabId === tutorial.category; + }) + .map((tutorial) => { + return ( + + + + ); + }) + } + + ); } render() { return ( - + Home @@ -231,9 +198,7 @@ export class TutorialDirectory extends React.Component { {this.renderTabs()} - - { this.renderTab() } - + {this.renderTabContent()} diff --git a/src/core_plugins/kibana/public/home/components/welcome.js b/src/core_plugins/kibana/public/home/components/welcome.js index aa10b551f4ca36..237a6de55507ef 100644 --- a/src/core_plugins/kibana/public/home/components/welcome.js +++ b/src/core_plugins/kibana/public/home/components/welcome.js @@ -41,7 +41,7 @@ import { * Shows a full-screen welcome page that gives helpful quick links to beginners. */ export class Welcome extends React.Component { - hideOnEsc = (e) => { + hideOnEsc = e => { if (e.key === 'Escape') { this.props.onSkip(); } @@ -59,21 +59,23 @@ export class Welcome extends React.Component { const { urlBasePath, onSkip } = this.props; return ( -
-
-
+
+
+
- + - +

Welcome to Kibana

- Your window into the Elastic Stack + +

Your window into the Elastic Stack

+
-
+
Try our sample data Explore on my own - )} + } /> diff --git a/src/core_plugins/kibana/public/home/home.less b/src/core_plugins/kibana/public/home/home.less index f2c4764a2b58b7..6333478ecd5c29 100644 --- a/src/core_plugins/kibana/public/home/home.less +++ b/src/core_plugins/kibana/public/home/home.less @@ -1,108 +1,7 @@ -@import (reference) "~ui/styles/variables.less"; - -home-app { - background-color: @globalColorLightestGray; -} - -.home { - min-height: 100vh; - max-width: 1200px; - margin: auto; -} - -.homeDirectory { - background: @white; - margin: 0; - border-left: 1px solid @globalColorLightGray; - border-right: 1px solid @globalColorLightGray; - border-bottom: 1px solid @globalColorLightGray; - padding: 16px; -} - -.homeDirectoryTab { - background-color: @globalColorLightestGray; -} - -.sampleDataSetCard { - flex-grow: 0; -} - -.home-welcome { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: 100000; - background: inherit; - // When sassified, should pull in EUI colors: $euiColorLightestShade, $euiColorEmptyShade - background-image: linear-gradient(0deg, @globalColorLightestGray 0%, white 100%); - color: inherit; - opacity: 0; - overflow: auto; - animation: homeFadeIn 0.5s ease-in 0s forwards; -} - -.home-welcome::before { +.homWelcome::before { content: url(../assets/bg_top_branded.svg); - position: absolute; - top: 0; - right: 0; - z-index: 1; } -.home-welcome::after { +.homWelcome::after { content: url(../assets/bg_bottom_branded.svg); - position: fixed; - bottom: -2px; // Hides an odd space at the bottom of the svg - left: 0; - z-index: 1; -} - -.home-welcome-header { - position: relative; - padding: 32px; - z-index: 10; -} - -.home-welcome-logo { - display: inline-block; - margin-bottom: 24px; - background-color: white; - border-radius: 100%; - padding: 16px; - box-shadow: 0 4px 16px -6px rgba(0, 0, 0, 0.75); -} - -.home-welcome-title { - color: inherit; - font-weight: 400; -} - -.home-welcome-footer-action { - margin-right: 8px; -} - -.welcome-subtitle { - opacity: 0.75; -} - -.home-welcome-content { - position: relative; - margin: auto; - max-width: 512px; - padding-left: 32px; - padding-right: 32px; - z-index: 10; -} - -@keyframes homeFadeIn { - from { - opacity: 0; - transform: translateY(200px), scale(0.75); - } - to { - opacity: 1; - transform: translateY(0), scale(1); - } } diff --git a/src/core_plugins/kibana/public/home/load_tutorials.js b/src/core_plugins/kibana/public/home/load_tutorials.js index e74b083c25b642..8894a0e78b8b8d 100644 --- a/src/core_plugins/kibana/public/home/load_tutorials.js +++ b/src/core_plugins/kibana/public/home/load_tutorials.js @@ -19,7 +19,7 @@ import _ from 'lodash'; import chrome from 'ui/chrome'; -import { notify } from 'ui/notify'; +import { toastNotifications } from 'ui/notify'; const baseUrl = chrome.addBasePath('/api/kibana/home/tutorials'); const headers = new Headers(); @@ -44,7 +44,10 @@ async function loadTutorials() { tutorials = await response.json(); tutorialsLoaded = true; } catch(err) { - notify.error(`Unable to load tutorials, ${err}`); + toastNotifications.addDanger({ + title: 'Unable to load tutorials', + text: err.message, + }); } } diff --git a/src/core_plugins/kibana/public/home/sample_data_sets.js b/src/core_plugins/kibana/public/home/sample_data_sets.js index 363986623bbb90..84823e1c01fca8 100644 --- a/src/core_plugins/kibana/public/home/sample_data_sets.js +++ b/src/core_plugins/kibana/public/home/sample_data_sets.js @@ -17,57 +17,16 @@ * under the License. */ -import chrome from 'ui/chrome'; -import { toastNotifications } from 'ui/notify'; +import { kfetch } from 'ui/kfetch'; -const sampleDataUrl = chrome.addBasePath('/api/sample_data'); -const headers = new Headers({ - Accept: 'application/json', - 'Content-Type': 'application/json', - 'kbn-xsrf': 'kibana', -}); +const sampleDataUrl = '/api/sample_data'; export async function listSampleDataSets() { - try { - const response = await fetch(sampleDataUrl, { - method: 'get', - credentials: 'include', - headers: headers, - }); - - if (response.status >= 300) { - throw new Error(`Request failed with status code: ${response.status}`); - } - - return await response.json(); - } catch (err) { - toastNotifications.addDanger({ - title: `Unable to load sample data sets list`, - text: `${err.message}`, - }); - return []; - } + return await kfetch({ method: 'GET', pathname: sampleDataUrl }); } -export async function installSampleDataSet(id, name, defaultIndex, getConfig, setConfig, clearIndexPatternsCache) { - try { - const response = await fetch(`${sampleDataUrl}/${id}`, { - method: 'post', - credentials: 'include', - headers: headers, - }); - - if (response.status >= 300) { - const body = await response.text(); - throw new Error(`Request failed with status code: ${response.status}, message: ${body}`); - } - } catch (err) { - toastNotifications.addDanger({ - title: `Unable to install sample data set: ${name}`, - text: `${err.message}`, - }); - return; - } +export async function installSampleDataSet(id, defaultIndex, getConfig, setConfig, clearIndexPatternsCache) { + await kfetch({ method: 'POST', pathname: `${sampleDataUrl}/${id}` }); const existingDefaultIndex = await getConfig('defaultIndex'); if (existingDefaultIndex === null) { @@ -75,31 +34,10 @@ export async function installSampleDataSet(id, name, defaultIndex, getConfig, se } clearIndexPatternsCache(); - - toastNotifications.addSuccess({ - title: `${name} installed`, - ['data-test-subj']: 'sampleDataSetInstallToast' - }); } -export async function uninstallSampleDataSet(id, name, defaultIndex, getConfig, setConfig, clearIndexPatternsCache) { - try { - const response = await fetch(`${sampleDataUrl}/${id}`, { - method: 'delete', - credentials: 'include', - headers: headers, - }); - if (response.status >= 300) { - const body = await response.text(); - throw new Error(`Request failed with status code: ${response.status}, message: ${body}`); - } - } catch (err) { - toastNotifications.addDanger({ - title: `Unable to uninstall sample data set`, - text: `${err.message}`, - }); - return; - } +export async function uninstallSampleDataSet(id, defaultIndex, getConfig, setConfig, clearIndexPatternsCache) { + await kfetch({ method: 'DELETE', pathname: `${sampleDataUrl}/${id}` }); const existingDefaultIndex = await getConfig('defaultIndex'); if (existingDefaultIndex && existingDefaultIndex === defaultIndex) { @@ -107,9 +45,4 @@ export async function uninstallSampleDataSet(id, name, defaultIndex, getConfig, } clearIndexPatternsCache(); - - toastNotifications.addSuccess({ - title: `${name} uninstalled`, - ['data-test-subj']: 'sampleDataSetUninstallToast' - }); } diff --git a/src/core_plugins/kibana/public/index.scss b/src/core_plugins/kibana/public/index.scss index 5ae30066a8dff8..713bc1323d7ecc 100644 --- a/src/core_plugins/kibana/public/index.scss +++ b/src/core_plugins/kibana/public/index.scss @@ -1,7 +1,10 @@ -@import '../../../../src/ui/public/styles/styling_constants'; +@import 'ui/public/styles/styling_constants'; // Discover styles -@import 'discover/index'; +@import './discover/index'; + +// Home styles +@import './home/index'; // Dashboard styles // MUST STAY AT THE BOTTOM BECAUSE OF DARK THEME IMPORTS diff --git a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/components/step_index_pattern/components/indices_list/__tests__/__snapshots__/indices_list.test.js.snap b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/components/step_index_pattern/components/indices_list/__tests__/__snapshots__/indices_list.test.js.snap index 943303f0bfdb2b..bdacb3b6c4734c 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/components/step_index_pattern/components/indices_list/__tests__/__snapshots__/indices_list.test.js.snap +++ b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/components/step_index_pattern/components/indices_list/__tests__/__snapshots__/indices_list.test.js.snap @@ -67,6 +67,7 @@ exports[`IndicesList should change pages 1`] = ` } closePopover={[Function]} + hasArrow={true} id="customizablePagination" isOpen={false} ownFocus={false} @@ -79,6 +80,7 @@ exports[`IndicesList should change pages 1`] = ` Array [ @@ -86,6 +88,7 @@ exports[`IndicesList should change pages 1`] = ` , @@ -93,6 +96,7 @@ exports[`IndicesList should change pages 1`] = ` , @@ -100,6 +104,7 @@ exports[`IndicesList should change pages 1`] = ` , @@ -171,6 +176,7 @@ exports[`IndicesList should change per page 1`] = ` } closePopover={[Function]} + hasArrow={true} id="customizablePagination" isOpen={false} ownFocus={false} @@ -183,6 +189,7 @@ exports[`IndicesList should change per page 1`] = ` Array [ @@ -190,6 +197,7 @@ exports[`IndicesList should change per page 1`] = ` , @@ -197,6 +205,7 @@ exports[`IndicesList should change per page 1`] = ` , @@ -204,6 +213,7 @@ exports[`IndicesList should change per page 1`] = ` , @@ -300,6 +310,7 @@ exports[`IndicesList should highlight the query in the matches 1`] = ` } closePopover={[Function]} + hasArrow={true} id="customizablePagination" isOpen={false} ownFocus={false} @@ -312,6 +323,7 @@ exports[`IndicesList should highlight the query in the matches 1`] = ` Array [ @@ -319,6 +331,7 @@ exports[`IndicesList should highlight the query in the matches 1`] = ` , @@ -326,6 +339,7 @@ exports[`IndicesList should highlight the query in the matches 1`] = ` , @@ -333,6 +347,7 @@ exports[`IndicesList should highlight the query in the matches 1`] = ` , @@ -414,6 +429,7 @@ exports[`IndicesList should render normally 1`] = ` } closePopover={[Function]} + hasArrow={true} id="customizablePagination" isOpen={false} ownFocus={false} @@ -426,6 +442,7 @@ exports[`IndicesList should render normally 1`] = ` Array [ @@ -433,6 +450,7 @@ exports[`IndicesList should render normally 1`] = ` , @@ -440,6 +458,7 @@ exports[`IndicesList should render normally 1`] = ` , @@ -447,6 +466,7 @@ exports[`IndicesList should render normally 1`] = ` , @@ -608,6 +628,7 @@ exports[`IndicesList updating props should render all new indices 1`] = ` } closePopover={[Function]} + hasArrow={true} id="customizablePagination" isOpen={false} ownFocus={false} @@ -620,6 +641,7 @@ exports[`IndicesList updating props should render all new indices 1`] = ` Array [ @@ -627,6 +649,7 @@ exports[`IndicesList updating props should render all new indices 1`] = ` , @@ -634,6 +657,7 @@ exports[`IndicesList updating props should render all new indices 1`] = ` , @@ -641,6 +665,7 @@ exports[`IndicesList updating props should render all new indices 1`] = ` , diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/edit_index_pattern.js b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/edit_index_pattern.js index dc7b253d1370f3..80b1f9895b622a 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/edit_index_pattern.js +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/edit_index_pattern.js @@ -22,7 +22,7 @@ import './index_header'; import './create_edit_field'; import { KbnUrlProvider } from 'ui/url'; import { IndicesEditSectionsProvider } from './edit_sections'; -import { fatalError } from 'ui/notify'; +import { fatalError, toastNotifications } from 'ui/notify'; import uiRoutes from 'ui/routes'; import { uiModules } from 'ui/modules'; import template from './edit_index_pattern.html'; @@ -181,8 +181,7 @@ uiRoutes uiModules.get('apps/management') .controller('managementIndicesEdit', function ( - $scope, $location, $route, config, indexPatterns, Notifier, Private, AppState, docTitle, confirmModal) { - const notify = new Notifier(); + $scope, $location, $route, config, indexPatterns, Private, AppState, docTitle, confirmModal) { const $state = $scope.state = new AppState(); const { fieldWildcardMatcher } = Private(FieldWildcardProvider); @@ -292,7 +291,7 @@ uiModules.get('apps/management') const errorMessage = i18n.translate('kbn.management.editIndexPattern.notDateErrorMessage', { defaultMessage: 'That field is a {fieldType} not a date.', values: { fieldType: field.type } }); - notify.error(errorMessage); + toastNotifications.addDanger(errorMessage); return; } $scope.indexPattern.timeFieldName = field.name; 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 c72d3e2f64c58b..f219c199b1d870 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 @@ -17,15 +17,7 @@ exports[`AdvancedSettings should render normally 1`] = ` component="div" grow={true} > - -

- Settings -

-
+ - -

- Settings -

-
+
{ - if(a === DEFAULT_CATEGORY) return -1; - if(b === DEFAULT_CATEGORY) return 1; - if(a > b) return 1; + if (a === DEFAULT_CATEGORY) return -1; + if (b === DEFAULT_CATEGORY) return 1; + if (a > b) return 1; return a === b ? 0 : -1; }); @@ -133,13 +136,13 @@ export class AdvancedSettings extends Component { render() { const { filteredSettings, query } = this.state; + const PageTitle = getSettingsComponent(PAGE_TITLE_COMPONENT); + return (
- -

Settings

-
+
- +
{ + it('should allow a component to be registered', () => { + const component = {}; + expect(tryRegisterSettingsComponent('tryTest1', component)).toEqual(true); + }); + + it('should return false if the component is already registered, and not allow an override', () => { + const component = {}; + expect(tryRegisterSettingsComponent('tryTest2', component)).toEqual(true); + + const updatedComponent = { updated: 'yay' }; + expect(tryRegisterSettingsComponent('tryTest2', updatedComponent)).toEqual(false); + expect(getSettingsComponent('tryTest2')).toBe(component); + }); +}); + +describe('registerSettingsComponent', () => { + it('should allow a component to be registered', () => { + const component = {}; + registerSettingsComponent('test', component); + }); + + it('should disallow registering a component with a duplicate id', () => { + const component = {}; + registerSettingsComponent('test2', component); + expect(() => registerSettingsComponent('test2', 'some other component')).toThrowErrorMatchingSnapshot(); + }); + + it('should allow a component to be overriden', () => { + const component = {}; + registerSettingsComponent('test3', component); + + const anotherComponent = { 'anotherComponent': 'ok' }; + registerSettingsComponent('test3', anotherComponent, true); + + expect(getSettingsComponent('test3')).toBe(anotherComponent); + }); + + it('should set a displayName for the component if one does not exist', () => { + const component = {}; + registerSettingsComponent('display_name_component', component); + + expect(component.displayName).toEqual('display_name_component'); + }); + + it('should not set a displayName for the component if one already exists', () => { + const component = { + displayName: '' + }; + + registerSettingsComponent('another_display_name_component', component); + + expect(component.displayName).toEqual(''); + }); +}); + +describe('getSettingsComponent', () => { + it('should allow a component to be retrieved', () => { + const component = {}; + registerSettingsComponent('test4', component); + expect(getSettingsComponent('test4')).toBe(component); + }); + + it('should throw an error when requesting a component that does not exist', () => { + expect(() => getSettingsComponent('does not exist')).toThrowErrorMatchingSnapshot(); + }); +}); \ No newline at end of file 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 new file mode 100644 index 00000000000000..b88fb11d63bbcf --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/components/default_component_registry.js @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { tryRegisterSettingsComponent } from './component_registry'; +import { PageTitle } from './page_title'; + +export const PAGE_TITLE_COMPONENT = 'advanced_settings_page_title'; + +export function registerDefaultComponents() { + tryRegisterSettingsComponent(PAGE_TITLE_COMPONENT, PageTitle); +} \ No newline at end of file diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/default_component_registry.test.js b/src/core_plugins/kibana/public/management/sections/settings/components/default_component_registry.test.js new file mode 100644 index 00000000000000..2c5b3c9b466137 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/components/default_component_registry.test.js @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { registerDefaultComponents, PAGE_TITLE_COMPONENT } from './default_component_registry'; +import { getSettingsComponent, registerSettingsComponent } from './component_registry'; +import { PageTitle } from './page_title'; + +describe('default_component_registry', () => { + it('should register default components with the registry', () => { + registerDefaultComponents(); + expect(getSettingsComponent(PAGE_TITLE_COMPONENT)).toEqual(PageTitle); + }); + + it('should be able to call "registerDefaultComponents" several times without throwing', () => { + registerDefaultComponents(); + registerDefaultComponents(); + registerDefaultComponents(); + }); + + it('should not override components if they are already registered', () => { + const newComponent = {}; + registerSettingsComponent(PAGE_TITLE_COMPONENT, newComponent, true); + registerDefaultComponents(); + + expect(getSettingsComponent(PAGE_TITLE_COMPONENT)).toEqual(newComponent); + }); +}); \ No newline at end of file diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/page_title/__snapshots__/page_title.test.js.snap b/src/core_plugins/kibana/public/management/sections/settings/components/page_title/__snapshots__/page_title.test.js.snap new file mode 100644 index 00000000000000..f93bd34d9312e3 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/components/page_title/__snapshots__/page_title.test.js.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PageTitle should render normally 1`] = ` + +

+ Settings +

+
+`; diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/page_title/index.js b/src/core_plugins/kibana/public/management/sections/settings/components/page_title/index.js new file mode 100644 index 00000000000000..f5553eb971ac34 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/components/page_title/index.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 { PageTitle } from './page_title'; \ No newline at end of file diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/page_title/page_title.js b/src/core_plugins/kibana/public/management/sections/settings/components/page_title/page_title.js new file mode 100644 index 00000000000000..d76cb6bc52856a --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/components/page_title/page_title.js @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { + EuiText +} from '@elastic/eui'; + +export const PageTitle = () => { + return ( + +

Settings

+
+ ); +}; \ No newline at end of file diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/page_title/page_title.test.js b/src/core_plugins/kibana/public/management/sections/settings/components/page_title/page_title.test.js new file mode 100644 index 00000000000000..0b3edd71764bb2 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/components/page_title/page_title.test.js @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { PageTitle } from './page_title'; + +describe('PageTitle', () => { + it('should render normally', () => { + expect(shallow()).toMatchSnapshot(); + }); +}); \ No newline at end of file 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 5d78579ea7087b..9e1fb23ad3b590 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,11 +17,9 @@ * under the License. */ -/* eslint-disable max-len */ +import { i18n } from '@kbn/i18n'; -import { i18n } from '@kbn/i18n'; - -export const NODE_CLIENT_INSTRUCTIONS = [ +export const createNodeClientInstructions = () => [ { title: i18n.translate('kbn.server.tutorials.apm.nodeClient.install.title', { defaultMessage: 'Install the APM agent', @@ -62,7 +60,9 @@ var apm = require('elastic-apm-node').start({curlyOpen} serverUrl: '' {curlyClose})`.split('\n'), 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}).', + // eslint-disable-next-line no-multi-str + 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', @@ -71,7 +71,7 @@ var apm = require('elastic-apm-node').start({curlyOpen} }, ]; -export const DJANGO_CLIENT_INSTRUCTIONS = [ +export const createDjangoClientInstructions = () => [ { title: i18n.translate('kbn.server.tutorials.apm.djangoClient.install.title', { defaultMessage: 'Install the APM agent', @@ -133,7 +133,7 @@ MIDDLEWARE = ( }, ]; -export const FLASK_CLIENT_INSTRUCTIONS = [ +export const createFlaskClientInstructions = () => [ { title: i18n.translate('kbn.server.tutorials.apm.flaskClient.install.title', { defaultMessage: 'Install the APM agent', @@ -192,7 +192,7 @@ apm = ElasticAPM(app)`.split('\n'), }, ]; -export const RAILS_CLIENT_INSTRUCTIONS = [ +export const createRailsClientInstructions = () => [ { title: i18n.translate('kbn.server.tutorials.apm.railsClient.install.title', { defaultMessage: 'Install the APM agent', @@ -228,7 +228,7 @@ export const RAILS_CLIENT_INSTRUCTIONS = [ }, ]; -export const RACK_CLIENT_INSTRUCTIONS = [ +export const createRackClientInstructions = () => [ { title: i18n.translate('kbn.server.tutorials.apm.rackClient.install.title', { defaultMessage: 'Install the APM agent', @@ -302,7 +302,7 @@ export const RACK_CLIENT_INSTRUCTIONS = [ }, ]; -export const JS_CLIENT_INSTRUCTIONS = [ +export const createJsClientInstructions = () => [ { title: i18n.translate('kbn.server.tutorials.apm.jsClient.enableRealUserMonitoring.title', { defaultMessage: 'Enable Real User Monitoring support in the APM server', @@ -354,7 +354,7 @@ var apm = initApm({curlyOpen} }, ]; -export const GO_CLIENT_INSTRUCTIONS = [ +export const createGoClientInstructions = () => [ { title: i18n.translate('kbn.server.tutorials.apm.goClient.install.title', { defaultMessage: 'Install the APM agent', @@ -433,7 +433,7 @@ code.\n\n**Warning: The Go agent is currently in Beta and not meant for producti }, ]; -export const JAVA_CLIENT_INSTRUCTIONS = [ +export const createJavaClientInstructions = () => [ { title: i18n.translate('kbn.server.tutorials.apm.javaClient.download.title', { defaultMessage: 'Download the APM agent', 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 bbb04bb03f4a68..6ee9bb1203940a 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,9 +17,9 @@ * under the License. */ -import { i18n } from '@kbn/i18n'; +import { i18n } from '@kbn/i18n'; -export const EDIT_CONFIG = { +export const createEditConfig = () => ({ title: i18n.translate('kbn.server.tutorials.apm.editConfig.title', { defaultMessage: 'Edit the configuration', }), @@ -34,38 +34,42 @@ credentials in the `apm-server.yml` config file.', ' username: ', ' password: ', ], -}; +}); -const START_SERVER = { +const createStartServer = () => ({ 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() { + const START_SERVER = createStartServer(); -export const START_SERVER_UNIX = { - title: START_SERVER.title, - textPre: START_SERVER.textPre, - commands: ['./apm-server -e'], -}; + return { + title: START_SERVER.title, + textPre: START_SERVER.textPre, + commands: ['./apm-server -e'], + }; +} -const DOWNLOAD_SERVER_TITLE = i18n.translate('kbn.server.tutorials.apm.downloadServer.title', { +const createDownloadServerTitle = () => i18n.translate('kbn.server.tutorials.apm.downloadServer.title', { defaultMessage: 'Download and unpack APM Server', }); -export const DOWNLOAD_SERVER_OSX = { - title: DOWNLOAD_SERVER_TITLE, +export const createDownloadServerOsx = () => ({ + title: createDownloadServerTitle(), commands: [ 'curl -L -O https://artifacts.elastic.co/downloads/apm-server/apm-server-{config.kibana.version}-darwin-x86_64.tar.gz', 'tar xzvf apm-server-{config.kibana.version}-darwin-x86_64.tar.gz', 'cd apm-server-{config.kibana.version}-darwin-x86_64/', ], -}; +}); -export const DOWNLOAD_SERVER_DEB = { - title: DOWNLOAD_SERVER_TITLE, +export const createDownloadServerDeb = () => ({ + title: createDownloadServerTitle(), commands: [ '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', @@ -74,10 +78,10 @@ export const DOWNLOAD_SERVER_DEB = { defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({downloadPageLink}).', values: { downloadPageLink: '{config.docs.base_url}downloads/apm/apm-server' }, }), -}; +}); -export const DOWNLOAD_SERVER_RPM = { - title: DOWNLOAD_SERVER_TITLE, +export const createDownloadServerRpm = () => ({ + title: createDownloadServerTitle(), commands: [ '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', @@ -86,40 +90,45 @@ export const DOWNLOAD_SERVER_RPM = { 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() { + const START_SERVER = createStartServer(); -export const WINDOWS_SERVER_INSTRUCTIONS = [ - { - title: DOWNLOAD_SERVER_TITLE, - textPre: i18n.translate('kbn.server.tutorials.apm.windowsServerInstructions.textPre', { - // eslint-disable-next-line no-multi-str - defaultMessage: '1. Download the APM Server Windows zip file from the \ + return [ + { + title: createDownloadServerTitle(), + textPre: i18n.translate('kbn.server.tutorials.apm.windowsServerInstructions.textPre', { + // eslint-disable-next-line no-multi-str + 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: i18n.translate('kbn.server.tutorials.apm.windowsServerInstructions.textPost', { - // eslint-disable-next-line no-multi-str - defaultMessage: 'Note: If script execution is disabled on your system, you need to set the execution policy for the current session \ + 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: i18n.translate('kbn.server.tutorials.apm.windowsServerInstructions.textPost', { + // eslint-disable-next-line no-multi-str + 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: `PowerShell.exe -ExecutionPolicy UnRestricted -File .\\install-service-apm-server.ps1`.', - }), - }, - EDIT_CONFIG, - { - title: START_SERVER.title, - textPre: START_SERVER.textPre, - commands: ['apm-server.exe -e'], - }, -]; + }), + }, + createEditConfig(), + { + title: START_SERVER.title, + textPre: START_SERVER.textPre, + commands: ['apm-server.exe -e'], + }, + ]; +} 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 78030ef2063634..453184633d5459 100644 --- a/src/core_plugins/kibana/server/tutorials/apm/elastic_cloud.js +++ b/src/core_plugins/kibana/server/tutorials/apm/elastic_cloud.js @@ -17,21 +17,21 @@ * under the License. */ -import { i18n } from '@kbn/i18n'; +import { i18n } from '@kbn/i18n'; import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; import { - NODE_CLIENT_INSTRUCTIONS, - DJANGO_CLIENT_INSTRUCTIONS, - FLASK_CLIENT_INSTRUCTIONS, - RAILS_CLIENT_INSTRUCTIONS, - RACK_CLIENT_INSTRUCTIONS, - JS_CLIENT_INSTRUCTIONS, - GO_CLIENT_INSTRUCTIONS, - JAVA_CLIENT_INSTRUCTIONS, + createNodeClientInstructions, + createDjangoClientInstructions, + createFlaskClientInstructions, + createRailsClientInstructions, + createRackClientInstructions, + createJsClientInstructions, + createGoClientInstructions, + createJavaClientInstructions, } from './apm_client_instructions'; -const SERVER_URL_INSTRUCTION = { +const createServerUrlInstruction = () => ({ title: i18n.translate('kbn.server.tutorials.apm.serverUrlInstruction.title', { defaultMessage: 'APM Server endpoint', }), @@ -40,48 +40,52 @@ const SERVER_URL_INSTRUCTION = { 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 const ELASTIC_CLOUD_INSTRUCTIONS = { - instructionSets: [ - { - title: i18n.translate('kbn.server.tutorials.apm.elasticCloudInstructions.title', { - defaultMessage: 'APM Agents', - }), - instructionVariants: [ - { - id: INSTRUCTION_VARIANT.NODE, - instructions: [SERVER_URL_INSTRUCTION, ...NODE_CLIENT_INSTRUCTIONS], - }, - { - id: INSTRUCTION_VARIANT.DJANGO, - instructions: [SERVER_URL_INSTRUCTION, ...DJANGO_CLIENT_INSTRUCTIONS], - }, - { - id: INSTRUCTION_VARIANT.FLASK, - instructions: [SERVER_URL_INSTRUCTION, ...FLASK_CLIENT_INSTRUCTIONS], - }, - { - id: INSTRUCTION_VARIANT.RAILS, - instructions: [SERVER_URL_INSTRUCTION, ...RAILS_CLIENT_INSTRUCTIONS], - }, - { - id: INSTRUCTION_VARIANT.RACK, - instructions: [SERVER_URL_INSTRUCTION, ...RACK_CLIENT_INSTRUCTIONS], - }, - { - id: INSTRUCTION_VARIANT.JS, - instructions: [SERVER_URL_INSTRUCTION, ...JS_CLIENT_INSTRUCTIONS], - }, - { - id: INSTRUCTION_VARIANT.GO, - instructions: [SERVER_URL_INSTRUCTION, ...GO_CLIENT_INSTRUCTIONS], - }, - { - id: INSTRUCTION_VARIANT.JAVA, - instructions: [SERVER_URL_INSTRUCTION, ...JAVA_CLIENT_INSTRUCTIONS], - }, - ], - }, - ], -}; +export function createElasticCloudInstructions() { + const SERVER_URL_INSTRUCTION = createServerUrlInstruction(); + + return { + instructionSets: [ + { + title: i18n.translate('kbn.server.tutorials.apm.elasticCloudInstructions.title', { + defaultMessage: 'APM Agents', + }), + instructionVariants: [ + { + id: INSTRUCTION_VARIANT.NODE, + instructions: [SERVER_URL_INSTRUCTION, ...createNodeClientInstructions()], + }, + { + id: INSTRUCTION_VARIANT.DJANGO, + instructions: [SERVER_URL_INSTRUCTION, ...createDjangoClientInstructions()], + }, + { + id: INSTRUCTION_VARIANT.FLASK, + instructions: [SERVER_URL_INSTRUCTION, ...createFlaskClientInstructions()], + }, + { + id: INSTRUCTION_VARIANT.RAILS, + instructions: [SERVER_URL_INSTRUCTION, ...createRailsClientInstructions()], + }, + { + id: INSTRUCTION_VARIANT.RACK, + instructions: [SERVER_URL_INSTRUCTION, ...createRackClientInstructions()], + }, + { + id: INSTRUCTION_VARIANT.JS, + instructions: [SERVER_URL_INSTRUCTION, ...createJsClientInstructions()], + }, + { + id: INSTRUCTION_VARIANT.GO, + instructions: [SERVER_URL_INSTRUCTION, ...createGoClientInstructions()], + }, + { + id: INSTRUCTION_VARIANT.JAVA, + instructions: [SERVER_URL_INSTRUCTION, ...createJavaClientInstructions()], + }, + ], + }, + ], + }; +} diff --git a/src/core_plugins/kibana/server/tutorials/apm/index.js b/src/core_plugins/kibana/server/tutorials/apm/index.js index d18513fc29f151..d0c5b1b4742132 100644 --- a/src/core_plugins/kibana/server/tutorials/apm/index.js +++ b/src/core_plugins/kibana/server/tutorials/apm/index.js @@ -17,10 +17,10 @@ * under the License. */ -import { i18n } from '@kbn/i18n'; +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions } from './on_prem'; -import { ELASTIC_CLOUD_INSTRUCTIONS } from './elastic_cloud'; +import { createElasticCloudInstructions } from './elastic_cloud'; import { getSavedObjects } from './saved_objects/get_saved_objects'; const apmIntro = i18n.translate('kbn.server.tutorials.apm.introduction', { @@ -47,10 +47,11 @@ export function apmSpecProvider(server) { linkLabel: i18n.translate('kbn.server.tutorials.apm.specProvider.artifacts.dashboards.linkLabel', { defaultMessage: 'APM dashboard', }), - isOverview: true - } - ] + isOverview: true, + }, + ], }; + if (isEnabled(config)) { artifacts.application = { path: '/app/apm', @@ -78,7 +79,7 @@ It allows you to monitor the performance of thousands of applications in real ti euiIconType: 'apmApp', artifacts: artifacts, onPrem: onPremInstructions(apmIndexPattern), - elasticCloud: ELASTIC_CLOUD_INSTRUCTIONS, + elasticCloud: createElasticCloudInstructions(), previewImagePath: '/plugins/kibana/home/tutorial_resources/apm/apm.png', savedObjects: getSavedObjects(apmIndexPattern), savedObjectsInstallMsg: i18n.translate('kbn.server.tutorials.apm.specProvider.savedObjectsInstallMsg', { 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 8ef5ba0fede0fb..a75ce27384b21c 100644 --- a/src/core_plugins/kibana/server/tutorials/apm/on_prem.js +++ b/src/core_plugins/kibana/server/tutorials/apm/on_prem.js @@ -17,28 +17,30 @@ * under the License. */ -import { i18n } from '@kbn/i18n'; +import { i18n } from '@kbn/i18n'; import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; import { - WINDOWS_SERVER_INSTRUCTIONS, - EDIT_CONFIG, - START_SERVER_UNIX, - DOWNLOAD_SERVER_RPM, - DOWNLOAD_SERVER_DEB, - DOWNLOAD_SERVER_OSX, + createWindowsServerInstructions, + createEditConfig, + createStartServerUnix, + createDownloadServerRpm, + createDownloadServerDeb, + createDownloadServerOsx, } from './apm_server_instructions'; import { - NODE_CLIENT_INSTRUCTIONS, - DJANGO_CLIENT_INSTRUCTIONS, - FLASK_CLIENT_INSTRUCTIONS, - RAILS_CLIENT_INSTRUCTIONS, - RACK_CLIENT_INSTRUCTIONS, - JS_CLIENT_INSTRUCTIONS, - GO_CLIENT_INSTRUCTIONS, - JAVA_CLIENT_INSTRUCTIONS, + createNodeClientInstructions, + createDjangoClientInstructions, + createFlaskClientInstructions, + createRailsClientInstructions, + createRackClientInstructions, + createJsClientInstructions, + createGoClientInstructions, + createJavaClientInstructions, } from './apm_client_instructions'; export function onPremInstructions(apmIndexPattern) { + const EDIT_CONFIG = createEditConfig(); + const START_SERVER_UNIX = createStartServerUnix(); return { instructionSets: [ @@ -49,31 +51,19 @@ export function onPremInstructions(apmIndexPattern) { instructionVariants: [ { id: INSTRUCTION_VARIANT.OSX, - instructions: [ - DOWNLOAD_SERVER_OSX, - EDIT_CONFIG, - START_SERVER_UNIX, - ], + instructions: [createDownloadServerOsx(), EDIT_CONFIG, START_SERVER_UNIX], }, { id: INSTRUCTION_VARIANT.DEB, - instructions: [ - DOWNLOAD_SERVER_DEB, - EDIT_CONFIG, - START_SERVER_UNIX, - ], + instructions: [createDownloadServerDeb(), EDIT_CONFIG, START_SERVER_UNIX], }, { id: INSTRUCTION_VARIANT.RPM, - instructions: [ - DOWNLOAD_SERVER_RPM, - EDIT_CONFIG, - START_SERVER_UNIX, - ], + instructions: [createDownloadServerRpm(), EDIT_CONFIG, START_SERVER_UNIX], }, { id: INSTRUCTION_VARIANT.WINDOWS, - instructions: WINDOWS_SERVER_INSTRUCTIONS, + instructions: createWindowsServerInstructions(), }, ], statusCheck: { @@ -113,35 +103,35 @@ export function onPremInstructions(apmIndexPattern) { instructionVariants: [ { id: INSTRUCTION_VARIANT.NODE, - instructions: NODE_CLIENT_INSTRUCTIONS, + instructions: createNodeClientInstructions(), }, { id: INSTRUCTION_VARIANT.DJANGO, - instructions: DJANGO_CLIENT_INSTRUCTIONS, + instructions: createDjangoClientInstructions(), }, { id: INSTRUCTION_VARIANT.FLASK, - instructions: FLASK_CLIENT_INSTRUCTIONS, + instructions: createFlaskClientInstructions(), }, { id: INSTRUCTION_VARIANT.RAILS, - instructions: RAILS_CLIENT_INSTRUCTIONS, + instructions: createRailsClientInstructions(), }, { id: INSTRUCTION_VARIANT.RACK, - instructions: RACK_CLIENT_INSTRUCTIONS, + instructions: createRackClientInstructions(), }, { id: INSTRUCTION_VARIANT.JS, - instructions: JS_CLIENT_INSTRUCTIONS, + instructions: createJsClientInstructions(), }, { id: INSTRUCTION_VARIANT.GO, - instructions: GO_CLIENT_INSTRUCTIONS, + instructions: createGoClientInstructions(), }, { id: INSTRUCTION_VARIANT.JAVA, - instructions: JAVA_CLIENT_INSTRUCTIONS, + instructions: createJavaClientInstructions(), }, ], statusCheck: { diff --git a/src/core_plugins/kibana/server/tutorials/ceph_metrics/index.js b/src/core_plugins/kibana/server/tutorials/ceph_metrics/index.js index 233cd08586278c..65668fd8d21445 100644 --- a/src/core_plugins/kibana/server/tutorials/ceph_metrics/index.js +++ b/src/core_plugins/kibana/server/tutorials/ceph_metrics/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/metricbeat_instructions'; @@ -24,15 +25,27 @@ export function cephMetricsSpecProvider() { const moduleName = 'ceph'; return { id: 'cephMetrics', - name: 'Ceph metrics', + name: i18n.translate('kbn.server.tutorials.cephMetrics.nameTitle', { + defaultMessage: 'Ceph metrics', + }), isBeta: true, category: TUTORIAL_CATEGORY.METRICS, - shortDescription: 'Fetch internal metrics from the Ceph server.', - longDescription: 'The `ceph` Metricbeat module fetches internal metrics from Ceph.' + - ' [Learn more]({config.docs.beats.metricbeat}/metricbeat-module-ceph.html).', + shortDescription: i18n.translate('kbn.server.tutorials.cephMetrics.shortDescription', { + defaultMessage: 'Fetch internal metrics from the Ceph server.', + }), + longDescription: i18n.translate('kbn.server.tutorials.cephMetrics.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `ceph` Metricbeat module fetches internal metrics from Ceph. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-ceph.html', + }, + }), artifacts: { application: { - label: 'Discover', + label: i18n.translate('kbn.server.tutorials.cephMetrics.artifacts.application.label', { + defaultMessage: 'Discover', + }), path: '/app/kibana#/discover' }, dashboards: [], diff --git a/src/core_plugins/kibana/server/tutorials/couchbase_metrics/index.js b/src/core_plugins/kibana/server/tutorials/couchbase_metrics/index.js index 8008d040b3e1aa..e8d52498eb31c6 100644 --- a/src/core_plugins/kibana/server/tutorials/couchbase_metrics/index.js +++ b/src/core_plugins/kibana/server/tutorials/couchbase_metrics/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/metricbeat_instructions'; @@ -24,15 +25,27 @@ export function couchbaseMetricsSpecProvider() { const moduleName = 'couchbase'; return { id: 'couchbaseMetrics', - name: 'Couchbase metrics', + name: i18n.translate('kbn.server.tutorials.couchbaseMetrics.nameTitle', { + defaultMessage: 'Couchbase metrics', + }), isBeta: true, category: TUTORIAL_CATEGORY.METRICS, - shortDescription: 'Fetch internal metrics from Couchbase.', - longDescription: 'The `couchbase` Metricbeat module fetches internal metrics from Couchbase.' + - ' [Learn more]({config.docs.beats.metricbeat}/metricbeat-module-couchbase.html).', + shortDescription: i18n.translate('kbn.server.tutorials.couchbaseMetrics.shortDescription', { + defaultMessage: 'Fetch internal metrics from Couchbase.', + }), + longDescription: i18n.translate('kbn.server.tutorials.couchbaseMetrics.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `couchbase` Metricbeat module fetches internal metrics from Couchbase. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-couchbase.html', + }, + }), artifacts: { application: { - label: 'Discover', + label: i18n.translate('kbn.server.tutorials.couchbaseMetrics.artifacts.application.label', { + defaultMessage: 'Discover', + }), path: '/app/kibana#/discover' }, dashboards: [], diff --git a/src/core_plugins/kibana/server/tutorials/docker_metrics/index.js b/src/core_plugins/kibana/server/tutorials/docker_metrics/index.js index 68b493bf6c1a0b..3d727874215015 100644 --- a/src/core_plugins/kibana/server/tutorials/docker_metrics/index.js +++ b/src/core_plugins/kibana/server/tutorials/docker_metrics/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/metricbeat_instructions'; @@ -24,17 +25,29 @@ export function dockerMetricsSpecProvider() { const moduleName = 'docker'; return { id: 'dockerMetrics', - name: 'Docker metrics', + name: i18n.translate('kbn.server.tutorials.dockerMetrics.nameTitle', { + defaultMessage: 'Docker metrics', + }), category: TUTORIAL_CATEGORY.METRICS, - shortDescription: 'Fetch metrics about your Docker containers.', - longDescription: 'The `docker` Metricbeat module fetches metrics from the Docker server.' + - ' [Learn more]({config.docs.beats.metricbeat}/metricbeat-module-docker.html).', + shortDescription: i18n.translate('kbn.server.tutorials.dockerMetrics.shortDescription', { + defaultMessage: 'Fetch metrics about your Docker containers.', + }), + longDescription: i18n.translate('kbn.server.tutorials.dockerMetrics.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `docker` Metricbeat module fetches metrics from the Docker server. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-docker.html', + }, + }), euiIconType: 'logoDocker', artifacts: { dashboards: [ { id: 'AV4REOpp5NkDleZmzKkE', - linkLabel: 'Docker metrics dashboard', + linkLabel: i18n.translate('kbn.server.tutorials.dockerMetrics.artifacts.dashboards.linkLabel', { + defaultMessage: 'Docker metrics dashboard', + }), isOverview: true } ], diff --git a/src/core_plugins/kibana/server/tutorials/dropwizard_metrics/index.js b/src/core_plugins/kibana/server/tutorials/dropwizard_metrics/index.js index 02d15c4d6388be..96c4daf3255f06 100644 --- a/src/core_plugins/kibana/server/tutorials/dropwizard_metrics/index.js +++ b/src/core_plugins/kibana/server/tutorials/dropwizard_metrics/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/metricbeat_instructions'; @@ -24,15 +25,27 @@ export function dropwizardMetricsSpecProvider() { const moduleName = 'dropwizard'; return { id: 'dropwizardMetrics', - name: 'Dropwizard metrics', + name: i18n.translate('kbn.server.tutorials.dropwizardMetrics.nameTitle', { + defaultMessage: 'Dropwizard metrics', + }), isBeta: true, category: TUTORIAL_CATEGORY.METRICS, - shortDescription: 'Fetch internal metrics from Dropwizard Java application.', - longDescription: 'The `dropwizard` Metricbeat module fetches internal metrics from Dropwizard Java Application.' + - ' [Learn more]({config.docs.beats.metricbeat}/metricbeat-module-dropwizard.html).', + shortDescription: i18n.translate('kbn.server.tutorials.dropwizardMetrics.shortDescription', { + defaultMessage: 'Fetch internal metrics from Dropwizard Java application.', + }), + longDescription: i18n.translate('kbn.server.tutorials.dropwizardMetrics.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `dropwizard` Metricbeat module fetches internal metrics from Dropwizard Java Application. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-dropwizard.html', + }, + }), artifacts: { application: { - label: 'Discover', + label: i18n.translate('kbn.server.tutorials.dropwizardMetrics.artifacts.application.label', { + defaultMessage: 'Discover', + }), path: '/app/kibana#/discover' }, dashboards: [], diff --git a/src/core_plugins/kibana/server/tutorials/elasticsearch_logs/index.js b/src/core_plugins/kibana/server/tutorials/elasticsearch_logs/index.js index ea9f7bd8dee036..c073968ce291d6 100644 --- a/src/core_plugins/kibana/server/tutorials/elasticsearch_logs/index.js +++ b/src/core_plugins/kibana/server/tutorials/elasticsearch_logs/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/filebeat_instructions'; @@ -27,16 +28,28 @@ export function elasticsearchLogsSpecProvider() { const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS']; return { id: 'elasticsearchLogs', - name: 'Elasticsearch logs', + name: i18n.translate('kbn.server.tutorials.elasticsearchLogs.nameTitle', { + defaultMessage: 'Elasticsearch logs', + }), category: TUTORIAL_CATEGORY.LOGGING, isBeta: true, - shortDescription: 'Collect and parse logs created by Elasticsearch.', - longDescription: 'The `elasticsearch` Filebeat module parses logs created by Elasticsearch.' + - ' [Learn more]({config.docs.beats.filebeat}/filebeat-module-elasticsearch.html).', + shortDescription: i18n.translate('kbn.server.tutorials.elasticsearchLogs.shortDescription', { + defaultMessage: 'Collect and parse logs created by Elasticsearch.', + }), + longDescription: i18n.translate('kbn.server.tutorials.elasticsearchLogs.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `elasticsearch` Filebeat module parses logs created by Elasticsearch. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-elasticsearch.html', + }, + }), euiIconType: 'logoElasticsearch', artifacts: { application: { - label: 'Discover', + label: i18n.translate('kbn.server.tutorials.elasticsearchLogs.artifacts.application.label', { + defaultMessage: 'Discover', + }), path: '/app/kibana#/discover' }, dashboards: [], diff --git a/src/core_plugins/kibana/server/tutorials/elasticsearch_metrics/index.js b/src/core_plugins/kibana/server/tutorials/elasticsearch_metrics/index.js index ef592a0a3839e6..8c5c63e1bcf4ae 100644 --- a/src/core_plugins/kibana/server/tutorials/elasticsearch_metrics/index.js +++ b/src/core_plugins/kibana/server/tutorials/elasticsearch_metrics/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/metricbeat_instructions'; @@ -24,16 +25,28 @@ export function elasticsearchMetricsSpecProvider() { const moduleName = 'elasticsearch'; return { id: 'elasticsearchMetrics', - name: 'Elasticsearch metrics', + name: i18n.translate('kbn.server.tutorials.elasticsearchMetrics.nameTitle', { + defaultMessage: 'Elasticsearch metrics', + }), isBeta: true, category: TUTORIAL_CATEGORY.METRICS, - shortDescription: 'Fetch internal metrics from Elasticsearch.', - longDescription: 'The `elasticsearch` Metricbeat module fetches internal metrics from Elasticsearch.' + - ' [Learn more]({config.docs.beats.metricbeat}/metricbeat-module-elasticsearch.html).', + shortDescription: i18n.translate('kbn.server.tutorials.elasticsearchMetrics.shortDescription', { + defaultMessage: 'Fetch internal metrics from Elasticsearch.', + }), + longDescription: i18n.translate('kbn.server.tutorials.elasticsearchMetrics.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `elasticsearch` Metricbeat module fetches internal metrics from Elasticsearch. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-elasticsearch.html', + }, + }), euiIconType: 'logoElasticsearch', artifacts: { application: { - label: 'Discover', + label: i18n.translate('kbn.server.tutorials.elasticsearchMetrics.artifacts.application.label', { + defaultMessage: 'Discover', + }), path: '/app/kibana#/discover' }, dashboards: [], diff --git a/src/core_plugins/kibana/server/tutorials/etcd_metrics/index.js b/src/core_plugins/kibana/server/tutorials/etcd_metrics/index.js index de6505d1ee1b23..6e43d697dfd44d 100644 --- a/src/core_plugins/kibana/server/tutorials/etcd_metrics/index.js +++ b/src/core_plugins/kibana/server/tutorials/etcd_metrics/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/metricbeat_instructions'; @@ -24,15 +25,27 @@ export function etcdMetricsSpecProvider() { const moduleName = 'etcd'; return { id: 'etcdMetrics', - name: 'Etcd metrics', + name: i18n.translate('kbn.server.tutorials.etcdMetrics.nameTitle', { + defaultMessage: 'Etcd metrics', + }), isBeta: true, category: TUTORIAL_CATEGORY.METRICS, - shortDescription: 'Fetch internal metrics from the Etcd server.', - longDescription: 'The `etcd` Metricbeat module fetches internal metrics from Etcd.' + - ' [Learn more]({config.docs.beats.metricbeat}/metricbeat-module-etcd.html).', + shortDescription: i18n.translate('kbn.server.tutorials.etcdMetrics.shortDescription', { + defaultMessage: 'Fetch internal metrics from the Etcd server.', + }), + longDescription: i18n.translate('kbn.server.tutorials.etcdMetrics.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `etcd` Metricbeat module fetches internal metrics from Etcd. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-etcd.html', + }, + }), artifacts: { application: { - label: 'Discover', + label: i18n.translate('kbn.server.tutorials.etcdMetrics.artifacts.application.label', { + defaultMessage: 'Discover', + }), path: '/app/kibana#/discover' }, dashboards: [], diff --git a/src/core_plugins/kibana/server/tutorials/golang_metrics/index.js b/src/core_plugins/kibana/server/tutorials/golang_metrics/index.js index e81b246c5074e8..a7a71dc870515e 100644 --- a/src/core_plugins/kibana/server/tutorials/golang_metrics/index.js +++ b/src/core_plugins/kibana/server/tutorials/golang_metrics/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/metricbeat_instructions'; @@ -24,17 +25,30 @@ export function golangMetricsSpecProvider() { const moduleName = 'golang'; return { id: moduleName + 'Metrics', - name: 'Golang metrics', + name: i18n.translate('kbn.server.tutorials.golangMetrics.nameTitle', { + defaultMessage: 'Golang metrics', + }), isBeta: true, category: TUTORIAL_CATEGORY.METRICS, - shortDescription: 'Fetch internal metrics from a Golang app.', - longDescription: 'The `' + moduleName + '` Metricbeat module fetches internal metrics from a Golang app.' + - ' [Learn more]({config.docs.beats.metricbeat}/metricbeat-module-' + moduleName + '.html).', + shortDescription: i18n.translate('kbn.server.tutorials.golangMetrics.shortDescription', { + defaultMessage: 'Fetch internal metrics from a Golang app.', + }), + longDescription: i18n.translate('kbn.server.tutorials.golangMetrics.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `{moduleName}` Metricbeat module fetches internal metrics from a Golang app. \ +[Learn more]({learnMoreLink}).', + values: { + moduleName, + learnMoreLink: `{config.docs.beats.metricbeat}/metricbeat-module-${moduleName}.html`, + }, + }), artifacts: { dashboards: [ { id: 'f2dc7320-f519-11e6-a3c9-9d1f7c42b045', - linkLabel: 'Golang metrics dashboard', + linkLabel: i18n.translate('kbn.server.tutorials.golangMetrics.artifacts.dashboards.linkLabel', { + defaultMessage: 'Golang metrics dashboard', + }), isOverview: true } ], diff --git a/src/core_plugins/kibana/server/tutorials/haproxy_metrics/index.js b/src/core_plugins/kibana/server/tutorials/haproxy_metrics/index.js index 704f7da17a4fe8..5c1befca7c5515 100644 --- a/src/core_plugins/kibana/server/tutorials/haproxy_metrics/index.js +++ b/src/core_plugins/kibana/server/tutorials/haproxy_metrics/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/metricbeat_instructions'; @@ -24,15 +25,27 @@ export function haproxyMetricsSpecProvider() { const moduleName = 'haproxy'; return { id: 'haproxyMetrics', - name: 'HAProxy metrics', + name: i18n.translate('kbn.server.tutorials.haproxyMetrics.nameTitle', { + defaultMessage: 'HAProxy metrics', + }), isBeta: true, category: TUTORIAL_CATEGORY.METRICS, - shortDescription: 'Fetch internal metrics from the HAProxy server.', - longDescription: 'The `haproxy` Metricbeat module fetches internal metrics from HAProxy.' + - ' [Learn more]({config.docs.beats.metricbeat}/metricbeat-module-haproxy.html).', + shortDescription: i18n.translate('kbn.server.tutorials.haproxyMetrics.shortDescription', { + defaultMessage: 'Fetch internal metrics from the HAProxy server.', + }), + longDescription: i18n.translate('kbn.server.tutorials.haproxyMetrics.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `haproxy` Metricbeat module fetches internal metrics from HAProxy. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-haproxy.html', + }, + }), artifacts: { application: { - label: 'Discover', + label: i18n.translate('kbn.server.tutorials.haproxyMetrics.artifacts.application.label', { + defaultMessage: 'Discover', + }), path: '/app/kibana#/discover' }, dashboards: [], diff --git a/src/core_plugins/kibana/server/tutorials/iis_logs/index.js b/src/core_plugins/kibana/server/tutorials/iis_logs/index.js index 9cb12280566a9e..77bf5121fd90a7 100644 --- a/src/core_plugins/kibana/server/tutorials/iis_logs/index.js +++ b/src/core_plugins/kibana/server/tutorials/iis_logs/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/filebeat_instructions'; @@ -27,17 +28,29 @@ export function iisLogsSpecProvider() { const platforms = ['WINDOWS']; return { id: 'iisLogs', - name: 'IIS logs', + name: i18n.translate('kbn.server.tutorials.iisLogs.nameTitle', { + defaultMessage: 'IIS logs', + }), category: TUTORIAL_CATEGORY.LOGGING, - shortDescription: 'Collect and parse access and error logs created by the IIS HTTP server.', - longDescription: 'The `iis` Filebeat module parses access and error logs created by the IIS HTTP server.' + - ' [Learn more]({config.docs.beats.filebeat}/filebeat-module-iis.html).', + shortDescription: i18n.translate('kbn.server.tutorials.iisLogs.shortDescription', { + defaultMessage: 'Collect and parse access and error logs created by the IIS HTTP server.', + }), + longDescription: i18n.translate('kbn.server.tutorials.iisLogs.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `iis` Filebeat module parses access and error logs created by the IIS HTTP server. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-iis.html', + }, + }), //euiIconType: 'logoIIS', artifacts: { dashboards: [ { id: '4278ad30-fe16-11e7-a3b0-d13028918f9f', - linkLabel: 'IIS logs dashboard', + linkLabel: i18n.translate('kbn.server.tutorials.iisLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'IIS logs dashboard', + }), isOverview: true } ], diff --git a/src/core_plugins/kibana/server/tutorials/kafka_logs/index.js b/src/core_plugins/kibana/server/tutorials/kafka_logs/index.js index e69536abecfbfa..3ca03e2e616a67 100644 --- a/src/core_plugins/kibana/server/tutorials/kafka_logs/index.js +++ b/src/core_plugins/kibana/server/tutorials/kafka_logs/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/filebeat_instructions'; @@ -27,17 +28,29 @@ export function kafkaLogsSpecProvider() { const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS']; return { id: 'kafkaLogs', - name: 'Kafka logs', + name: i18n.translate('kbn.server.tutorials.kafkaLogs.nameTitle', { + defaultMessage: 'Kafka logs', + }), category: TUTORIAL_CATEGORY.LOGGING, - shortDescription: 'Collect and parse logs created by Kafka.', - longDescription: 'The `kafka` Filebeat module parses logs created by Kafka.' + - ' [Learn more]({config.docs.beats.filebeat}/filebeat-module-kafka.html).', + shortDescription: i18n.translate('kbn.server.tutorials.kafkaLogs.shortDescription', { + defaultMessage: 'Collect and parse logs created by Kafka.', + }), + longDescription: i18n.translate('kbn.server.tutorials.kafkaLogs.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `kafka` Filebeat module parses logs created by Kafka. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-kafka.html', + }, + }), //euiIconType: 'logoKafka', artifacts: { dashboards: [ { id: '943caca0-87ee-11e7-ad9c-db80de0bf8d3', - linkLabel: 'Kafka logs dashboard', + linkLabel: i18n.translate('kbn.server.tutorials.kafkaLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'Kafka logs dashboard', + }), isOverview: true } ], diff --git a/src/core_plugins/kibana/server/tutorials/kafka_metrics/index.js b/src/core_plugins/kibana/server/tutorials/kafka_metrics/index.js index c27aa8e58ccfeb..07e059f2eab4b8 100644 --- a/src/core_plugins/kibana/server/tutorials/kafka_metrics/index.js +++ b/src/core_plugins/kibana/server/tutorials/kafka_metrics/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/metricbeat_instructions'; @@ -24,15 +25,27 @@ export function kafkaMetricsSpecProvider() { const moduleName = 'kafka'; return { id: 'kafkaMetrics', - name: 'Kafka metrics', + name: i18n.translate('kbn.server.tutorials.kafkaMetrics.nameTitle', { + defaultMessage: 'Kafka metrics', + }), isBeta: true, category: TUTORIAL_CATEGORY.METRICS, - shortDescription: 'Fetch internal metrics from the Kafka server.', - longDescription: 'The `kafka` Metricbeat module fetches internal metrics from Kafka.' + - ' [Learn more]({config.docs.beats.metricbeat}/metricbeat-module-kafka.html).', + shortDescription: i18n.translate('kbn.server.tutorials.kafkaMetrics.shortDescription', { + defaultMessage: 'Fetch internal metrics from the Kafka server.', + }), + longDescription: i18n.translate('kbn.server.tutorials.kafkaMetrics.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `kafka` Metricbeat module fetches internal metrics from Kafka. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-kafka.html', + }, + }), artifacts: { application: { - label: 'Discover', + label: i18n.translate('kbn.server.tutorials.kafkaMetrics.artifacts.application.label', { + defaultMessage: 'Discover', + }), path: '/app/kibana#/discover' }, dashboards: [], diff --git a/src/core_plugins/kibana/server/tutorials/kibana_metrics/index.js b/src/core_plugins/kibana/server/tutorials/kibana_metrics/index.js index a2f44d7d0cd5f0..8dd3adff2beb78 100644 --- a/src/core_plugins/kibana/server/tutorials/kibana_metrics/index.js +++ b/src/core_plugins/kibana/server/tutorials/kibana_metrics/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/metricbeat_instructions'; @@ -24,16 +25,28 @@ export function kibanaMetricsSpecProvider() { const moduleName = 'kibana'; return { id: 'kibanaMetrics', - name: 'Kibana metrics', + name: i18n.translate('kbn.server.tutorials.kibanaMetrics.nameTitle', { + defaultMessage: 'Kibana metrics', + }), isBeta: true, category: TUTORIAL_CATEGORY.METRICS, - shortDescription: 'Fetch internal metrics from Kibana.', - longDescription: 'The `kibana` Metricbeat module fetches internal metrics from Kibana.' + - ' [Learn more]({config.docs.beats.metricbeat}/metricbeat-module-kibana.html).', + shortDescription: i18n.translate('kbn.server.tutorials.kibanaMetrics.shortDescription', { + defaultMessage: 'Fetch internal metrics from Kibana.', + }), + longDescription: i18n.translate('kbn.server.tutorials.kibanaMetrics.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `kibana` Metricbeat module fetches internal metrics from Kibana. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-kibana.html', + }, + }), euiIconType: 'logoKibana', artifacts: { application: { - label: 'Discover', + label: i18n.translate('kbn.server.tutorials.kibanaMetrics.artifacts.application.label', { + defaultMessage: 'Discover', + }), path: '/app/kibana#/discover' }, dashboards: [], diff --git a/src/core_plugins/kibana/server/tutorials/kubernetes_metrics/index.js b/src/core_plugins/kibana/server/tutorials/kubernetes_metrics/index.js index e4ab0d7fc24da2..0bbcc56943be59 100644 --- a/src/core_plugins/kibana/server/tutorials/kubernetes_metrics/index.js +++ b/src/core_plugins/kibana/server/tutorials/kubernetes_metrics/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/metricbeat_instructions'; @@ -24,17 +25,29 @@ export function kubernetesMetricsSpecProvider() { const moduleName = 'kubernetes'; return { id: 'kubernetesMetrics', - name: 'Kubernetes metrics', + name: i18n.translate('kbn.server.tutorials.kubernetesMetrics.nameTitle', { + defaultMessage: 'Kubernetes metrics', + }), category: TUTORIAL_CATEGORY.METRICS, - shortDescription: 'Fetch metrics from your Kubernetes installation.', - longDescription: 'The `kubernetes` Metricbeat module fetches metrics from the Kubernetes APIs.' + - ' [Learn more]({config.docs.beats.metricbeat}/metricbeat-module-kubernetes.html).', + shortDescription: i18n.translate('kbn.server.tutorials.kubernetesMetrics.shortDescription', { + defaultMessage: 'Fetch metrics from your Kubernetes installation.', + }), + longDescription: i18n.translate('kbn.server.tutorials.kubernetesMetrics.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `kubernetes` Metricbeat module fetches metrics from the Kubernetes APIs. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-kubernetes.html', + }, + }), euiIconType: 'logoKubernetes', artifacts: { dashboards: [ { id: 'AV4RGUqo5NkDleZmzKuZ', - linkLabel: 'Kubernetes metrics dashboard', + linkLabel: i18n.translate('kbn.server.tutorials.kubernetesMetrics.artifacts.dashboards.linkLabel', { + defaultMessage: 'Kubernetes metrics dashboard', + }), isOverview: true } ], diff --git a/src/core_plugins/kibana/server/tutorials/logstash_logs/index.js b/src/core_plugins/kibana/server/tutorials/logstash_logs/index.js index 8d3458d575f8d5..222f48def2e19d 100644 --- a/src/core_plugins/kibana/server/tutorials/logstash_logs/index.js +++ b/src/core_plugins/kibana/server/tutorials/logstash_logs/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/filebeat_instructions'; @@ -27,17 +28,29 @@ export function logstashLogsSpecProvider() { const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS']; return { id: 'logstashLogs', - name: 'Logstash logs', + name: i18n.translate('kbn.server.tutorials.logstashLogs.nameTitle', { + defaultMessage: 'Logstash logs', + }), category: TUTORIAL_CATEGORY.LOGGING, - shortDescription: 'Collect and parse debug and slow logs created by Logstash itself.', - longDescription: 'The `logstash` Filebeat module parses debug and slow logs created by Logstash itself.' + - ' [Learn more]({config.docs.beats.filebeat}/filebeat-module-logstash.html).', + shortDescription: i18n.translate('kbn.server.tutorials.logstashLogs.shortDescription', { + defaultMessage: 'Collect and parse debug and slow logs created by Logstash itself.', + }), + longDescription: i18n.translate('kbn.server.tutorials.logstashLogs.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `logstash` Filebeat module parses debug and slow logs created by Logstash itself. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-logstash.html', + }, + }), euiIconType: 'logoLogstash', artifacts: { dashboards: [ { id: 'Filebeat-Logstash-Log-Dashboard', - linkLabel: 'Logstash logs dashboard', + linkLabel: i18n.translate('kbn.server.tutorials.logstashLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'Logstash logs dashboard', + }), isOverview: true } ], diff --git a/src/core_plugins/kibana/server/tutorials/logstash_metrics/index.js b/src/core_plugins/kibana/server/tutorials/logstash_metrics/index.js index 90196d24ae9d05..b5da3edce5e4a8 100644 --- a/src/core_plugins/kibana/server/tutorials/logstash_metrics/index.js +++ b/src/core_plugins/kibana/server/tutorials/logstash_metrics/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/metricbeat_instructions'; @@ -24,16 +25,29 @@ export function logstashMetricsSpecProvider() { const moduleName = 'logstash'; return { id: moduleName + 'Metrics', - name: 'Logstash metrics', + name: i18n.translate('kbn.server.tutorials.logstashMetrics.nameTitle', { + defaultMessage: 'Logstash metrics', + }), isBeta: true, category: TUTORIAL_CATEGORY.METRICS, - shortDescription: 'Fetch interal metrics from a Logstash server.', - longDescription: 'The `' + moduleName + '` Metricbeat module fetches internal metrics from a Logstash server.' + - ' [Learn more]({config.docs.beats.metricbeat}/metricbeat-module-' + moduleName + '.html).', + shortDescription: i18n.translate('kbn.server.tutorials.logstashMetrics.shortDescription', { + defaultMessage: 'Fetch interal metrics from a Logstash server.', + }), + longDescription: i18n.translate('kbn.server.tutorials.logstashMetrics.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `{moduleName}` Metricbeat module fetches internal metrics from a Logstash server. \ +[Learn more]({learnMoreLink}).', + values: { + moduleName, + learnMoreLink: `{config.docs.beats.metricbeat}/metricbeat-module-${moduleName}.html`, + }, + }), euiIconType: 'logoLogstash', artifacts: { application: { - label: 'Discover', + label: i18n.translate('kbn.server.tutorials.logstashMetrics.artifacts.application.label', { + defaultMessage: 'Discover', + }), path: '/app/kibana#/discover' }, dashboards: [], diff --git a/src/core_plugins/kibana/server/tutorials/memcached_metrics/index.js b/src/core_plugins/kibana/server/tutorials/memcached_metrics/index.js index 54937ca25891f6..b090e4511b60cf 100644 --- a/src/core_plugins/kibana/server/tutorials/memcached_metrics/index.js +++ b/src/core_plugins/kibana/server/tutorials/memcached_metrics/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/metricbeat_instructions'; @@ -24,15 +25,27 @@ export function memcachedMetricsSpecProvider() { const moduleName = 'memcached'; return { id: 'memcachedMetrics', - name: 'Memcached metrics', + name: i18n.translate('kbn.server.tutorials.memcachedMetrics.nameTitle', { + defaultMessage: 'Memcached metrics', + }), isBeta: true, category: TUTORIAL_CATEGORY.METRICS, - shortDescription: 'Fetch internal metrics from the Memcached server.', - longDescription: 'The `memcached` Metricbeat module fetches internal metrics from Memcached.' + - ' [Learn more]({config.docs.beats.metricbeat}/metricbeat-module-memcached.html).', + shortDescription: i18n.translate('kbn.server.tutorials.memcachedMetrics.shortDescription', { + defaultMessage: 'Fetch internal metrics from the Memcached server.', + }), + longDescription: i18n.translate('kbn.server.tutorials.memcachedMetrics.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `memcached` Metricbeat module fetches internal metrics from Memcached. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-memcached.html', + }, + }), artifacts: { application: { - label: 'Discover', + label: i18n.translate('kbn.server.tutorials.memcachedMetrics.artifacts.application.label', { + defaultMessage: 'Discover', + }), path: '/app/kibana#/discover' }, dashboards: [], diff --git a/src/core_plugins/kibana/server/tutorials/mongodb_metrics/index.js b/src/core_plugins/kibana/server/tutorials/mongodb_metrics/index.js index 1c13ea688145e6..6f188926ff2e66 100644 --- a/src/core_plugins/kibana/server/tutorials/mongodb_metrics/index.js +++ b/src/core_plugins/kibana/server/tutorials/mongodb_metrics/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/metricbeat_instructions'; @@ -24,17 +25,29 @@ export function mongodbMetricsSpecProvider() { const moduleName = 'mongodb'; return { id: 'mongodbMetrics', - name: 'MongoDB metrics', + name: i18n.translate('kbn.server.tutorials.mongodbMetrics.nameTitle', { + defaultMessage: 'MongoDB metrics', + }), category: TUTORIAL_CATEGORY.METRICS, - shortDescription: 'Fetch internal metrics from MongoDB.', - longDescription: 'The `mongodb` Metricbeat module fetches internal metrics from the MongoDB server.' + - ' [Learn more]({config.docs.beats.metricbeat}/metricbeat-module-mongodb.html).', + shortDescription: i18n.translate('kbn.server.tutorials.mongodbMetrics.shortDescription', { + defaultMessage: 'Fetch internal metrics from MongoDB.', + }), + longDescription: i18n.translate('kbn.server.tutorials.mongodbMetrics.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `mongodb` Metricbeat module fetches internal metrics from the MongoDB server. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-mongodb.html', + }, + }), //euiIconType: 'logoMongoDB', artifacts: { dashboards: [ { id: 'Metricbeat-MongoDB', - linkLabel: 'MongoDB metrics dashboard', + linkLabel: i18n.translate('kbn.server.tutorials.mongodbMetrics.artifacts.dashboards.linkLabel', { + defaultMessage: 'MongoDB metrics dashboard', + }), isOverview: true } ], diff --git a/src/core_plugins/kibana/server/tutorials/munin_metrics/index.js b/src/core_plugins/kibana/server/tutorials/munin_metrics/index.js index 8fb80c3e6680c2..0bc8699d79945d 100644 --- a/src/core_plugins/kibana/server/tutorials/munin_metrics/index.js +++ b/src/core_plugins/kibana/server/tutorials/munin_metrics/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/metricbeat_instructions'; @@ -24,15 +25,27 @@ export function muninMetricsSpecProvider() { const moduleName = 'munin'; return { id: 'muninMetrics', - name: 'Munin metrics', + name: i18n.translate('kbn.server.tutorials.muninMetrics.nameTitle', { + defaultMessage: 'Munin metrics', + }), isBeta: true, category: TUTORIAL_CATEGORY.METRICS, - shortDescription: 'Fetch internal metrics from the Munin server.', - longDescription: 'The `munin` Metricbeat module fetches internal metrics from Munin.' + - ' [Learn more]({config.docs.beats.metricbeat}/metricbeat-module-munin.html).', + shortDescription: i18n.translate('kbn.server.tutorials.muninMetrics.shortDescription', { + defaultMessage: 'Fetch internal metrics from the Munin server.', + }), + longDescription: i18n.translate('kbn.server.tutorials.muninMetrics.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `munin` Metricbeat module fetches internal metrics from Munin. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-munin.html', + }, + }), artifacts: { application: { - label: 'Discover', + label: i18n.translate('kbn.server.tutorials.muninMetrics.artifacts.application.label', { + defaultMessage: 'Discover', + }), path: '/app/kibana#/discover' }, dashboards: [], diff --git a/src/core_plugins/kibana/server/tutorials/mysql_logs/index.js b/src/core_plugins/kibana/server/tutorials/mysql_logs/index.js index 7ec74428c7759a..6caab605d93845 100644 --- a/src/core_plugins/kibana/server/tutorials/mysql_logs/index.js +++ b/src/core_plugins/kibana/server/tutorials/mysql_logs/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/filebeat_instructions'; @@ -27,17 +28,29 @@ export function mysqlLogsSpecProvider() { const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS']; return { id: 'mysqlLogs', - name: 'MySQL logs', + name: i18n.translate('kbn.server.tutorials.mysqlLogs.nameTitle', { + defaultMessage: 'MySQL logs', + }), category: TUTORIAL_CATEGORY.LOGGING, - shortDescription: 'Collect and parse error and slow logs created by MySQL.', - longDescription: 'The `mysql` Filebeat module parses error and slow logs created by MySQL.' + - ' [Learn more]({config.docs.beats.filebeat}/filebeat-module-mysql.html).', + shortDescription: i18n.translate('kbn.server.tutorials.mysqlLogs.shortDescription', { + defaultMessage: 'Collect and parse error and slow logs created by MySQL.', + }), + longDescription: i18n.translate('kbn.server.tutorials.mysqlLogs.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `mysql` Filebeat module parses error and slow logs created by MySQL. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-mysql.html', + }, + }), euiIconType: 'logoMySQL', artifacts: { dashboards: [ { id: 'Filebeat-MySQL-Dashboard', - linkLabel: 'MySQL logs dashboard', + linkLabel: i18n.translate('kbn.server.tutorials.mysqlLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'MySQL logs dashboard', + }), isOverview: true } ], diff --git a/src/core_plugins/kibana/server/tutorials/mysql_metrics/index.js b/src/core_plugins/kibana/server/tutorials/mysql_metrics/index.js index d6b85f6586481e..f1897bbdf4d882 100644 --- a/src/core_plugins/kibana/server/tutorials/mysql_metrics/index.js +++ b/src/core_plugins/kibana/server/tutorials/mysql_metrics/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/metricbeat_instructions'; @@ -24,17 +25,29 @@ export function mysqlMetricsSpecProvider() { const moduleName = 'mysql'; return { id: 'mysqlMetrics', - name: 'MySQL metrics', + name: i18n.translate('kbn.server.tutorials.mysqlMetrics.nameTitle', { + defaultMessage: 'MySQL metrics', + }), category: TUTORIAL_CATEGORY.METRICS, - shortDescription: 'Fetch internal metrics from MySQL.', - longDescription: 'The `mysql` Metricbeat module fetches internal metrics from the MySQL server.' + - ' [Learn more]({config.docs.beats.metricbeat}/metricbeat-module-mysql.html).', + shortDescription: i18n.translate('kbn.server.tutorials.mysqlMetrics.shortDescription', { + defaultMessage: 'Fetch internal metrics from MySQL.', + }), + longDescription: i18n.translate('kbn.server.tutorials.mysqlMetrics.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `mysql` Metricbeat module fetches internal metrics from the MySQL server. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-mysql.html', + }, + }), euiIconType: 'logoMySQL', artifacts: { dashboards: [ { id: '66881e90-0006-11e7-bf7f-c9acc3d3e306', - linkLabel: 'MySQL metrics dashboard', + linkLabel: i18n.translate('kbn.server.tutorials.mysqlMetrics.artifacts.dashboards.linkLabel', { + defaultMessage: 'MySQL metrics dashboard', + }), isOverview: true } ], diff --git a/src/core_plugins/kibana/server/tutorials/netflow/common_instructions.js b/src/core_plugins/kibana/server/tutorials/netflow/common_instructions.js index 2a7364c0529729..eaf554d6b21895 100644 --- a/src/core_plugins/kibana/server/tutorials/netflow/common_instructions.js +++ b/src/core_plugins/kibana/server/tutorials/netflow/common_instructions.js @@ -17,129 +17,125 @@ * under the License. */ -export const COMMON_NETFLOW_INSTRUCTIONS = { - CONFIG: { - ON_PREM: { - OSX: [ - { - title: 'Edit the configuration', - textPre: 'Modify `config/logstash.yml` to set the configuration parameters:', - commands: [ - 'modules:', - ' - name: netflow', - ' var.input.udp.port: ', - ], - textPost: 'Where `` is the UDP port on which Logstash will receive Netflow data.' - - } - ], - WINDOWS: [ - { - title: 'Edit the configuration', - textPre: 'Modify `config\\logstash.yml` to set the configuration parameters:', - commands: [ - 'modules:', - ' - name: netflow', - ' var.input.udp.port: ', - ], - textPost: 'Where `` is the UDP port on which Logstash will receive Netflow data.' - } - ] +export function createCommonNetflowInstructions() { + return { + CONFIG: { + ON_PREM: { + OSX: [ + { + title: 'Edit the configuration', + textPre: 'Modify `config/logstash.yml` to set the configuration parameters:', + commands: ['modules:', ' - name: netflow', ' var.input.udp.port: '], + textPost: + 'Where `` is the UDP port on which Logstash will receive Netflow data.', + }, + ], + WINDOWS: [ + { + title: 'Edit the configuration', + textPre: 'Modify `config\\logstash.yml` to set the configuration parameters:', + commands: ['modules:', ' - name: netflow', ' var.input.udp.port: '], + textPost: + 'Where `` is the UDP port on which Logstash will receive Netflow data.', + }, + ], + }, + ON_PREM_ELASTIC_CLOUD: { + OSX: [ + { + title: 'Edit the configuration', + textPre: 'Modify `config/logstash.yml` to set the configuration parameters:', + commands: [ + 'modules:', + ' - name: netflow', + ' var.input.udp.port: ', + ' var.elasticsearch.hosts: [ "" ]', + ' var.elasticsearch.username: elastic', + ' var.elasticsearch.password: ', + ], + textPost: + 'Where `` is the UDP port on which Logstash will receive Netflow data, ' + + '`` is the URL of Elasticsearch running on Elastic Cloud, and ' + + '`` is the password of the `elastic` user.', + }, + ], + WINDOWS: [ + { + title: 'Edit the configuration', + textPre: 'Modify `config\\logstash.yml` to set the configuration parameters:', + commands: [ + 'modules:', + ' - name: netflow', + ' var.input.udp.port: ', + ' var.elasticsearch.hosts: [ "" ]', + ' var.elasticsearch.username: elastic', + ' var.elasticsearch.password: ', + ], + textPost: + 'Where `` is the UDP port on which Logstash will receive Netflow data, ' + + '`` is the URL of Elasticsearch running on Elastic Cloud, and ' + + '`` is the password of the `elastic` user.', + }, + ], + }, + ELASTIC_CLOUD: { + OSX: [ + { + title: 'Edit the configuration', + textPre: 'Modify `config/logstash.yml` to set the configuration parameters:', + commands: [ + 'cloud.id: "{config.cloud.id}"', + 'cloud.auth: "elastic:"', + ' ', + 'modules:', + ' - name: netflow', + ' var.input.udp.port: ', + ], + textPost: + 'Where `` is the UDP port on which Logstash will receive Netflow data and ' + + '`` is the password of the `elastic` user.', + }, + ], + WINDOWS: [ + { + title: 'Edit the configuration', + textPre: 'Modify `config\\logstash.yml` to set the configuration parameters:', + commands: [ + 'cloud.id: "{config.cloud.id}"', + 'cloud.auth: "elastic:"', + ' ', + 'modules:', + ' - name: netflow', + ' var.input.udp.port: ', + ], + textPost: + 'Where `` is the UDP port on which Logstash will receive Netflow data and ' + + '`` is the password of the `elastic` user.', + }, + ], + }, }, - ON_PREM_ELASTIC_CLOUD: { + SETUP: { OSX: [ { - title: 'Edit the configuration', - textPre: 'Modify `config/logstash.yml` to set the configuration parameters:', - commands: [ - 'modules:', - ' - name: netflow', - ' var.input.udp.port: ', - ' var.elasticsearch.hosts: [ "" ]', - ' var.elasticsearch.username: elastic', - ' var.elasticsearch.password: ', - ], - textPost: 'Where `` is the UDP port on which Logstash will receive Netflow data, ' - + '`` is the URL of Elasticsearch running on Elastic Cloud, and ' - + '`` is the password of the `elastic` user.' - } + title: 'Run the Netflow module', + textPre: 'Run:', + commands: ['./bin/logstash --modules netflow --setup'], + textPost: + 'The `--setup` option creates a `netflow-*` index pattern in Elasticsearch and imports' + + ' Kibana dashboards and visualizations. Omit this option for subsequent runs to avoid overwriting existing dashboards.', + }, ], WINDOWS: [ { - title: 'Edit the configuration', - textPre: 'Modify `config\\logstash.yml` to set the configuration parameters:', - commands: [ - 'modules:', - ' - name: netflow', - ' var.input.udp.port: ', - ' var.elasticsearch.hosts: [ "" ]', - ' var.elasticsearch.username: elastic', - ' var.elasticsearch.password: ', - ], - textPost: 'Where `` is the UDP port on which Logstash will receive Netflow data, ' - + '`` is the URL of Elasticsearch running on Elastic Cloud, and ' - + '`` is the password of the `elastic` user.' - - } - ] - }, - ELASTIC_CLOUD: { - OSX: [ - { - title: 'Edit the configuration', - textPre: 'Modify `config/logstash.yml` to set the configuration parameters:', - commands: [ - 'cloud.id: "{config.cloud.id}"', - 'cloud.auth: "elastic:"', - ' ', - 'modules:', - ' - name: netflow', - ' var.input.udp.port: ', - ], - textPost: 'Where `` is the UDP port on which Logstash will receive Netflow data and ' - + '`` is the password of the `elastic` user.' - } + title: 'Run the Netflow module', + textPre: 'Run:', + commands: ['bin\\logstash --modules netflow --setup'], + textPost: + 'The `--setup` option creates a `netflow-*` index pattern in Elasticsearch and imports' + + ' Kibana dashboards and visualizations. Omit this option for subsequent runs to avoid overwriting existing dashboards.', + }, ], - WINDOWS: [ - { - title: 'Edit the configuration', - textPre: 'Modify `config\\logstash.yml` to set the configuration parameters:', - commands: [ - 'cloud.id: "{config.cloud.id}"', - 'cloud.auth: "elastic:"', - ' ', - 'modules:', - ' - name: netflow', - ' var.input.udp.port: ', - ], - textPost: 'Where `` is the UDP port on which Logstash will receive Netflow data and ' - + '`` is the password of the `elastic` user.' - } - ] - } - }, - SETUP: { - OSX: [ - { - title: 'Run the Netflow module', - textPre: 'Run:', - commands: [ - './bin/logstash --modules netflow --setup', - ], - textPost: 'The `--setup` option creates a `netflow-*` index pattern in Elasticsearch and imports' + - ' Kibana dashboards and visualizations. Omit this option for subsequent runs to avoid overwriting existing dashboards.' - } - ], - WINDOWS: [ - { - title: 'Run the Netflow module', - textPre: 'Run:', - commands: [ - 'bin\\logstash --modules netflow --setup', - ], - textPost: 'The `--setup` option creates a `netflow-*` index pattern in Elasticsearch and imports' + - ' Kibana dashboards and visualizations. Omit this option for subsequent runs to avoid overwriting existing dashboards.' - } - ] - } -}; + }, + }; +} diff --git a/src/core_plugins/kibana/server/tutorials/netflow/elastic_cloud.js b/src/core_plugins/kibana/server/tutorials/netflow/elastic_cloud.js index 21e396bb9db280..ddb3afc0488fdb 100644 --- a/src/core_plugins/kibana/server/tutorials/netflow/elastic_cloud.js +++ b/src/core_plugins/kibana/server/tutorials/netflow/elastic_cloud.js @@ -18,32 +18,37 @@ */ import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; -import { LOGSTASH_INSTRUCTIONS } from '../../../common/tutorials/logstash_instructions'; -import { COMMON_NETFLOW_INSTRUCTIONS } from './common_instructions'; +import { createLogstashInstructions } from '../../../common/tutorials/logstash_instructions'; +import { createCommonNetflowInstructions } from './common_instructions'; // TODO: compare with onPremElasticCloud and onPrem scenarios and extract out common bits -export const ELASTIC_CLOUD_INSTRUCTIONS = { - instructionSets: [ - { - title: 'Getting Started', - instructionVariants: [ - { - id: INSTRUCTION_VARIANT.OSX, - instructions: [ - ...LOGSTASH_INSTRUCTIONS.INSTALL.OSX, - ...COMMON_NETFLOW_INSTRUCTIONS.CONFIG.ELASTIC_CLOUD.OSX, - ...COMMON_NETFLOW_INSTRUCTIONS.SETUP.OSX - ] - }, - { - id: INSTRUCTION_VARIANT.WINDOWS, - instructions: [ - ...LOGSTASH_INSTRUCTIONS.INSTALL.WINDOWS, - ...COMMON_NETFLOW_INSTRUCTIONS.CONFIG.ELASTIC_CLOUD.WINDOWS, - ...COMMON_NETFLOW_INSTRUCTIONS.SETUP.WINDOWS - ] - } - ] - } - ] -}; +export function createElasticCloudInstructions() { + const COMMON_NETFLOW_INSTRUCTIONS = createCommonNetflowInstructions(); + const LOGSTASH_INSTRUCTIONS = createLogstashInstructions(); + + return { + instructionSets: [ + { + title: 'Getting Started', + instructionVariants: [ + { + id: INSTRUCTION_VARIANT.OSX, + instructions: [ + ...LOGSTASH_INSTRUCTIONS.INSTALL.OSX, + ...COMMON_NETFLOW_INSTRUCTIONS.CONFIG.ELASTIC_CLOUD.OSX, + ...COMMON_NETFLOW_INSTRUCTIONS.SETUP.OSX, + ], + }, + { + id: INSTRUCTION_VARIANT.WINDOWS, + instructions: [ + ...LOGSTASH_INSTRUCTIONS.INSTALL.WINDOWS, + ...COMMON_NETFLOW_INSTRUCTIONS.CONFIG.ELASTIC_CLOUD.WINDOWS, + ...COMMON_NETFLOW_INSTRUCTIONS.SETUP.WINDOWS, + ], + }, + ], + }, + ], + }; +} diff --git a/src/core_plugins/kibana/server/tutorials/netflow/index.js b/src/core_plugins/kibana/server/tutorials/netflow/index.js index e634282acc5650..9617fe033b630f 100644 --- a/src/core_plugins/kibana/server/tutorials/netflow/index.js +++ b/src/core_plugins/kibana/server/tutorials/netflow/index.js @@ -18,9 +18,9 @@ */ import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; -import { ON_PREM_INSTRUCTIONS } from './on_prem'; -import { ELASTIC_CLOUD_INSTRUCTIONS } from './elastic_cloud'; -import { ON_PREM_ELASTIC_CLOUD_INSTRUCTIONS } from './on_prem_elastic_cloud'; +import { createOnPremInstructions } from './on_prem'; +import { createElasticCloudInstructions } from './elastic_cloud'; +import { createOnPremElasticCloudInstructions } from './on_prem_elastic_cloud'; export function netflowSpecProvider() { return { @@ -28,14 +28,15 @@ export function netflowSpecProvider() { name: 'Netflow', category: TUTORIAL_CATEGORY.SECURITY, shortDescription: 'Collect Netflow records sent by a Netflow exporter.', - longDescription: 'The Logstash Netflow module collects and parses network flow data, ' + + longDescription: + 'The Logstash Netflow module collects and parses network flow data, ' + ' indexes the events into Elasticsearch, and installs a suite of Kibana dashboards.' + ' This module support Netflow Version 5 and 9.' + ' [Learn more]({config.docs.logstash}/netflow-module.html).', completionTimeMinutes: 10, //previewImagePath: 'kibana-apache.png', TODO - onPrem: ON_PREM_INSTRUCTIONS, - elasticCloud: ELASTIC_CLOUD_INSTRUCTIONS, - onPremElasticCloud: ON_PREM_ELASTIC_CLOUD_INSTRUCTIONS + onPrem: createOnPremInstructions(), + elasticCloud: createElasticCloudInstructions(), + onPremElasticCloud: createOnPremElasticCloudInstructions(), }; } diff --git a/src/core_plugins/kibana/server/tutorials/netflow/on_prem.js b/src/core_plugins/kibana/server/tutorials/netflow/on_prem.js index 5b0ecbbaac2c70..6fb41ee0eb9d36 100644 --- a/src/core_plugins/kibana/server/tutorials/netflow/on_prem.js +++ b/src/core_plugins/kibana/server/tutorials/netflow/on_prem.js @@ -18,32 +18,37 @@ */ import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; -import { LOGSTASH_INSTRUCTIONS } from '../../../common/tutorials/logstash_instructions'; -import { COMMON_NETFLOW_INSTRUCTIONS } from './common_instructions'; +import { createLogstashInstructions } from '../../../common/tutorials/logstash_instructions'; +import { createCommonNetflowInstructions } from './common_instructions'; // TODO: compare with onPremElasticCloud and elasticCloud scenarios and extract out common bits -export const ON_PREM_INSTRUCTIONS = { - instructionSets: [ - { - title: 'Getting Started', - instructionVariants: [ - { - id: INSTRUCTION_VARIANT.OSX, - instructions: [ - ...LOGSTASH_INSTRUCTIONS.INSTALL.OSX, - ...COMMON_NETFLOW_INSTRUCTIONS.CONFIG.ON_PREM.OSX, - ...COMMON_NETFLOW_INSTRUCTIONS.SETUP.OSX - ] - }, - { - id: INSTRUCTION_VARIANT.WINDOWS, - instructions: [ - ...LOGSTASH_INSTRUCTIONS.INSTALL.WINDOWS, - ...COMMON_NETFLOW_INSTRUCTIONS.CONFIG.ON_PREM.WINDOWS, - ...COMMON_NETFLOW_INSTRUCTIONS.SETUP.WINDOWS - ] - } - ] - } - ] -}; +export function createOnPremInstructions() { + const COMMON_NETFLOW_INSTRUCTIONS = createCommonNetflowInstructions(); + const LOGSTASH_INSTRUCTIONS = createLogstashInstructions(); + + return { + instructionSets: [ + { + title: 'Getting Started', + instructionVariants: [ + { + id: INSTRUCTION_VARIANT.OSX, + instructions: [ + ...LOGSTASH_INSTRUCTIONS.INSTALL.OSX, + ...COMMON_NETFLOW_INSTRUCTIONS.CONFIG.ON_PREM.OSX, + ...COMMON_NETFLOW_INSTRUCTIONS.SETUP.OSX, + ], + }, + { + id: INSTRUCTION_VARIANT.WINDOWS, + instructions: [ + ...LOGSTASH_INSTRUCTIONS.INSTALL.WINDOWS, + ...COMMON_NETFLOW_INSTRUCTIONS.CONFIG.ON_PREM.WINDOWS, + ...COMMON_NETFLOW_INSTRUCTIONS.SETUP.WINDOWS, + ], + }, + ], + }, + ], + }; +} diff --git a/src/core_plugins/kibana/server/tutorials/netflow/on_prem_elastic_cloud.js b/src/core_plugins/kibana/server/tutorials/netflow/on_prem_elastic_cloud.js index 0e5cb74064f804..87f647882518f5 100644 --- a/src/core_plugins/kibana/server/tutorials/netflow/on_prem_elastic_cloud.js +++ b/src/core_plugins/kibana/server/tutorials/netflow/on_prem_elastic_cloud.js @@ -18,40 +18,47 @@ */ import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; -import { LOGSTASH_INSTRUCTIONS } from '../../../common/tutorials/logstash_instructions'; +import { createLogstashInstructions } from '../../../common/tutorials/logstash_instructions'; import { - TRYCLOUD_OPTION1, - TRYCLOUD_OPTION2 + createTrycloudOption1, + createTrycloudOption2, } from '../../../common/tutorials/onprem_cloud_instructions'; -import { COMMON_NETFLOW_INSTRUCTIONS } from './common_instructions'; +import { createCommonNetflowInstructions } from './common_instructions'; // TODO: compare with onPrem and elasticCloud scenarios and extract out common bits -export const ON_PREM_ELASTIC_CLOUD_INSTRUCTIONS = { - instructionSets: [ - { - title: 'Getting Started', - instructionVariants: [ - { - id: INSTRUCTION_VARIANT.OSX, - instructions: [ - TRYCLOUD_OPTION1, - TRYCLOUD_OPTION2, - ...LOGSTASH_INSTRUCTIONS.INSTALL.OSX, - ...COMMON_NETFLOW_INSTRUCTIONS.CONFIG.ON_PREM_ELASTIC_CLOUD.OSX, - ...COMMON_NETFLOW_INSTRUCTIONS.SETUP.OSX - ] - }, - { - id: INSTRUCTION_VARIANT.WINDOWS, - instructions: [ - TRYCLOUD_OPTION1, - TRYCLOUD_OPTION2, - ...LOGSTASH_INSTRUCTIONS.INSTALL.WINDOWS, - ...COMMON_NETFLOW_INSTRUCTIONS.CONFIG.ON_PREM_ELASTIC_CLOUD.WINDOWS, - ...COMMON_NETFLOW_INSTRUCTIONS.SETUP.WINDOWS - ] - } - ] - } - ] -}; +export function createOnPremElasticCloudInstructions() { + const COMMON_NETFLOW_INSTRUCTIONS = createCommonNetflowInstructions(); + const TRYCLOUD_OPTION1 = createTrycloudOption1(); + const TRYCLOUD_OPTION2 = createTrycloudOption2(); + const LOGSTASH_INSTRUCTIONS = createLogstashInstructions(); + + return { + instructionSets: [ + { + title: 'Getting Started', + instructionVariants: [ + { + id: INSTRUCTION_VARIANT.OSX, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + ...LOGSTASH_INSTRUCTIONS.INSTALL.OSX, + ...COMMON_NETFLOW_INSTRUCTIONS.CONFIG.ON_PREM_ELASTIC_CLOUD.OSX, + ...COMMON_NETFLOW_INSTRUCTIONS.SETUP.OSX, + ], + }, + { + id: INSTRUCTION_VARIANT.WINDOWS, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + ...LOGSTASH_INSTRUCTIONS.INSTALL.WINDOWS, + ...COMMON_NETFLOW_INSTRUCTIONS.CONFIG.ON_PREM_ELASTIC_CLOUD.WINDOWS, + ...COMMON_NETFLOW_INSTRUCTIONS.SETUP.WINDOWS, + ], + }, + ], + }, + ], + }; +} diff --git a/src/core_plugins/kibana/server/tutorials/nginx_logs/index.js b/src/core_plugins/kibana/server/tutorials/nginx_logs/index.js index 53452918eb93a9..82bd676d49a28d 100644 --- a/src/core_plugins/kibana/server/tutorials/nginx_logs/index.js +++ b/src/core_plugins/kibana/server/tutorials/nginx_logs/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/filebeat_instructions'; @@ -27,17 +28,29 @@ export function nginxLogsSpecProvider() { const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS']; return { id: 'nginxLogs', - name: 'Nginx logs', + name: i18n.translate('kbn.server.tutorials.nginxLogs.nameTitle', { + defaultMessage: 'Nginx logs', + }), category: TUTORIAL_CATEGORY.LOGGING, - shortDescription: 'Collect and parse access and error logs created by the Nginx HTTP server.', - longDescription: 'The `nginx` Filebeat module parses access and error logs created by the Nginx HTTP server.' + - ' [Learn more]({config.docs.beats.filebeat}/filebeat-module-nginx.html).', + shortDescription: i18n.translate('kbn.server.tutorials.nginxLogs.shortDescription', { + defaultMessage: 'Collect and parse access and error logs created by the Nginx HTTP server.', + }), + longDescription: i18n.translate('kbn.server.tutorials.nginxLogs.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `nginx` Filebeat module parses access and error logs created by the Nginx HTTP server. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-nginx.html', + }, + }), euiIconType: 'logoNginx', artifacts: { dashboards: [ { id: '55a9e6e0-a29e-11e7-928f-5dbe6f6f5519', - linkLabel: 'Nginx logs dashboard', + linkLabel: i18n.translate('kbn.server.tutorials.nginxLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'Nginx logs dashboard', + }), isOverview: true } ], diff --git a/src/core_plugins/kibana/server/tutorials/nginx_metrics/index.js b/src/core_plugins/kibana/server/tutorials/nginx_metrics/index.js index fd1d291bab5b43..c521f138cfc354 100644 --- a/src/core_plugins/kibana/server/tutorials/nginx_metrics/index.js +++ b/src/core_plugins/kibana/server/tutorials/nginx_metrics/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/metricbeat_instructions'; @@ -24,20 +25,33 @@ export function nginxMetricsSpecProvider() { const moduleName = 'nginx'; return { id: 'nginxMetrics', - name: 'Nginx metrics', + name: i18n.translate('kbn.server.tutorials.nginxMetrics.nameTitle', { + defaultMessage: 'Nginx metrics', + }), category: TUTORIAL_CATEGORY.METRICS, - shortDescription: 'Fetch internal metrics from the Nginx HTTP server.', - longDescription: 'The `nginx` Metricbeat module fetches internal metrics from the Nginx HTTP server.' + - ' The module scrapes the server status data from the web page generated by the' + - ' [ngx_http_stub_status_module](http://nginx.org/en/docs/http/ngx_http_stub_status_module.html),' + - ' which must be enabled in your Nginx installation.' + - ' [Learn more]({config.docs.beats.metricbeat}/metricbeat-module-nginx.html).', + shortDescription: i18n.translate('kbn.server.tutorials.nginxMetrics.shortDescription', { + defaultMessage: 'Fetch internal metrics from the Nginx HTTP server.', + }), + longDescription: i18n.translate('kbn.server.tutorials.nginxMetrics.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `nginx` Metricbeat module fetches internal metrics from the Nginx HTTP server. \ +The module scrapes the server status data from the web page generated by the \ +{statusModuleLink}, \ +which must be enabled in your Nginx installation. \ +[Learn more]({learnMoreLink}).', + values: { + statusModuleLink: '[ngx_http_stub_status_module](http://nginx.org/en/docs/http/ngx_http_stub_status_module.html)', + learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-nginx.html', + }, + }), euiIconType: 'logoNginx', artifacts: { dashboards: [ { id: '023d2930-f1a5-11e7-a9ef-93c69af7b129', - linkLabel: 'Nginx metrics dashboard', + linkLabel: i18n.translate('kbn.server.tutorials.nginxMetrics.artifacts.dashboards.linkLabel', { + defaultMessage: 'Nginx metrics dashboard', + }), isOverview: true } ], diff --git a/src/core_plugins/kibana/server/tutorials/osquery_logs/index.js b/src/core_plugins/kibana/server/tutorials/osquery_logs/index.js index a0e1a84e5722e5..ef18d55c37d549 100644 --- a/src/core_plugins/kibana/server/tutorials/osquery_logs/index.js +++ b/src/core_plugins/kibana/server/tutorials/osquery_logs/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/filebeat_instructions'; @@ -27,17 +28,29 @@ export function osqueryLogsSpecProvider() { const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS']; return { id: 'osqueryLogs', - name: 'Osquery logs', + name: i18n.translate('kbn.server.tutorials.osqueryLogs.nameTitle', { + defaultMessage: 'Osquery logs', + }), category: TUTORIAL_CATEGORY.SECURITY, - shortDescription: 'Collect the result logs created by osqueryd.', - longDescription: 'The `osquery` Filebeat module collects the JSON result logs collected by `osqueryd`.' + - ' [Learn more]({config.docs.beats.filebeat}/filebeat-module-osquery.html).', + shortDescription: i18n.translate('kbn.server.tutorials.osqueryLogs.shortDescription', { + defaultMessage: 'Collect the result logs created by osqueryd.', + }), + longDescription: i18n.translate('kbn.server.tutorials.osqueryLogs.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `osquery` Filebeat module collects the JSON result logs collected by `osqueryd`. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-osquery.html', + }, + }), //euiIconType: 'logoOsquery', artifacts: { dashboards: [ { id: '69f5ae20-eb02-11e7-8f04-51231daa5b05', - linkLabel: 'Osquery logs dashboard', + linkLabel: i18n.translate('kbn.server.tutorials.osqueryLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'Osquery logs dashboard', + }), isOverview: true } ], diff --git a/src/core_plugins/kibana/server/tutorials/php_fpm_metrics/index.js b/src/core_plugins/kibana/server/tutorials/php_fpm_metrics/index.js index bf0d27740ecc73..a7e54efb76c0a1 100644 --- a/src/core_plugins/kibana/server/tutorials/php_fpm_metrics/index.js +++ b/src/core_plugins/kibana/server/tutorials/php_fpm_metrics/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/metricbeat_instructions'; @@ -24,12 +25,22 @@ export function phpfpmMetricsSpecProvider() { const moduleName = 'php_fpm'; return { id: 'phpfpmMetrics', - name: 'PHP-FPM metrics', + name: i18n.translate('kbn.server.tutorials.phpFpmMetrics.nameTitle', { + defaultMessage: 'PHP-FPM metrics', + }), category: TUTORIAL_CATEGORY.METRICS, isBeta: true, - shortDescription: 'Fetch internal metrics from PHP-FPM.', - longDescription: 'The `php_fpm` Metricbeat module fetches internal metrics from the PHP-FPM server.' + - ' [Learn more]({config.docs.beats.metricbeat}/metricbeat-module-php_fpm.html).', + shortDescription: i18n.translate('kbn.server.tutorials.phpFpmMetrics.shortDescription', { + defaultMessage: 'Fetch internal metrics from PHP-FPM.', + }), + longDescription: i18n.translate('kbn.server.tutorials.phpFpmMetrics.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `php_fpm` Metricbeat module fetches internal metrics from the PHP-FPM server. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-php_fpm.html', + }, + }), //euiIconType: 'logoPHPFPM', artifacts: { dashboards: [ diff --git a/src/core_plugins/kibana/server/tutorials/postgresql_logs/index.js b/src/core_plugins/kibana/server/tutorials/postgresql_logs/index.js index fa17700fc9b347..0ca4757668452e 100644 --- a/src/core_plugins/kibana/server/tutorials/postgresql_logs/index.js +++ b/src/core_plugins/kibana/server/tutorials/postgresql_logs/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/filebeat_instructions'; @@ -27,17 +28,29 @@ export function postgresqlLogsSpecProvider() { const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS']; return { id: 'postgresqlLogs', - name: 'PostgreSQL logs', + name: i18n.translate('kbn.server.tutorials.postgresqlLogs.nameTitle', { + defaultMessage: 'PostgreSQL logs', + }), category: TUTORIAL_CATEGORY.LOGGING, - shortDescription: 'Collect and parse error and slow logs created by PostgreSQL.', - longDescription: 'The `postgresql` Filebeat module parses error and slow logs created by PostgreSQL.' + - ' [Learn more]({config.docs.beats.filebeat}/filebeat-module-postgresql.html).', + shortDescription: i18n.translate('kbn.server.tutorials.postgresqlLogs.shortDescription', { + defaultMessage: 'Collect and parse error and slow logs created by PostgreSQL.', + }), + longDescription: i18n.translate('kbn.server.tutorials.postgresqlLogs.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `postgresql` Filebeat module parses error and slow logs created by PostgreSQL. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-postgresql.html', + }, + }), //euiIconType: 'logoPostgreSQL', artifacts: { dashboards: [ { id: '158be870-87f4-11e7-ad9c-db80de0bf8d3', - linkLabel: 'PostgreSQL logs dashboard', + linkLabel: i18n.translate('kbn.server.tutorials.postgresqlLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'PostgreSQL logs dashboard', + }), isOverview: true } ], diff --git a/src/core_plugins/kibana/server/tutorials/postgresql_metrics/index.js b/src/core_plugins/kibana/server/tutorials/postgresql_metrics/index.js index be827e6c4891c8..2060c9e98ef843 100644 --- a/src/core_plugins/kibana/server/tutorials/postgresql_metrics/index.js +++ b/src/core_plugins/kibana/server/tutorials/postgresql_metrics/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/metricbeat_instructions'; @@ -24,12 +25,22 @@ export function postgresqlMetricsSpecProvider() { const moduleName = 'postgresql'; return { id: 'postgresqlMetrics', - name: 'PostgreSQL metrics', + name: i18n.translate('kbn.server.tutorials.postgresqlMetrics.nameTitle', { + defaultMessage: 'PostgreSQL metrics', + }), category: TUTORIAL_CATEGORY.METRICS, isBeta: true, - shortDescription: 'Fetch internal metrics from PostgreSQL.', - longDescription: 'The `postgresql` Metricbeat module fetches internal metrics from the PostgreSQL server.' + - ' [Learn more]({config.docs.beats.metricbeat}/metricbeat-module-postgresql.html).', + shortDescription: i18n.translate('kbn.server.tutorials.postgresqlMetrics.shortDescription', { + defaultMessage: 'Fetch internal metrics from PostgreSQL.', + }), + longDescription: i18n.translate('kbn.server.tutorials.postgresqlMetrics.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `postgresql` Metricbeat module fetches internal metrics from the PostgreSQL server. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-postgresql.html', + }, + }), //euiIconType: 'logoPostgreSQL', artifacts: { dashboards: [ diff --git a/src/core_plugins/kibana/server/tutorials/prometheus_metrics/index.js b/src/core_plugins/kibana/server/tutorials/prometheus_metrics/index.js index 2da1fa47053b13..b4887ba7a053d7 100644 --- a/src/core_plugins/kibana/server/tutorials/prometheus_metrics/index.js +++ b/src/core_plugins/kibana/server/tutorials/prometheus_metrics/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/metricbeat_instructions'; @@ -24,15 +25,28 @@ export function prometheusMetricsSpecProvider() { const moduleName = 'prometheus'; return { id: moduleName + 'Metrics', - name: 'Prometheus metrics', + name: i18n.translate('kbn.server.tutorials.prometheusMetrics.nameTitle', { + defaultMessage: 'Prometheus metrics', + }), isBeta: true, category: TUTORIAL_CATEGORY.METRICS, - shortDescription: 'Fetch metrics from a Prometheus exporter.', - longDescription: 'The `' + moduleName + '` Metricbeat module fetches metrics from Prometheus endpoint.' + - ' [Learn more]({config.docs.beats.metricbeat}/metricbeat-module-' + moduleName + '.html).', + shortDescription: i18n.translate('kbn.server.tutorials.prometheusMetrics.shortDescription', { + defaultMessage: 'Fetch metrics from a Prometheus exporter.', + }), + longDescription: i18n.translate('kbn.server.tutorials.prometheusMetrics.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `{moduleName}` Metricbeat module fetches metrics from Prometheus endpoint. \ +[Learn more]({learnMoreLink}).', + values: { + moduleName, + learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-' + moduleName + '.html', + }, + }), artifacts: { application: { - label: 'Discover', + label: i18n.translate('kbn.server.tutorials.prometheusMetrics.artifacts.application.label', { + defaultMessage: 'Discover', + }), path: '/app/kibana#/discover' }, dashboards: [], diff --git a/src/core_plugins/kibana/server/tutorials/rabbitmq_metrics/index.js b/src/core_plugins/kibana/server/tutorials/rabbitmq_metrics/index.js index 52abb5facda571..fd7874d8dcb8a0 100644 --- a/src/core_plugins/kibana/server/tutorials/rabbitmq_metrics/index.js +++ b/src/core_plugins/kibana/server/tutorials/rabbitmq_metrics/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/metricbeat_instructions'; @@ -24,18 +25,30 @@ export function rabbitmqMetricsSpecProvider() { const moduleName = 'rabbitmq'; return { id: 'rabbitmqMetrics', - name: 'RabbitMQ metrics', + name: i18n.translate('kbn.server.tutorials.rabbitmqMetrics.nameTitle', { + defaultMessage: 'RabbitMQ metrics', + }), category: TUTORIAL_CATEGORY.METRICS, - shortDescription: 'Fetch internal metrics from the RabbitMQ server.', - longDescription: 'The `rabbitmq` Metricbeat module fetches internal metrics from the RabbitMQ server.' + - ' [Learn more]({config.docs.beats.metricbeat}/metricbeat-module-rabbitmq.html).', + shortDescription: i18n.translate('kbn.server.tutorials.rabbitmqMetrics.shortDescription', { + defaultMessage: 'Fetch internal metrics from the RabbitMQ server.', + }), + longDescription: i18n.translate('kbn.server.tutorials.rabbitmqMetrics.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `rabbitmq` Metricbeat module fetches internal metrics from the RabbitMQ server. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-rabbitmq.html', + }, + }), //euiIconType: 'logoRabbitMQ', isBeta: true, artifacts: { dashboards: [ { id: 'AV4YobKIge1VCbKU_qVo', - linkLabel: 'RabbitMQ metrics dashboard', + linkLabel: i18n.translate('kbn.server.tutorials.rabbitmqMetrics.artifacts.dashboards.linkLabel', { + defaultMessage: 'RabbitMQ metrics dashboard', + }), isOverview: true } ], diff --git a/src/core_plugins/kibana/server/tutorials/redis_logs/index.js b/src/core_plugins/kibana/server/tutorials/redis_logs/index.js index edd94e28a0e67b..a6a75247143f51 100644 --- a/src/core_plugins/kibana/server/tutorials/redis_logs/index.js +++ b/src/core_plugins/kibana/server/tutorials/redis_logs/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/filebeat_instructions'; @@ -27,23 +28,35 @@ export function redisLogsSpecProvider() { const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS']; return { id: 'redisLogs', - name: 'Redis logs', + name: i18n.translate('kbn.server.tutorials.redisLogs.nameTitle', { + defaultMessage: 'Redis logs', + }), category: TUTORIAL_CATEGORY.LOGGING, - shortDescription: 'Collect and parse error and slow logs created by Redis.', - longDescription: 'The `redis` Filebeat module parses error and slow logs created by Redis.' + - ' For Redis to write error logs, make sure the `logfile` option, from the' + - ' Redis configuration file, is set to `redis-server.log`.' + - ' The slow logs are read directly from Redis via the `SLOWLOG` command.' + - ' For Redis to record slow logs, make sure the `slowlog-log-slower-than`' + - ' option is set.' + - ' Note that the `slowlog` fileset is experimental.' + - ' [Learn more]({config.docs.beats.filebeat}/filebeat-module-redis.html).', + shortDescription: i18n.translate('kbn.server.tutorials.redisLogs.shortDescription', { + defaultMessage: 'Collect and parse error and slow logs created by Redis.', + }), + longDescription: i18n.translate('kbn.server.tutorials.redisLogs.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `redis` Filebeat module parses error and slow logs created by Redis. \ +For Redis to write error logs, make sure the `logfile` option, from the \ +Redis configuration file, is set to `redis-server.log`. \ +The slow logs are read directly from Redis via the `SLOWLOG` command. \ +For Redis to record slow logs, make sure the `slowlog-log-slower-than` \ +option is set. \ +Note that the `slowlog` fileset is experimental. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-redis.html', + }, + }), euiIconType: 'logoRedis', artifacts: { dashboards: [ { id: '7fea2930-478e-11e7-b1f0-cb29bac6bf8b', - linkLabel: 'Redis logs dashboard', + linkLabel: i18n.translate('kbn.server.tutorials.redisLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'Redis logs dashboard', + }), isOverview: true } ], diff --git a/src/core_plugins/kibana/server/tutorials/redis_metrics/index.js b/src/core_plugins/kibana/server/tutorials/redis_metrics/index.js index c725ce2a850ae2..4db10f551bfe69 100644 --- a/src/core_plugins/kibana/server/tutorials/redis_metrics/index.js +++ b/src/core_plugins/kibana/server/tutorials/redis_metrics/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/metricbeat_instructions'; @@ -24,17 +25,29 @@ export function redisMetricsSpecProvider() { const moduleName = 'redis'; return { id: 'redisMetrics', - name: 'Redis metrics', + name: i18n.translate('kbn.server.tutorials.redisMetrics.nameTitle', { + defaultMessage: 'Redis metrics', + }), category: TUTORIAL_CATEGORY.METRICS, - shortDescription: 'Fetch internal metrics from Redis.', - longDescription: 'The `redis` Metricbeat module fetches internal metrics from the Redis server.' + - ' [Learn more]({config.docs.beats.metricbeat}/metricbeat-module-redis.html).', + shortDescription: i18n.translate('kbn.server.tutorials.redisMetrics.shortDescription', { + defaultMessage: 'Fetch internal metrics from Redis.', + }), + longDescription: i18n.translate('kbn.server.tutorials.redisMetrics.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `redis` Metricbeat module fetches internal metrics from the Redis server. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-redis.html', + }, + }), euiIconType: 'logoRedis', artifacts: { dashboards: [ { id: 'AV4YjZ5pux-M-tCAunxK', - linkLabel: 'Redis metrics dashboard', + linkLabel: i18n.translate('kbn.server.tutorials.redisMetrics.artifacts.dashboards.linkLabel', { + defaultMessage: 'Redis metrics dashboard', + }), isOverview: true } ], diff --git a/src/core_plugins/kibana/server/tutorials/system_logs/index.js b/src/core_plugins/kibana/server/tutorials/system_logs/index.js index 770155b16aa701..70629983ee8e16 100644 --- a/src/core_plugins/kibana/server/tutorials/system_logs/index.js +++ b/src/core_plugins/kibana/server/tutorials/system_logs/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/filebeat_instructions'; @@ -27,17 +28,29 @@ export function systemLogsSpecProvider() { const platforms = ['OSX', 'DEB', 'RPM']; return { id: 'systemLogs', - name: 'System logs', + name: i18n.translate('kbn.server.tutorials.systemLogs.nameTitle', { + defaultMessage: 'System logs', + }), category: TUTORIAL_CATEGORY.LOGGING, - shortDescription: 'Collect and parse logs written by the local Syslog server.', - longDescription: 'The `system` Filebeat module collects and parses logs created by the system logging service of common ' + - ' Unix/Linux based distributions. This module is not available on Windows.' + - ' [Learn more]({config.docs.beats.filebeat}/filebeat-module-system.html).', + shortDescription: i18n.translate('kbn.server.tutorials.systemLogs.shortDescription', { + defaultMessage: 'Collect and parse logs written by the local Syslog server.', + }), + longDescription: i18n.translate('kbn.server.tutorials.systemLogs.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `system` Filebeat module collects and parses logs created by the system logging service of common \ +Unix/Linux based distributions. This module is not available on Windows. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-system.html', + }, + }), artifacts: { dashboards: [ { id: 'Filebeat-syslog-dashboard', - linkLabel: 'System logs dashboard', + linkLabel: i18n.translate('kbn.server.tutorials.systemLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'System logs dashboard', + }), isOverview: true } ], diff --git a/src/core_plugins/kibana/server/tutorials/system_metrics/index.js b/src/core_plugins/kibana/server/tutorials/system_metrics/index.js index 83cc79f5de49a0..d16667ea3b66e1 100644 --- a/src/core_plugins/kibana/server/tutorials/system_metrics/index.js +++ b/src/core_plugins/kibana/server/tutorials/system_metrics/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/metricbeat_instructions'; @@ -24,17 +25,29 @@ export function systemMetricsSpecProvider() { const moduleName = 'system'; return { id: 'systemMetrics', - name: 'System metrics', + name: i18n.translate('kbn.server.tutorials.systemMetrics.nameTitle', { + defaultMessage: 'System metrics', + }), category: TUTORIAL_CATEGORY.METRICS, - shortDescription: 'Collect CPU, memory, network, and disk statistics from the host.', - longDescription: 'The `system` Metricbeat module collects CPU, memory, network, and disk statistics from the host.' + - ' It collects system wide statistics and statistics per process and filesystem.' + - ' [Learn more]({config.docs.beats.metricbeat}/metricbeat-module-system.html).', + shortDescription: i18n.translate('kbn.server.tutorials.systemMetrics.shortDescription', { + defaultMessage: 'Collect CPU, memory, network, and disk statistics from the host.', + }), + longDescription: i18n.translate('kbn.server.tutorials.systemMetrics.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `system` Metricbeat module collects CPU, memory, network, and disk statistics from the host. \ +It collects system wide statistics and statistics per process and filesystem. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-system.html', + }, + }), artifacts: { dashboards: [ { id: 'Metricbeat-system-overview', - linkLabel: 'System metrics dashboard', + linkLabel: i18n.translate('kbn.server.tutorials.systemMetrics.artifacts.dashboards.linkLabel', { + defaultMessage: 'System metrics dashboard', + }), isOverview: true } ], diff --git a/src/core_plugins/kibana/server/tutorials/traefik_logs/index.js b/src/core_plugins/kibana/server/tutorials/traefik_logs/index.js index f3786d964b0f7a..50a5dc714933fa 100644 --- a/src/core_plugins/kibana/server/tutorials/traefik_logs/index.js +++ b/src/core_plugins/kibana/server/tutorials/traefik_logs/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/filebeat_instructions'; @@ -27,17 +28,29 @@ export function traefikLogsSpecProvider() { const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS']; return { id: 'traefikLogs', - name: 'Traefik logs', + name: i18n.translate('kbn.server.tutorials.traefikLogs.nameTitle', { + defaultMessage: 'Traefik logs', + }), category: TUTORIAL_CATEGORY.LOGGING, - shortDescription: 'Collect and parse access logs created by the Traefik Proxy.', - longDescription: 'The `traefik` Filebeat module parses access logs created by Traefik.' + - ' [Learn more]({config.docs.beats.filebeat}/filebeat-module-traefik.html).', + shortDescription: i18n.translate('kbn.server.tutorials.traefikLogs.shortDescription', { + defaultMessage: 'Collect and parse access logs created by the Traefik Proxy.', + }), + longDescription: i18n.translate('kbn.server.tutorials.traefikLogs.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `traefik` Filebeat module parses access logs created by Traefik. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-traefik.html', + }, + }), //euiIconType: 'logoTraefik', artifacts: { dashboards: [ { id: 'Filebeat-Traefik-Dashboard', - linkLabel: 'Traefik logs dashboard', + linkLabel: i18n.translate('kbn.server.tutorials.traefikLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'Traefik logs dashboard', + }), isOverview: true } ], diff --git a/src/core_plugins/kibana/server/tutorials/uwsgi_metrics/index.js b/src/core_plugins/kibana/server/tutorials/uwsgi_metrics/index.js index f8a30c0191ee74..cd799851fa76ec 100644 --- a/src/core_plugins/kibana/server/tutorials/uwsgi_metrics/index.js +++ b/src/core_plugins/kibana/server/tutorials/uwsgi_metrics/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/metricbeat_instructions'; @@ -24,18 +25,30 @@ export function uwsgiMetricsSpecProvider() { const moduleName = 'uwsgi'; return { id: 'uwsgiMetrics', - name: 'uWSGI metrics', + name: i18n.translate('kbn.server.tutorials.uwsgiMetrics.nameTitle', { + defaultMessage: 'uWSGI metrics', + }), category: TUTORIAL_CATEGORY.METRICS, - shortDescription: 'Fetch internal metrics from the uWSGI server.', - longDescription: 'The `uwsgi` Metricbeat module fetches internal metrics from the uWSGI server.' + - ' [Learn more]({config.docs.beats.metricbeat}/metricbeat-module-uwsgi.html).', + shortDescription: i18n.translate('kbn.server.tutorials.uwsgiMetrics.shortDescription', { + defaultMessage: 'Fetch internal metrics from the uWSGI server.', + }), + longDescription: i18n.translate('kbn.server.tutorials.uwsgiMetrics.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `uwsgi` Metricbeat module fetches internal metrics from the uWSGI server. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-uwsgi.html', + }, + }), //euiIconType: 'logouWSGI', isBeta: true, artifacts: { dashboards: [ { id: '32fca290-f0af-11e7-b9ff-9f96241065de', - linkLabel: 'uWSGI metrics dashboard', + linkLabel: i18n.translate('kbn.server.tutorials.uwsgiMetrics.artifacts.dashboards.linkLabel', { + defaultMessage: 'uWSGI metrics dashboard', + }), isOverview: true } ], diff --git a/src/core_plugins/kibana/server/tutorials/vsphere_metrics/index.js b/src/core_plugins/kibana/server/tutorials/vsphere_metrics/index.js index 074f4574c9ac8e..d92f7de737ed6a 100644 --- a/src/core_plugins/kibana/server/tutorials/vsphere_metrics/index.js +++ b/src/core_plugins/kibana/server/tutorials/vsphere_metrics/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/metricbeat_instructions'; @@ -24,15 +25,27 @@ export function vSphereMetricsSpecProvider() { const moduleName = 'vsphere'; return { id: 'vsphereMetrics', - name: 'vSphere metrics', + name: i18n.translate('kbn.server.tutorials.vsphereMetrics.nameTitle', { + defaultMessage: 'vSphere metrics', + }), category: TUTORIAL_CATEGORY.METRICS, - shortDescription: 'Fetch internal metrics from vSphere.', - longDescription: 'The `vsphere` Metricbeat module fetches internal metrics from a vSphere cluster.' + - ' [Learn more]({config.docs.beats.metricbeat}/metricbeat-module-vsphere.html).', + shortDescription: i18n.translate('kbn.server.tutorials.vsphereMetrics.shortDescription', { + defaultMessage: 'Fetch internal metrics from vSphere.', + }), + longDescription: i18n.translate('kbn.server.tutorials.vsphereMetrics.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `vsphere` Metricbeat module fetches internal metrics from a vSphere cluster. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-vsphere.html', + }, + }), //euiIconType: 'logoVSphere', artifacts: { application: { - label: 'Discover', + label: i18n.translate('kbn.server.tutorials.vsphereMetrics.artifacts.application.label', { + defaultMessage: 'Discover', + }), path: '/app/kibana#/discover' }, dashboards: [], diff --git a/src/core_plugins/kibana/server/tutorials/windows_metrics/index.js b/src/core_plugins/kibana/server/tutorials/windows_metrics/index.js index 97c8b2ed1b3cd4..a7a1dd0c88c3da 100644 --- a/src/core_plugins/kibana/server/tutorials/windows_metrics/index.js +++ b/src/core_plugins/kibana/server/tutorials/windows_metrics/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/metricbeat_instructions'; @@ -24,15 +25,27 @@ export function windowsMetricsSpecProvider() { const moduleName = 'windows'; return { id: 'windowsMetrics', - name: 'Windows metrics', + name: i18n.translate('kbn.server.tutorials.windowsMetrics.nameTitle', { + defaultMessage: 'Windows metrics', + }), isBeta: true, category: TUTORIAL_CATEGORY.METRICS, - shortDescription: 'Fetch internal metrics from Windows.', - longDescription: 'The `windows` Metricbeat module fetches internal metrics from Windows.' + - ' [Learn more]({config.docs.beats.metricbeat}/metricbeat-module-windows.html).', + shortDescription: i18n.translate('kbn.server.tutorials.windowsMetrics.shortDescription', { + defaultMessage: 'Fetch internal metrics from Windows.', + }), + longDescription: i18n.translate('kbn.server.tutorials.windowsMetrics.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `windows` Metricbeat module fetches internal metrics from Windows. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-windows.html', + }, + }), artifacts: { application: { - label: 'Discover', + label: i18n.translate('kbn.server.tutorials.windowsMetrics.artifacts.application.label', { + defaultMessage: 'Discover', + }), path: '/app/kibana#/discover' }, dashboards: [], diff --git a/src/core_plugins/kibana/server/tutorials/zookeeper_metrics/index.js b/src/core_plugins/kibana/server/tutorials/zookeeper_metrics/index.js index c24f1b8acfd2e8..f86c56680454aa 100644 --- a/src/core_plugins/kibana/server/tutorials/zookeeper_metrics/index.js +++ b/src/core_plugins/kibana/server/tutorials/zookeeper_metrics/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/metricbeat_instructions'; @@ -24,15 +25,28 @@ export function zookeeperMetricsSpecProvider() { const moduleName = 'zookeeper'; return { id: moduleName + 'Metrics', - name: 'Zookeeper metrics', + name: i18n.translate('kbn.server.tutorials.zookeeperMetrics.nameTitle', { + defaultMessage: 'Zookeeper metrics', + }), isBeta: true, category: TUTORIAL_CATEGORY.METRICS, - shortDescription: 'Fetch interal metrics from a Zookeeper server.', - longDescription: 'The `' + moduleName + '` Metricbeat module fetches internal metrics from a Zookeeper server.' + - ' [Learn more]({config.docs.beats.metricbeat}/metricbeat-module-' + moduleName + '.html).', + shortDescription: i18n.translate('kbn.server.tutorials.zookeeperMetrics.shortDescription', { + defaultMessage: 'Fetch interal metrics from a Zookeeper server.', + }), + longDescription: i18n.translate('kbn.server.tutorials.zookeeperMetrics.longDescription', { + // eslint-disable-next-line no-multi-str + defaultMessage: 'The `{moduleName}` Metricbeat module fetches internal metrics from a Zookeeper server. \ +[Learn more]({learnMoreLink}).', + values: { + moduleName, + learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-' + moduleName + '.html', + }, + }), artifacts: { application: { - label: 'Discover', + label: i18n.translate('kbn.server.tutorials.zookeeperMetrics.artifacts.application.label', { + defaultMessage: 'Discover', + }), path: '/app/kibana#/discover' }, dashboards: [], diff --git a/src/core_plugins/kibana/ui_setting_defaults.js b/src/core_plugins/kibana/ui_setting_defaults.js index dc00edaad0b27c..acfb03db0e3927 100644 --- a/src/core_plugins/kibana/ui_setting_defaults.js +++ b/src/core_plugins/kibana/ui_setting_defaults.js @@ -510,5 +510,13 @@ export function getUiSettingDefaults() { is present and sortable in the current index pattern is used.`, category: ['discover'], }, + 'accessibility:disableAnimations': { + name: 'Disable Animations', + value: false, + description: ` + Turn off all unnecessary animations in the Kibana UI. Refresh the page to apply the changes. + `, + category: ['accessibility'], + }, }; } diff --git a/src/core_plugins/status_page/public/index.scss b/src/core_plugins/status_page/public/index.scss index bef40abe1a8cfc..8bab6ef41e1589 100644 --- a/src/core_plugins/status_page/public/index.scss +++ b/src/core_plugins/status_page/public/index.scss @@ -1,4 +1,4 @@ -@import '../../../ui/public/styles/styling_constants'; +@import 'ui/public/styles/styling_constants'; // SASSTODO: Remove when K7 applies background color to body .stsPage { diff --git a/src/core_plugins/status_page/public/lib/load_status.js b/src/core_plugins/status_page/public/lib/load_status.js index d21fe0c9e9b5a0..9cb1046005ea00 100644 --- a/src/core_plugins/status_page/public/lib/load_status.js +++ b/src/core_plugins/status_page/public/lib/load_status.js @@ -20,7 +20,7 @@ import _ from 'lodash'; import chrome from 'ui/chrome'; -import { notify } from 'ui/notify'; +import { toastNotifications } from 'ui/notify'; import { i18n } from '@kbn/i18n'; // Module-level error returned by notify.error @@ -130,7 +130,7 @@ async function loadStatus(fetchFn = fetchData) { }, ); - errorNotif = notify.error(serverIsDownErrorMessage); + errorNotif = toastNotifications.addDanger(serverIsDownErrorMessage); return e; } @@ -144,7 +144,7 @@ async function loadStatus(fetchFn = fetchData) { }, ); - errorNotif = notify.error(serverStatusCodeErrorMessage); + errorNotif = toastNotifications.addDanger(serverStatusCodeErrorMessage); return; } diff --git a/src/dev/build/cli.js b/src/dev/build/cli.js index 2c42dec6d82d2e..8107a7b35b885f 100644 --- a/src/dev/build/cli.js +++ b/src/dev/build/cli.js @@ -23,7 +23,7 @@ import getopts from 'getopts'; import dedent from 'dedent'; import chalk from 'chalk'; -import { createToolingLog, pickLevelFromFlags } from '@kbn/dev-utils'; +import { ToolingLog, pickLevelFromFlags } from '@kbn/dev-utils'; import { buildDistributables } from './build_distributables'; import { isErrorLogged } from './lib'; @@ -82,8 +82,10 @@ if (flags.help) { process.exit(1); } -const log = createToolingLog(pickLevelFromFlags(flags)); -log.pipe(process.stdout); +const log = new ToolingLog({ + level: pickLevelFromFlags(flags), + writeTo: process.stdout +}); function isOsPackageDesired(name) { if (flags['skip-os-packages']) { diff --git a/src/dev/build/lib/__tests__/exec.js b/src/dev/build/lib/__tests__/exec.js index 33ade585bd3af3..9d1ead0c3f22b3 100644 --- a/src/dev/build/lib/__tests__/exec.js +++ b/src/dev/build/lib/__tests__/exec.js @@ -20,16 +20,22 @@ import sinon from 'sinon'; import stripAnsi from 'strip-ansi'; -import { createToolingLog } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; import { exec } from '../exec'; describe('dev/build/lib/exec', () => { const sandbox = sinon.createSandbox(); afterEach(() => sandbox.reset()); - const log = createToolingLog('verbose'); const onLogLine = sandbox.stub(); - log.on('data', line => onLogLine(stripAnsi(line))); + const log = new ToolingLog({ + level: 'verbose', + writeTo: { + write: chunk => { + onLogLine(stripAnsi(chunk)); + } + } + }); it('executes a command, logs the command, and logs the output', async () => { await exec(log, process.execPath, ['-e', 'console.log("hi")']); diff --git a/src/dev/build/lib/__tests__/runner.js b/src/dev/build/lib/__tests__/runner.js index b5a02bef9117a2..9b51a724f06c52 100644 --- a/src/dev/build/lib/__tests__/runner.js +++ b/src/dev/build/lib/__tests__/runner.js @@ -20,7 +20,7 @@ import sinon from 'sinon'; import expect from 'expect.js'; -import { createToolingLog } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; import { createRunner } from '../runner'; import { isErrorLogged, markErrorLogged } from '../errors'; @@ -29,9 +29,13 @@ describe('dev/build/lib/runner', () => { const config = {}; - const log = createToolingLog('verbose'); const onLogLine = sandbox.stub(); - log.on('data', onLogLine); + const log = new ToolingLog({ + level: 'verbose', + writeTo: { + write: onLogLine + } + }); const buildMatcher = sinon.match({ isOss: sinon.match.func, diff --git a/src/dev/build/tasks/nodejs/__tests__/download.js b/src/dev/build/tasks/nodejs/__tests__/download.js index 5041ed0521760f..a3514d3254ccef 100644 --- a/src/dev/build/tasks/nodejs/__tests__/download.js +++ b/src/dev/build/tasks/nodejs/__tests__/download.js @@ -26,7 +26,7 @@ import sinon from 'sinon'; import expect from 'expect.js'; import Wreck from 'wreck'; -import { createToolingLog } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; import { download } from '../download'; const TMP_DESTINATION = resolve(__dirname, '__tmp__'); @@ -41,9 +41,13 @@ describe('src/dev/build/tasks/nodejs/download', () => { const sandbox = sinon.createSandbox(); afterEach(() => sandbox.reset()); - const log = createToolingLog('verbose'); const onLogLine = sandbox.stub(); - log.on('data', onLogLine); + const log = new ToolingLog({ + level: 'verbose', + writeTo: { + write: onLogLine + } + }); const FOO_SHA256 = '2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae'; const createSendHandler = (send) => (req, res) => { diff --git a/src/dev/build/tasks/transpile_scss_task.js b/src/dev/build/tasks/transpile_scss_task.js index 69621797ea2526..04a5ed03bd6394 100644 --- a/src/dev/build/tasks/transpile_scss_task.js +++ b/src/dev/build/tasks/transpile_scss_task.js @@ -26,7 +26,9 @@ export const TranspileScssTask = { async run(config, log, build) { const scanDirs = [ build.resolvePath('src/core_plugins') ]; - const { spec$ } = findPluginSpecs({ plugins: { scanDirs, paths: [] } }); + const paths = [ build.resolvePath('node_modules/x-pack') ]; + + const { spec$ } = findPluginSpecs({ plugins: { scanDirs, paths } }); const enabledPlugins = await spec$.pipe(toArray()).toPromise(); try { diff --git a/src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_2/test_file.html b/src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_2/test_file.html index b102216c7d12e6..c5d01f0c9c973d 100644 --- a/src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_2/test_file.html +++ b/src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_2/test_file.html @@ -1 +1 @@ -

{{ 'test-plugin.message-id' | i18n: { defaultMessage: 'Message text' } }}

+

{{ 'plugin_2.message-id' | i18n: { defaultMessage: 'Message text' } }}

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 409e6dc08d4807..b84544e6207039 100644 --- a/src/dev/i18n/__snapshots__/extract_default_translations.test.js.snap +++ b/src/dev/i18n/__snapshots__/extract_default_translations.test.js.snap @@ -2,132 +2,135 @@ exports[`dev/i18n/extract_default_translations extracts messages to en.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', - }, + \\"formats\\": { + \\"number\\": { + \\"currency\\": { + \\"style\\": \\"currency\\" + }, + \\"percent\\": { + \\"style\\": \\"percent\\" + } }, - 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', - }, + \\"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\\" + } + } + }, + \\"plugin_1.id_1\\": \\"Message 1\\", + \\"plugin_1.id_2\\": { + \\"text\\": \\"Message 2\\", + \\"comment\\": \\"Message context\\" }, - 'plugin_1.id_1': 'Message 1', - 'plugin_1.id_2': 'Message 2', // 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', -} -" + \\"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', - }, - }, - 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', - }, + \\"formats\\": { + \\"number\\": { + \\"currency\\": { + \\"style\\": \\"currency\\" + }, + \\"percent\\": { + \\"style\\": \\"percent\\" + } }, - 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', - }, + \\"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\\" + } + } }, - 'test-plugin.message-id': 'Message text', -} -" + \\"plugin_2.message-id\\": \\"Message text\\" +}" `; + +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/extract_default_translations.js b/src/dev/i18n/extract_default_translations.js index 3d498f4e0cde79..645c43ed319d15 100644 --- a/src/dev/i18n/extract_default_translations.js +++ b/src/dev/i18n/extract_default_translations.js @@ -17,15 +17,19 @@ * under the License. */ -import { resolve } from 'path'; +import path from 'path'; import { i18n } from '@kbn/i18n'; import JSON5 from 'json5'; +import normalize from 'normalize-path'; 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, makeDirAsync, accessAsync, readFileAsync, writeFileAsync } from './utils'; +import { globAsync, readFileAsync, writeFileAsync } from './utils'; +import { paths, exclude } from '../../../.i18nrc.json'; + +const ESCAPE_SINGLE_QUOTE_REGEX = /\\([\s\S])|(')/g; function addMessageToMap(targetMap, key, value) { const existingValue = targetMap.get(key); @@ -38,7 +42,48 @@ function addMessageToMap(targetMap, key, value) { targetMap.set(key, value); } -export async function extractDefaultTranslations(inputPath) { +function normalizePath(inputPath) { + return normalize(path.relative('.', inputPath)); +} + +function filterPaths(inputPaths) { + const availablePaths = Object.values(paths); + const pathsForExtraction = new Set(); + + for (const inputPath of inputPaths) { + const normalizedPath = normalizePath(inputPath); + + // If input path is the sub path of or equal to any available path, include it. + if ( + availablePaths.some(path => normalizedPath.startsWith(`${path}/`) || path === normalizedPath) + ) { + pathsForExtraction.add(normalizedPath); + } else { + // Otherwise go through all available paths and see if any of them is the sub + // path of the input path (empty normalized path corresponds to root or above). + availablePaths + .filter(path => !normalizedPath || path.startsWith(`${normalizedPath}/`)) + .forEach(ePath => pathsForExtraction.add(ePath)); + } + } + + return [...pathsForExtraction]; +} + +export function validateMessageNamespace(id, filePath) { + const normalizedPath = normalizePath(filePath); + + const [expectedNamespace] = Object.entries(paths).find(([, pluginPath]) => + normalizedPath.startsWith(`${pluginPath}/`) + ); + + if (!id.startsWith(`${expectedNamespace}.`)) { + throw new Error(`Expected "${id}" id to have "${expectedNamespace}" namespace. \ +See i18nrc.json for the list of supported namespaces.`); + } +} + +export async function extractMessagesFromPathToMap(inputPath, targetMap) { const entries = await globAsync('*.{js,jsx,pug,ts,tsx,html,hbs,handlebars}', { cwd: inputPath, matchBase: true, @@ -46,7 +91,7 @@ export async function extractDefaultTranslations(inputPath) { const { htmlEntries, codeEntries, pugEntries, hbsEntries } = entries.reduce( (paths, entry) => { - const resolvedPath = resolve(inputPath, entry); + const resolvedPath = path.resolve(inputPath, entry); if (resolvedPath.endsWith('.html')) { paths.htmlEntries.push(resolvedPath); @@ -63,8 +108,6 @@ export async function extractDefaultTranslations(inputPath) { { htmlEntries: [], codeEntries: [], pugEntries: [], hbsEntries: [] } ); - const defaultMessagesMap = new Map(); - await Promise.all( [ [htmlEntries, extractHtmlMessages], @@ -73,7 +116,7 @@ export async function extractDefaultTranslations(inputPath) { [hbsEntries, extractHandlebarsMessages], ].map(async ([entries, extractFunction]) => { const files = await Promise.all( - entries.map(async entry => { + entries.filter(entry => !exclude.includes(normalizePath(entry))).map(async entry => { return { name: entry, content: await readFileAsync(entry), @@ -84,7 +127,8 @@ export async function extractDefaultTranslations(inputPath) { for (const { name, content } of files) { try { for (const [id, value] of extractFunction(content)) { - addMessageToMap(defaultMessagesMap, id, value); + validateMessageNamespace(id, name); + addMessageToMap(targetMap, id, value); } } catch (error) { throw new Error(`Error in ${name}\n${error.message || error}`); @@ -92,32 +136,65 @@ export async function extractDefaultTranslations(inputPath) { } }) ); +} +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) ); - const defaultMessages = [...defaultMessagesMap].sort(([key1], [key2]) => { - return key1 < key2 ? -1 : 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}': '${mapValue.message}',`), - Buffer.from(mapValue.context ? ` // ${mapValue.context}\n` : '\n'), + Buffer.from(` '${mapKey}': '${formattedMessage}',`), + Buffer.from(formattedContext ? ` // ${formattedContext}\n` : '\n'), ]); } // append previously removed closing curly brace jsonBuffer = Buffer.concat([jsonBuffer, Buffer.from('}\n')]); - try { - await accessAsync(resolve(inputPath, 'translations')); - } catch (_) { - await makeDirAsync(resolve(inputPath, 'translations')); + 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; + } } - await writeFileAsync(resolve(inputPath, 'translations', 'en.json'), jsonBuffer); + 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 43b4b4d70adcdd..b1e3ade402ebc5 100644 --- a/src/dev/i18n/extract_default_translations.test.js +++ b/src/dev/i18n/extract_default_translations.test.js @@ -18,14 +18,11 @@ */ import path from 'path'; -import fs from 'fs'; -import { promisify } from 'util'; -import { extractDefaultTranslations } from './extract_default_translations'; - -const readFileAsync = promisify(fs.readFile); -const removeDirAsync = promisify(fs.rmdir); -const unlinkAsync = promisify(fs.unlink); +import { + extractDefaultTranslations, + validateMessageNamespace, +} from './extract_default_translations'; const fixturesPath = path.resolve(__dirname, '__fixtures__', 'extract_default_translations'); const pluginsPaths = [ @@ -34,40 +31,72 @@ const pluginsPaths = [ path.join(fixturesPath, 'test_plugin_3'), ]; +jest.mock('../../../.i18nrc.json', () => ({ + paths: { + plugin_1: 'src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_1', + plugin_2: 'src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_2', + plugin_3: 'src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_3', + }, + exclude: [], +})); + +const utils = require('./utils'); +utils.writeFileAsync = jest.fn(); + describe('dev/i18n/extract_default_translations', () => { test('extracts messages to en.json', async () => { const [pluginPath] = pluginsPaths; - await extractDefaultTranslations(pluginPath); - const extractedJSONBuffer = await readFileAsync( - path.join(pluginPath, 'translations', 'en.json') - ); + utils.writeFileAsync.mockClear(); + await extractDefaultTranslations({ + paths: [pluginPath], + output: pluginPath, + }); - await unlinkAsync(path.join(pluginPath, 'translations', 'en.json')); - await removeDirAsync(path.join(pluginPath, 'translations')); + const [[, json]] = utils.writeFileAsync.mock.calls; - expect(extractedJSONBuffer.toString()).toMatchSnapshot(); + expect(json.toString()).toMatchSnapshot(); }); test('injects default formats into en.json', async () => { const [, pluginPath] = pluginsPaths; - await extractDefaultTranslations(pluginPath); - const extractedJSONBuffer = await readFileAsync( - path.join(pluginPath, 'translations', 'en.json') - ); + utils.writeFileAsync.mockClear(); + await extractDefaultTranslations({ + paths: [pluginPath], + output: pluginPath, + }); - await unlinkAsync(path.join(pluginPath, 'translations', 'en.json')); - await removeDirAsync(path.join(pluginPath, 'translations')); + const [[, json]] = utils.writeFileAsync.mock.calls; - expect(extractedJSONBuffer.toString()).toMatchSnapshot(); + expect(json.toString()).toMatchSnapshot(); }); test('throws on id collision', async () => { const [, , pluginPath] = pluginsPaths; - await expect(extractDefaultTranslations(pluginPath)).rejects.toMatchObject({ + await expect( + extractDefaultTranslations({ paths: [pluginPath], output: pluginPath }) + ).rejects.toMatchObject({ message: `Error in ${path.join(pluginPath, 'test_file.jsx')} There is more than one default message for the same id "plugin_3.duplicate_id": "Message 1" and "Message 2"`, }); }); + + test('validates message namespace', () => { + const id = 'plugin_2.message-id'; + const filePath = path.resolve( + __dirname, + '__fixtures__/extract_default_translations/test_plugin_2/test_file.html' + ); + expect(() => validateMessageNamespace(id, filePath)).not.toThrow(); + }); + + test('throws on wrong message namespace', () => { + const id = 'wrong_plugin_namespace.message-id'; + const filePath = path.resolve( + __dirname, + '__fixtures__/extract_default_translations/test_plugin_2/test_file.html' + ); + expect(() => validateMessageNamespace(id, filePath)).toThrowErrorMatchingSnapshot(); + }); }); diff --git a/src/dev/i18n/utils.js b/src/dev/i18n/utils.js index fad17eeae9d0f4..af01c4721f14ad 100644 --- a/src/dev/i18n/utils.js +++ b/src/dev/i18n/utils.js @@ -29,15 +29,12 @@ import glob from 'glob'; import { promisify } from 'util'; const ESCAPE_LINE_BREAK_REGEX = /(? { classname: sharedClassname, name: 'SUITE works', time: testPass.$.time, - } + }, + 'system-out': testPass['system-out'] }); expect(testFail.$.time).to.match(DURATION_REGEX); @@ -104,6 +105,7 @@ describe('dev/mocha/junit report generation', () => { name: 'SUITE fails', time: testFail.$.time, }, + 'system-out': testFail['system-out'], failure: [ testFail.failure[0] ] @@ -118,6 +120,7 @@ describe('dev/mocha/junit report generation', () => { name: 'SUITE SUB_SUITE "before each" hook: fail hook for "never runs"', time: beforeEachFail.$.time, }, + 'system-out': testFail['system-out'], failure: [ beforeEachFail.failure[0] ] @@ -128,6 +131,7 @@ describe('dev/mocha/junit report generation', () => { classname: sharedClassname, name: 'SUITE SUB_SUITE never runs', }, + 'system-out': testFail['system-out'], skipped: [''] }); }); diff --git a/src/dev/mocha/junit_report_generation.js b/src/dev/mocha/junit_report_generation.js index 5572d4287a241f..f3bc741f8f2b22 100644 --- a/src/dev/mocha/junit_report_generation.js +++ b/src/dev/mocha/junit_report_generation.js @@ -23,6 +23,27 @@ import { inspect } from 'util'; import mkdirp from 'mkdirp'; import xmlBuilder from 'xmlbuilder'; +import stripAnsi from 'strip-ansi'; +import regenerate from 'regenerate'; + +import { getSnapshotOfRunnableLogs } from './log_cache'; + +// create a regular expression using regenerate() that selects any character that is explicitly allowed by https://www.w3.org/TR/xml/#NT-Char +const validXmlCharsRE = new RegExp( + `(?:${ + regenerate() + .add(0x9, 0xA, 0xD) + .addRange(0x20, 0xD7FF) + .addRange(0xE000, 0xFFFD) + .addRange(0x10000, 0x10FFFF) + .toString() + })*`, + 'g' +); + +function escapeCdata(string) { + return stripAnsi(string).match(validXmlCharsRE).join(''); +} export function setupJUnitReportGeneration(runner, options = {}) { const { @@ -120,9 +141,12 @@ export function setupJUnitReportGeneration(runner, options = {}) { [...results, ...skippedResults].forEach(result => { const el = addTestcaseEl(result.node); + el.ele('system-out').dat( + escapeCdata(getSnapshotOfRunnableLogs(result.node) || '') + ); if (result.failed) { - el.ele('failure').dat(inspect(result.error)); + el.ele('failure').dat(escapeCdata(inspect(result.error))); return; } diff --git a/src/dev/mocha/log_cache.js b/src/dev/mocha/log_cache.js new file mode 100644 index 00000000000000..e879beffbd0c05 --- /dev/null +++ b/src/dev/mocha/log_cache.js @@ -0,0 +1,62 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const cachedSuiteLogs = new WeakMap(); +const cachesRunnableLogs = new WeakMap(); + +/** + * Add a chunk of log output to the cached + * output for a suite + * @param {Mocha.Suite} suite + * @param {string} chunk + */ +export function recordLog(suite, chunk) { + const cache = cachedSuiteLogs.get(suite) || ''; + cachedSuiteLogs.set(suite, cache + chunk); +} + +/** + * Recursively walk up from a runnable to collect + * the cached log for its suite and all its parents + * @param {Mocha.Suite} suite + */ +function getCurrentCachedSuiteLogs(suite) { + const history = suite.parent ? getCurrentCachedSuiteLogs(suite.parent) : ''; + const ownLogs = cachedSuiteLogs.get(suite) || ''; + return history + ownLogs; +} + +/** + * Snapshot the logs from this runnable's suite at this point, + * as the suite logs will get updated to include output from + * subsequent runnables + * @param {Mocha.Runnable} runnable + */ +export function snapshotLogsForRunnable(runnable) { + cachesRunnableLogs.set(runnable, getCurrentCachedSuiteLogs(runnable.parent)); +} + +/** + * Get the suite logs as they were when the logs for this runnable + * were snapshotted + * @param {Mocha.Runnable} runnable + */ +export function getSnapshotOfRunnableLogs(runnable) { + return cachesRunnableLogs.get(runnable); +} diff --git a/src/dev/notice/cli.js b/src/dev/notice/cli.js index ed621a753ec3de..7c1ab57445b6f1 100644 --- a/src/dev/notice/cli.js +++ b/src/dev/notice/cli.js @@ -22,7 +22,7 @@ import { resolve } from 'path'; import getopts from 'getopts'; import dedent from 'dedent'; -import { createToolingLog, pickLevelFromFlags } from '@kbn/dev-utils'; +import { ToolingLog, pickLevelFromFlags } from '@kbn/dev-utils'; import { REPO_ROOT } from '../constants'; import { generateNoticeFromSource } from './generate_notice_from_source'; @@ -40,8 +40,10 @@ const opts = getopts(process.argv.slice(2), { } }); -const log = createToolingLog(pickLevelFromFlags(opts)); -log.pipe(process.stdout); +const log = new ToolingLog({ + level: pickLevelFromFlags(opts), + writeTo: process.stdout +}); if (unknownFlags.length) { log.error(`Unknown flags ${unknownFlags.map(f => `"${f}"`).join(',')}`); diff --git a/src/dev/run/run.js b/src/dev/run/run.js index e37cd3d7661fd9..d7d0b4ec30c725 100644 --- a/src/dev/run/run.js +++ b/src/dev/run/run.js @@ -17,7 +17,7 @@ * under the License. */ -import { createToolingLog, pickLevelFromFlags } from '@kbn/dev-utils'; +import { ToolingLog, pickLevelFromFlags } from '@kbn/dev-utils'; import { isFailError } from './fail'; import { getFlags, getHelp } from './flags'; @@ -29,8 +29,10 @@ export async function run(body) { process.exit(1); } - const log = createToolingLog(pickLevelFromFlags(flags)); - log.pipe(process.stdout); + const log = new ToolingLog({ + level: pickLevelFromFlags(flags), + writeTo: process.stdout + }); try { await body({ log, flags }); diff --git a/src/dev/run_extract_default_translations.js b/src/dev/run_extract_default_translations.js index f883172d9af59e..9d5f0011d6f387 100644 --- a/src/dev/run_extract_default_translations.js +++ b/src/dev/run_extract_default_translations.js @@ -20,8 +20,10 @@ import { run } from './run'; import { extractDefaultTranslations } from './i18n/extract_default_translations'; -run(async () => { - for (const inputPath of process.argv.slice(2)) { - await extractDefaultTranslations(inputPath); - } +run(async ({ flags: { path, output, 'output-format': outputFormat } }) => { + await extractDefaultTranslations({ + paths: Array.isArray(path) ? path : [path || './'], + output, + outputFormat, + }); }); diff --git a/src/dev/tslint/run_tslint_cli.ts b/src/dev/tslint/run_tslint_cli.ts index 4da2c7bca789ba..28840e45d54a4d 100644 --- a/src/dev/tslint/run_tslint_cli.ts +++ b/src/dev/tslint/run_tslint_cli.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createToolingLog } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; import getopts from 'getopts'; import { execInProjects, filterProjectsByFlag, Project } from '../typescript'; @@ -27,8 +27,10 @@ export function runTslintCliOnTsConfigPaths(tsConfigPaths: string[]) { } export function runTslintCli(projects?: Project[]) { - const log = createToolingLog('info'); - log.pipe(process.stdout); + const log = new ToolingLog({ + level: 'info', + writeTo: process.stdout, + }); const opts = getopts(process.argv.slice(2)); projects = projects || filterProjectsByFlag(opts.project); diff --git a/src/dev/typescript/run_type_check_cli.ts b/src/dev/typescript/run_type_check_cli.ts index 91f072dbba723c..f0ddf6f56e359c 100644 --- a/src/dev/typescript/run_type_check_cli.ts +++ b/src/dev/typescript/run_type_check_cli.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createToolingLog } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; import chalk from 'chalk'; import dedent from 'dedent'; import getopts from 'getopts'; @@ -38,8 +38,10 @@ export function runTypeCheckCli() { }, }); - const log = createToolingLog('info'); - log.pipe(process.stdout); + const log = new ToolingLog({ + level: 'info', + writeTo: process.stdout, + }); if (extraFlags.length) { for (const flag of extraFlags) { diff --git a/src/es_archiver/cli.js b/src/es_archiver/cli.js index 456e43b4f5e917..902da0fcbefc65 100644 --- a/src/es_archiver/cli.js +++ b/src/es_archiver/cli.js @@ -31,7 +31,7 @@ import { Command } from 'commander'; import elasticsearch from 'elasticsearch'; import { EsArchiver } from './es_archiver'; -import { createToolingLog } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; import { readConfigFile } from '../functional_test_runner'; const cmd = new Command('node scripts/es_archiver'); @@ -76,8 +76,10 @@ if (missingCommand) { async function execute(fn) { try { - const log = createToolingLog(cmd.verbose ? 'debug' : 'info'); - log.pipe(process.stdout); + const log = new ToolingLog({ + level: cmd.verbose ? 'debug' : 'info', + writeTo: process.stdout + }); if (cmd.config) { // load default values from the specified config file diff --git a/src/es_archiver/es_archiver.js b/src/es_archiver/es_archiver.js index bc4f79b8f8075a..e600a37d98fe43 100644 --- a/src/es_archiver/es_archiver.js +++ b/src/es_archiver/es_archiver.js @@ -95,7 +95,7 @@ export class EsArchiver { * @return Promise */ async rebuildAll() { - return rebuildAllAction({ + return await rebuildAllAction({ client: this.client, dataDir: this.dataDir, log: this.log @@ -109,6 +109,6 @@ export class EsArchiver { * @return Promise */ async loadIfNeeded(name) { - return this.load(name, { skipExisting: true }); + return await this.load(name, { skipExisting: true }); } } diff --git a/src/es_archiver/lib/__tests__/stats.js b/src/es_archiver/lib/__tests__/stats.js index eeb4f18766d662..eb7d9f09f858df 100644 --- a/src/es_archiver/lib/__tests__/stats.js +++ b/src/es_archiver/lib/__tests__/stats.js @@ -22,18 +22,17 @@ import { uniq } from 'lodash'; import sinon from 'sinon'; import { createStats } from '../'; -import { createToolingLog } from '@kbn/dev-utils'; -import { - createConcatStream, - createPromiseFromStreams -} from '../../../utils'; +import { ToolingLog } from '@kbn/dev-utils'; -function drain(log) { - log.end(); - return createPromiseFromStreams([ - log, - createConcatStream('') - ]); +function createBufferedLog() { + const log = new ToolingLog({ + level: 'debug', + writeTo: { + write: (chunk) => log.buffer += chunk + } + }); + log.buffer = ''; + return log; } function assertDeepClones(a, b) { @@ -66,110 +65,106 @@ function assertDeepClones(a, b) { describe('esArchiver: Stats', () => { describe('#skippedIndex(index)', () => { it('marks the index as skipped', () => { - const stats = createStats('name', createToolingLog()); + const stats = createStats('name', new ToolingLog()); stats.skippedIndex('index-name'); const indexStats = stats.toJSON()['index-name']; expect(indexStats).to.have.property('skipped', true); }); it('logs that the index was skipped', async () => { - const log = createToolingLog('debug'); + const log = createBufferedLog(); const stats = createStats('name', log); stats.skippedIndex('index-name'); - expect(await drain(log)).to.contain('Skipped'); + expect(log.buffer).to.contain('Skipped'); }); }); describe('#deletedIndex(index)', () => { it('marks the index as deleted', () => { - const stats = createStats('name', createToolingLog()); + const stats = createStats('name', new ToolingLog()); stats.deletedIndex('index-name'); const indexStats = stats.toJSON()['index-name']; expect(indexStats).to.have.property('deleted', true); }); it('logs that the index was deleted', async () => { - const log = createToolingLog('debug'); + const log = createBufferedLog(); const stats = createStats('name', log); stats.deletedIndex('index-name'); - expect(await drain(log)).to.contain('Deleted'); + expect(log.buffer).to.contain('Deleted'); }); }); describe('#createdIndex(index, [metadata])', () => { it('marks the index as created', () => { - const stats = createStats('name', createToolingLog()); + const stats = createStats('name', new ToolingLog()); stats.createdIndex('index-name'); const indexStats = stats.toJSON()['index-name']; expect(indexStats).to.have.property('created', true); }); it('logs that the index was created', async () => { - const log = createToolingLog('debug'); + const log = createBufferedLog(); const stats = createStats('name', log); stats.createdIndex('index-name'); - expect(await drain(log)).to.contain('Created'); + expect(log.buffer).to.contain('Created'); }); describe('with metadata', () => { it('debug-logs each key from the metadata', async () => { - const log = createToolingLog('debug'); + const log = createBufferedLog(); const stats = createStats('name', log); stats.createdIndex('index-name', { foo: 'bar' }); - const output = await drain(log); - expect(output).to.contain('debg'); - expect(output).to.contain('foo "bar"'); + expect(log.buffer).to.contain('debg'); + expect(log.buffer).to.contain('foo "bar"'); }); }); describe('without metadata', () => { it('no debug logging', async () => { - const log = createToolingLog('debug'); + const log = createBufferedLog(); const stats = createStats('name', log); stats.createdIndex('index-name'); - const output = await drain(log); - expect(output).to.not.contain('debg'); + expect(log.buffer).to.not.contain('debg'); }); }); }); describe('#archivedIndex(index, [metadata])', () => { it('marks the index as archived', () => { - const stats = createStats('name', createToolingLog()); + const stats = createStats('name', new ToolingLog()); stats.archivedIndex('index-name'); const indexStats = stats.toJSON()['index-name']; expect(indexStats).to.have.property('archived', true); }); it('logs that the index was archived', async () => { - const log = createToolingLog('debug'); + const log = createBufferedLog(); const stats = createStats('name', log); stats.archivedIndex('index-name'); - expect(await drain(log)).to.contain('Archived'); + expect(log.buffer).to.contain('Archived'); }); describe('with metadata', () => { it('debug-logs each key from the metadata', async () => { - const log = createToolingLog('debug'); + const log = createBufferedLog(); const stats = createStats('name', log); stats.archivedIndex('index-name', { foo: 'bar' }); - const output = await drain(log); - expect(output).to.contain('debg'); - expect(output).to.contain('foo "bar"'); + expect(log.buffer).to.contain('debg'); + expect(log.buffer).to.contain('foo "bar"'); }); }); describe('without metadata', () => { it('no debug logging', async () => { - const log = createToolingLog('debug'); + const log = createBufferedLog(); const stats = createStats('name', log); stats.archivedIndex('index-name'); - const output = await drain(log); - expect(output).to.not.contain('debg'); + expect(log.buffer).to.not.contain('debg'); }); }); }); describe('#indexedDoc(index)', () => { it('increases the docs.indexed count for the index', () => { - const stats = createStats('name', createToolingLog()); + const stats = createStats('name', new ToolingLog()); stats.indexedDoc('index-name'); expect(stats.toJSON()['index-name'].docs.indexed).to.be(1); stats.indexedDoc('index-name'); @@ -180,7 +175,7 @@ describe('esArchiver: Stats', () => { describe('#archivedDoc(index)', () => { it('increases the docs.archived count for the index', () => { - const stats = createStats('name', createToolingLog()); + const stats = createStats('name', new ToolingLog()); stats.archivedDoc('index-name'); expect(stats.toJSON()['index-name'].docs.archived).to.be(1); stats.archivedDoc('index-name'); @@ -191,13 +186,13 @@ describe('esArchiver: Stats', () => { describe('#toJSON()', () => { it('returns the stats for all indexes', () => { - const stats = createStats('name', createToolingLog()); + const stats = createStats('name', new ToolingLog()); stats.archivedIndex('index1'); stats.archivedIndex('index2'); expect(Object.keys(stats.toJSON())).to.eql(['index1', 'index2']); }); it('returns a deep clone of the stats', () => { - const stats = createStats('name', createToolingLog()); + const stats = createStats('name', new ToolingLog()); stats.archivedIndex('index1'); stats.archivedIndex('index2'); stats.deletedIndex('index3'); @@ -208,7 +203,7 @@ describe('esArchiver: Stats', () => { describe('#forEachIndex(fn)', () => { it('iterates a clone of the index stats', () => { - const stats = createStats('name', createToolingLog()); + const stats = createStats('name', new ToolingLog()); stats.archivedIndex('index1'); stats.archivedIndex('index2'); stats.deletedIndex('index3'); diff --git a/src/functional_test_runner/__tests__/fixtures/failure_hooks/config.js b/src/functional_test_runner/__tests__/fixtures/failure_hooks/config.js index 6acfb728161a40..3ff674c89682d9 100644 --- a/src/functional_test_runner/__tests__/fixtures/failure_hooks/config.js +++ b/src/functional_test_runner/__tests__/fixtures/failure_hooks/config.js @@ -42,6 +42,9 @@ export default function () { log.info('testHookFailureAfterDelay %s %s', err.message, test.fullTitle()); }); } + }, + mochaReporter: { + captureLogOutput: false } }; } diff --git a/src/functional_test_runner/cli.js b/src/functional_test_runner/cli.js index 3a2bd374788220..f784245c5716f4 100644 --- a/src/functional_test_runner/cli.js +++ b/src/functional_test_runner/cli.js @@ -21,7 +21,7 @@ import { resolve } from 'path'; import { Command } from 'commander'; -import { createToolingLog } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; import { createFunctionalTestRunner } from './functional_test_runner'; const cmd = new Command('node scripts/functional_test_runner'); @@ -46,8 +46,10 @@ if (cmd.quiet) logLevel = 'error'; if (cmd.debug) logLevel = 'debug'; if (cmd.verbose) logLevel = 'verbose'; -const log = createToolingLog(logLevel); -log.pipe(process.stdout); +const log = new ToolingLog({ + level: logLevel, + writeTo: process.stdout +}); const functionalTestRunner = createFunctionalTestRunner({ log, diff --git a/src/functional_test_runner/lib/config/__tests__/read_config_file.js b/src/functional_test_runner/lib/config/__tests__/read_config_file.js index 20bcb25c6c7ba4..d9da00d9b3c817 100644 --- a/src/functional_test_runner/lib/config/__tests__/read_config_file.js +++ b/src/functional_test_runner/lib/config/__tests__/read_config_file.js @@ -19,11 +19,11 @@ import expect from 'expect.js'; -import { createToolingLog } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; import { readConfigFile } from '../read_config_file'; import { Config } from '../config'; -const log = createToolingLog().resume(); +const log = new ToolingLog(); describe('readConfigFile()', () => { it('reads config from a file, returns an instance of Config class', async () => { diff --git a/src/functional_test_runner/lib/config/schema.js b/src/functional_test_runner/lib/config/schema.js index 93450fc88732cc..faf7de7fea9403 100644 --- a/src/functional_test_runner/lib/config/schema.js +++ b/src/functional_test_runner/lib/config/schema.js @@ -96,6 +96,10 @@ export const schema = Joi.object().keys({ rootDirectory: Joi.string(), }).default(), + mochaReporter: Joi.object().keys({ + captureLogOutput: Joi.boolean().default(!!process.env.CI), + }).default(), + users: Joi.object().pattern( ID_PATTERN, Joi.object().keys({ @@ -138,7 +142,12 @@ export const schema = Joi.object().keys({ // settings for the esArchiver module esArchiver: Joi.object().keys({ - directory: Joi.string().default(defaultRelativeToConfigPath('fixtures/es_archiver')) + directory: Joi.string().default(defaultRelativeToConfigPath('fixtures/es_archiver')), + }).default(), + + // settings for the kibanaServer.uiSettings module + uiSettings: Joi.object().keys({ + defaults: Joi.object().unknown(true) }).default(), // settings for the screenshots module diff --git a/src/functional_test_runner/lib/mocha/reporter/reporter.js b/src/functional_test_runner/lib/mocha/reporter/reporter.js index efced410ab937a..39c69f914e43f0 100644 --- a/src/functional_test_runner/lib/mocha/reporter/reporter.js +++ b/src/functional_test_runner/lib/mocha/reporter/reporter.js @@ -20,16 +20,19 @@ import { format } from 'util'; import Mocha from 'mocha'; +import { ToolingLogTextWriter } from '@kbn/dev-utils'; import { setupJUnitReportGeneration } from '../../../../dev'; import * as colors from './colors'; import * as symbols from './symbols'; import { ms } from './ms'; import { writeEpilogue } from './write_epilogue'; +import { recordLog, snapshotLogsForRunnable } from '../../../../dev/mocha/log_cache'; export function MochaReporterProvider({ getService }) { const log = getService('log'); const config = getService('config'); + let originalLogWriters; return class MochaReporter extends Mocha.reporters.Base { constructor(runner, options) { @@ -55,11 +58,40 @@ export function MochaReporterProvider({ getService }) { } onStart = () => { + if (config.get('mochaReporter.captureLogOutput')) { + log.warning('debug logs are being captured, only error logs will be written to the console'); + originalLogWriters = log.getWriters(); + log.setWriters([ + new ToolingLogTextWriter({ + level: 'error', + writeTo: process.stdout + }), + new ToolingLogTextWriter({ + level: 'debug', + writeTo: { + write: (chunk) => { + // if the current runnable is a beforeEach hook then + // `runner.suite` is set to the suite that defined the + // hook, rather than the suite executing, so instead we + // grab the suite from the test, but that's only available + // when we are doing something test specific, so for global + // hooks we fallback to `runner.suite` + const currentSuite = this.runner.test + ? this.runner.test.parent + : this.runner.suite; + + recordLog(currentSuite, chunk); + } + } + }) + ]); + } + log.write(''); } onHookStart = hook => { - log.write('-> ' + colors.suite(hook.title)); + log.write(`-> ${colors.suite(hook.title)}`); log.indent(2); } @@ -76,7 +108,7 @@ export function MochaReporterProvider({ getService }) { } onSuiteEnd = () => { - if (log.indent(-2) === '') { + if (log.indent(-2) === 0) { log.write(); } } @@ -86,7 +118,8 @@ export function MochaReporterProvider({ getService }) { log.indent(2); } - onTestEnd = () => { + onTestEnd = (test) => { + snapshotLogsForRunnable(test); log.indent(-2); } @@ -96,7 +129,6 @@ export function MochaReporterProvider({ getService }) { } onPass = test => { - let time = ''; if (test.speed !== 'fast') { time = colors.speed(test.speed, ` (${ms(test.duration)})`); @@ -106,7 +138,7 @@ export function MochaReporterProvider({ getService }) { log.write(`- ${pass} ${time}`); } - onFail = test => { + onFail = runnable => { // NOTE: this is super gross // // - I started by trying to extract the Base.list() logic from mocha @@ -118,33 +150,34 @@ export function MochaReporterProvider({ getService }) { const realLog = console.log; console.log = (...args) => output += `${format(...args)}\n`; try { - Mocha.reporters.Base.list([test]); + Mocha.reporters.Base.list([runnable]); } finally { console.log = realLog; } - log.indent(-2); log.write( - `- ${symbols.err} ` + - colors.fail(`fail: "${test.fullTitle()}"`) + + `- ${colors.fail(`${symbols.err} fail: "${runnable.fullTitle()}"`)}` + '\n' + output .split('\n') - .slice(2) // drop the first two lines, (empty + test title) - .map(line => { - // move leading colors behind leading spaces - return line.replace(/^((?:\[.+m)+)(\s+)/, '$2$1'); - }) - .map(line => { - // shrink mocha's indentation - return line.replace(/^\s{5,5}/, ' '); - }) + // drop the first two lines, (empty + test title) + .slice(2) + // move leading colors behind leading spaces + .map(line => line.replace(/^((?:\[.+m)+)(\s+)/, '$2$1')) + .map(line => ` ${line}`) .join('\n') ); - log.indent(2); + + // failed hooks trigger the `onFail(runnable)` callback, so we snapshot the logs for + // them here. Tests will re-capture the snapshot in `onTestEnd()` + snapshotLogsForRunnable(runnable); } onEnd = () => { + if (originalLogWriters) { + log.setWriters(originalLogWriters); + } + writeEpilogue(log, this.stats); } }; diff --git a/src/functional_test_runner/lib/providers/provider_collection.js b/src/functional_test_runner/lib/providers/provider_collection.js index 11a6181c25e844..5826f09502ba09 100644 --- a/src/functional_test_runner/lib/providers/provider_collection.js +++ b/src/functional_test_runner/lib/providers/provider_collection.js @@ -31,6 +31,10 @@ export class ProviderCollection { this._getInstance('Service', name) ) + hasService = name => ( + Boolean(this._findProvider('Service', name)) + ) + getPageObject = name => ( this._getInstance('PageObject', name) ) @@ -68,8 +72,12 @@ export class ProviderCollection { } } + _findProvider(type, name) { + return this._providers.find(p => p.type === type && p.name === name); + } + _getProvider(type, name) { - const providerDef = this._providers.find(p => p.type === type && p.name === name); + const providerDef = this._findProvider(type, name); if (!providerDef) { throw new Error(`Unknown ${type} "${name}"`); } @@ -87,6 +95,7 @@ export class ProviderCollection { if (!instances.has(provider)) { let instance = provider({ getService: this.getService, + hasService: this.hasService, getPageObject: this.getPageObject, getPageObjects: this.getPageObjects, }); diff --git a/src/server/sample_data/routes/uninstall.js b/src/server/sample_data/routes/uninstall.js index d28b04dad61c2e..98b03dad060108 100644 --- a/src/server/sample_data/routes/uninstall.js +++ b/src/server/sample_data/routes/uninstall.js @@ -63,7 +63,7 @@ export const createUninstallRoute = () => ({ } } - reply(); + reply({}); } } }); diff --git a/src/server/sass/build.js b/src/server/sass/build.js index 6aebd9cbb34e5f..fee7891bac0f9c 100644 --- a/src/server/sass/build.js +++ b/src/server/sass/build.js @@ -66,6 +66,10 @@ export class Build { outFile, sourceMap: true, sourceMapEmbed: true, + includePaths: [ + path.resolve(__dirname, '../..'), + path.resolve(__dirname, '../../../node_modules') + ] }); diff --git a/src/server/sass/build.test.js b/src/server/sass/build.test.js index 7e5f55576df8b5..fbc3ede3a355d0 100644 --- a/src/server/sass/build.test.js +++ b/src/server/sass/build.test.js @@ -17,6 +17,7 @@ * under the License. */ +import path from 'path'; import sass from 'node-sass'; import { Build } from './build'; @@ -27,7 +28,7 @@ describe('SASS builder', () => { it('generates a glob', () => { const builder = new Build('/foo/style.sass'); - expect(builder.getGlob()).toEqual('/foo/**/*.s{a,c}ss'); + expect(builder.getGlob()).toEqual(path.join('/foo', '**', '*.s{a,c}ss')); }); it('builds SASS', () => { @@ -35,16 +36,15 @@ describe('SASS builder', () => { const builder = new Build('/foo/style.sass'); builder.build(); - expect(sass.render.mock.calls[0][0]).toEqual({ - file: '/foo/style.sass', - outFile: '/foo/style.css', - sourceMap: true, - sourceMapEmbed: true - }); + const sassCall = sass.render.mock.calls[0][0]; + expect(sassCall.file).toEqual('/foo/style.sass'); + expect(sassCall.outFile).toEqual(path.join('/foo', 'style.css')); + expect(sassCall.sourceMap).toBe(true); + expect(sassCall.sourceMapEmbed).toBe(true); }); it('has an output file with a different extension', () => { const builder = new Build('/foo/style.sass'); - expect(builder.outputPath()).toEqual('/foo/style.css'); + expect(builder.outputPath()).toEqual(path.join('/foo', 'style.css')); }); }); \ No newline at end of file diff --git a/src/server/saved_objects/service/lib/decorate_es_error.js b/src/server/saved_objects/service/lib/decorate_es_error.js index 66d34d95dc1099..0f6190fcca465a 100644 --- a/src/server/saved_objects/service/lib/decorate_es_error.js +++ b/src/server/saved_objects/service/lib/decorate_es_error.js @@ -28,6 +28,7 @@ const { Conflict, 401: NotAuthorized, 403: Forbidden, + 413: RequestEntityTooLarge, NotFound, BadRequest } = elasticsearch.errors; @@ -36,6 +37,7 @@ import { decorateBadRequestError, decorateNotAuthorizedError, decorateForbiddenError, + decorateRequestEntityTooLargeError, createGenericNotFoundError, decorateConflictError, decorateEsUnavailableError, @@ -69,6 +71,10 @@ export function decorateEsError(error) { return decorateForbiddenError(error, reason); } + if (error instanceof RequestEntityTooLarge) { + return decorateRequestEntityTooLargeError(error, reason); + } + if (error instanceof NotFound) { return createGenericNotFoundError(); } diff --git a/src/server/saved_objects/service/lib/decorate_es_error.test.js b/src/server/saved_objects/service/lib/decorate_es_error.test.js index 07e284d0a894a3..8d070e17132027 100644 --- a/src/server/saved_objects/service/lib/decorate_es_error.test.js +++ b/src/server/saved_objects/service/lib/decorate_es_error.test.js @@ -25,6 +25,7 @@ import { isConflictError, isNotAuthorizedError, isForbiddenError, + isRequestEntityTooLargeError, isNotFoundError, isBadRequestError, } from './errors'; @@ -84,6 +85,13 @@ describe('savedObjectsClient/decorateEsError', () => { expect(isForbiddenError(error)).toBe(true); }); + it('makes es.RequestEntityTooLarge a SavedObjectsClient/RequestEntityTooLarge error', () => { + const error = new esErrors.RequestEntityTooLarge(); + expect(isRequestEntityTooLargeError(error)).toBe(false); + expect(decorateEsError(error)).toBe(error); + expect(isRequestEntityTooLargeError(error)).toBe(true); + }); + it('discards es.NotFound errors and returns a generic NotFound error', () => { const error = new esErrors.NotFound(); expect(isNotFoundError(error)).toBe(false); diff --git a/src/server/saved_objects/service/lib/errors.js b/src/server/saved_objects/service/lib/errors.js index dba2f61419a667..d0a4d6ecec796d 100644 --- a/src/server/saved_objects/service/lib/errors.js +++ b/src/server/saved_objects/service/lib/errors.js @@ -71,6 +71,16 @@ export function isForbiddenError(error) { } +// 413 - Request Entity Too Large +const CODE_REQUEST_ENTITY_TOO_LARGE = 'SavedObjectsClient/requestEntityTooLarge'; +export function decorateRequestEntityTooLargeError(error, reason) { + return decorate(error, CODE_REQUEST_ENTITY_TOO_LARGE, 413, reason); +} +export function isRequestEntityTooLargeError(error) { + return error && error[code] === CODE_REQUEST_ENTITY_TOO_LARGE; +} + + // 404 - Not Found const CODE_NOT_FOUND = 'SavedObjectsClient/notFound'; export function createGenericNotFoundError(type = null, id = null) { diff --git a/src/server/status/collectors/get_ops_stats_collector.js b/src/server/status/collectors/get_ops_stats_collector.js index ba4a3d92699ae1..d7e8e154b3bc26 100644 --- a/src/server/status/collectors/get_ops_stats_collector.js +++ b/src/server/status/collectors/get_ops_stats_collector.js @@ -44,6 +44,7 @@ export function getOpsStatsCollector(server, kbnServer) { kibana: getKibanaInfoForStats(server, kbnServer), ...kbnServer.metrics // latest metrics captured from the ops event listener in src/server/status/index }; - } + }, + ignoreForInternalUploader: true, // Ignore this one from internal uploader. A different stats collector is used there. }); } diff --git a/src/server/usage/classes/collector.js b/src/server/usage/classes/collector.js index 7e433068911ea1..f103126740e130 100644 --- a/src/server/usage/classes/collector.js +++ b/src/server/usage/classes/collector.js @@ -25,26 +25,48 @@ export class Collector { * @param {String} options.type - property name as the key for the data * @param {Function} options.init (optional) - initialization function * @param {Function} options.fetch - function to query data + * @param {Function} options.formatForBulkUpload - optional + * @param {Function} options.rest - optional other properties */ - constructor(server, { type, init, fetch } = {}) { + constructor(server, { type, init, fetch, formatForBulkUpload = null, ...options } = {}) { if (type === undefined) { throw new Error('Collector must be instantiated with a options.type string property'); } + if (typeof init !== 'undefined' && typeof init !== 'function') { + throw new Error('If init property is passed, Collector must be instantiated with a options.init as a function property'); + } if (typeof fetch !== 'function') { throw new Error('Collector must be instantiated with a options.fetch function property'); } + this.log = getCollectorLogger(server); + + Object.assign(this, options); // spread in other properties and mutate "this" + this.type = type; this.init = init; this.fetch = fetch; - this.log = getCollectorLogger(server); + const defaultFormatterForBulkUpload = result => ({ type, payload: result }); + this._formatForBulkUpload = formatForBulkUpload || defaultFormatterForBulkUpload; } + /* + * @param {Function} callCluster - callCluster function + */ fetchInternal(callCluster) { if (typeof callCluster !== 'function') { throw new Error('A `callCluster` function must be passed to the fetch methods of collectors'); } return this.fetch(callCluster); } + + /* + * A hook for allowing the fetched data payload to be organized into a typed + * data model for internal bulk upload. See defaultFormatterForBulkUpload for + * a generic example. + */ + formatForBulkUpload(result) { + return this._formatForBulkUpload(result); + } } diff --git a/src/server/usage/classes/collector_set.js b/src/server/usage/classes/collector_set.js index 2b6d3717cfdb9b..8bcb75e39959f8 100644 --- a/src/server/usage/classes/collector_set.js +++ b/src/server/usage/classes/collector_set.js @@ -26,19 +26,17 @@ import { UsageCollector } from './usage_collector'; /* * A collector object has types registered into it with the register(type) * function. Each type that gets registered defines how to fetch its own data - * and combine it into a unified payload for bulk upload. + * and optionally, how to combine it into a unified payload for bulk upload. */ export class CollectorSet { /* * @param {Object} server - server object - * @param {Number} options.interval - in milliseconds - * @param {Function} options.combineTypes - * @param {Function} options.onPayload + * @param {Array} collectors to initialize, usually as a result of filtering another CollectorSet instance */ - constructor(server) { + constructor(server, collectors = []) { this._log = getCollectorLogger(server); - this._collectors = []; + this._collectors = collectors; /* * Helper Factory methods @@ -46,6 +44,7 @@ export class CollectorSet { */ this.makeStatsCollector = options => new Collector(server, options); this.makeUsageCollector = options => new UsageCollector(server, options); + this._makeCollectorSetFromArray = collectorsArray => new CollectorSet(server, collectorsArray); } /* @@ -71,14 +70,14 @@ export class CollectorSet { /* * Call a bunch of fetch methods and then do them in bulk - * @param {Array} collectors - an array of collectors, default to all registered collectors + * @param {CollectorSet} collectorSet - a set of collectors to fetch. Default to all registered collectors */ - bulkFetch(callCluster, collectors = this._collectors) { - if (!Array.isArray(collectors)) { - throw new Error(`bulkFetch method given bad collectors parameter: ` + typeof collectors); + bulkFetch(callCluster, collectorSet = this) { + if (!(collectorSet instanceof CollectorSet)) { + throw new Error(`bulkFetch method given bad collectorSet parameter: ` + typeof collectorSet); } - return Promise.map(collectors, collector => { + const fetchPromises = collectorSet.map(collector => { const collectorType = collector.type; this._log.debug(`Fetching data from ${collectorType} collector`); return Promise.props({ @@ -90,10 +89,19 @@ export class CollectorSet { this._log.warn(`Unable to fetch data from ${collectorType} collector`); }); }); + return Promise.all(fetchPromises); + } + + /* + * @return {new CollectorSet} + */ + getFilteredCollectorSet(filter) { + const filtered = this._collectors.filter(filter); + return this._makeCollectorSetFromArray(filtered); } async bulkFetchUsage(callCluster) { - const usageCollectors = this._collectors.filter(c => c instanceof UsageCollector); + const usageCollectors = this.getFilteredCollectorSet(c => c instanceof UsageCollector); return this.bulkFetch(callCluster, usageCollectors); } @@ -137,4 +145,8 @@ export class CollectorSet { }; }, {}); } + + map(mapFn) { + return this._collectors.map(mapFn); + } } diff --git a/src/server/usage/classes/usage_collector.js b/src/server/usage/classes/usage_collector.js index d8ddd8a2552521..559deaef2ce15c 100644 --- a/src/server/usage/classes/usage_collector.js +++ b/src/server/usage/classes/usage_collector.js @@ -17,6 +17,35 @@ * under the License. */ +import { KIBANA_STATS_TYPE } from '../../status/constants'; import { Collector } from './collector'; -export class UsageCollector extends Collector {} +export class UsageCollector extends Collector { + /* + * @param {Object} server - server object + * @param {String} options.type - property name as the key for the data + * @param {Function} options.init (optional) - initialization function + * @param {Function} options.fetch - function to query data + * @param {Function} options.formatForBulkUpload - optional + * @param {Function} options.rest - optional other properties + */ + constructor(server, { type, init, fetch, formatForBulkUpload = null, ...options } = {}) { + super(server, { type, init, fetch, formatForBulkUpload, ...options }); + + /* + * Currently, for internal bulk uploading, usage stats are part of + * `kibana_stats` type, under the `usage` namespace in the document. + */ + const defaultUsageFormatterForBulkUpload = result => { + return { + type: KIBANA_STATS_TYPE, + payload: { + usage: { + [type]: result + } + } + }; + }; + this._formatForBulkUpload = formatForBulkUpload || defaultUsageFormatterForBulkUpload; + } +} diff --git a/src/ui/public/agg_response/hierarchical/__tests__/build_hierarchical_data.js b/src/ui/public/agg_response/hierarchical/__tests__/build_hierarchical_data.js index 293032e0f6cc7b..f0b5f8f2bb533a 100644 --- a/src/ui/public/agg_response/hierarchical/__tests__/build_hierarchical_data.js +++ b/src/ui/public/agg_response/hierarchical/__tests__/build_hierarchical_data.js @@ -23,6 +23,7 @@ import fixtures from 'fixtures/fake_hierarchical_data'; import sinon from 'sinon'; import expect from 'expect.js'; import ngMock from 'ng_mock'; +import { toastNotifications } from 'ui/notify'; import { VisProvider } from '../../../vis'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; import { BuildHierarchicalDataProvider } from '../build_hierarchical_data'; @@ -276,6 +277,9 @@ describe('buildHierarchicalData', function () { let results; beforeEach(function () { + // Clear existing toasts. + toastNotifications.list.splice(0); + let id = 1; vis = new Vis(indexPattern, { type: 'pie', @@ -299,10 +303,11 @@ describe('buildHierarchicalData', function () { }); it('should set the hits attribute for the results', function () { - const errCall = Notifier.prototype.error.getCall(0); - expect(errCall).to.be.ok(); - expect(errCall.args[0]).to.contain('not supported'); - + // Ideally, buildHierarchicalData shouldn't be tightly coupled to toastNotifications. Instead, + // it should notify its consumer of this error and the consumer should be responsible for + // notifying the user. This test verifies the side effect of the error until we can remove + // this coupling. + expect(toastNotifications.list).to.have.length(1); expect(results).to.have.property('slices'); expect(results).to.have.property('names'); expect(results.names).to.have.length(2); diff --git a/src/ui/public/agg_response/hierarchical/build_hierarchical_data.js b/src/ui/public/agg_response/hierarchical/build_hierarchical_data.js index d0b98a7ef01ba9..fc7734a13d8526 100644 --- a/src/ui/public/agg_response/hierarchical/build_hierarchical_data.js +++ b/src/ui/public/agg_response/hierarchical/build_hierarchical_data.js @@ -18,6 +18,7 @@ */ import _ from 'lodash'; +import { toastNotifications } from 'ui/notify'; import { extractBuckets } from './_extract_buckets'; import { createRawData } from './_create_raw_data'; import { arrayToLinkedList } from './_array_to_linked_list'; @@ -25,15 +26,10 @@ import AggConfigResult from '../../vis/agg_config_result'; import { AggResponseHierarchicalBuildSplitProvider } from './_build_split'; import { HierarchicalTooltipFormatterProvider } from './_hierarchical_tooltip_formatter'; -export function BuildHierarchicalDataProvider(Private, Notifier) { +export function BuildHierarchicalDataProvider(Private) { const buildSplit = Private(AggResponseHierarchicalBuildSplitProvider); const tooltipFormatter = Private(HierarchicalTooltipFormatterProvider); - - const notify = new Notifier({ - location: 'Pie chart response converter' - }); - return function (vis, resp) { // Create a reference to the buckets let buckets = vis.getAggConfig().bySchemaGroup.buckets; @@ -73,7 +69,10 @@ export function BuildHierarchicalDataProvider(Private, Notifier) { const aggData = resp.aggregations ? resp.aggregations[firstAgg.id] : null; if (!firstAgg._next && firstAgg.schema.name === 'split') { - notify.error('Splitting charts without splitting slices is not supported. Pretending that we are just splitting slices.'); + toastNotifications.addDanger({ + title: 'Splitting charts without splitting slices is not supported', + text: 'Pretending that we are just splitting slices.' + }); } // start with splitting slices diff --git a/src/ui/public/agg_types/buckets/date_histogram.js b/src/ui/public/agg_types/buckets/date_histogram.js index c9c26689f3d539..fe93fe41f28e9d 100644 --- a/src/ui/public/agg_types/buckets/date_histogram.js +++ b/src/ui/public/agg_types/buckets/date_histogram.js @@ -42,7 +42,7 @@ function getInterval(agg) { } function getBounds(vis) { - if (vis.API.timeFilter.isTimeRangeSelectorEnabled && vis.filters && vis.filters.timeRange) { + if (vis.filters && vis.filters.timeRange) { return vis.API.timeFilter.calculateBounds(vis.filters.timeRange); } } diff --git a/src/ui/public/autoload/styles.js b/src/ui/public/autoload/styles.js index df9e05e6d71c52..c919bd33cda1e8 100644 --- a/src/ui/public/autoload/styles.js +++ b/src/ui/public/autoload/styles.js @@ -36,3 +36,6 @@ theme.applyTheme('light'); // All Kibana styles inside of the /styles dir const context = require.context('../styles', false, /[\/\\](?!mixins|variables|_|\.)[^\/\\]+\.less/); context.keys().forEach(key => context(key)); + +// manually require non-less files +require('../styles/disable_animations'); diff --git a/src/ui/public/chrome/directives/__snapshots__/loading_indicator.test.js.snap b/src/ui/public/chrome/directives/__snapshots__/loading_indicator.test.js.snap index 301e34bb420504..b9d9391a05d80e 100644 --- a/src/ui/public/chrome/directives/__snapshots__/loading_indicator.test.js.snap +++ b/src/ui/public/chrome/directives/__snapshots__/loading_indicator.test.js.snap @@ -6,7 +6,7 @@ exports[`kbnLoadingIndicator is hidden by default 1`] = ` data-test-subj="globalLoadingIndicator-hidden" >
`; @@ -17,7 +17,7 @@ exports[`kbnLoadingIndicator is visible when loadingCount is > 0 1`] = ` data-test-subj="globalLoadingIndicator" >
`; diff --git a/src/ui/public/chrome/directives/loading_indicator.js b/src/ui/public/chrome/directives/loading_indicator.js index f8f6c671683f58..dee04bc2d51c37 100644 --- a/src/ui/public/chrome/directives/loading_indicator.js +++ b/src/ui/public/chrome/directives/loading_indicator.js @@ -56,7 +56,7 @@ export class LoadingIndicator extends React.Component { return (
-
+
); } diff --git a/src/ui/public/chrome/index.d.ts b/src/ui/public/chrome/index.d.ts index 5363e83631f244..caadd5f09828bb 100644 --- a/src/ui/public/chrome/index.d.ts +++ b/src/ui/public/chrome/index.d.ts @@ -26,6 +26,7 @@ declare class Chrome { public dangerouslyGetActiveInjector(): Promise; public getBasePath(): string; public getXsrfToken(): string; + public getKibanaVersion(): string; } declare const chrome: Chrome; diff --git a/src/ui/public/inspector/ui/__snapshots__/inspector_panel.test.js.snap b/src/ui/public/inspector/ui/__snapshots__/inspector_panel.test.js.snap index 8fff7e5caf3b8a..c0174670aa09cb 100644 --- a/src/ui/public/inspector/ui/__snapshots__/inspector_panel.test.js.snap +++ b/src/ui/public/inspector/ui/__snapshots__/inspector_panel.test.js.snap @@ -125,6 +125,7 @@ exports[`InspectorPanel should render as expected 1`] = ` } closePopover={[Function]} + hasArrow={true} id="inspectorViewChooser" isOpen={false} ownFocus={true} diff --git a/src/ui/public/management/index.js b/src/ui/public/management/index.js index 56793da6832b8e..616600f1ac1dec 100644 --- a/src/ui/public/management/index.js +++ b/src/ui/public/management/index.js @@ -19,6 +19,8 @@ import { ManagementSection } from './section'; +export { registerSettingsComponent } from '../../../core_plugins/kibana/public/management/sections/settings/components/component_registry'; + export const management = new ManagementSection('management', { display: 'Management' }); diff --git a/src/ui/public/scripting_languages/index.js b/src/ui/public/scripting_languages/index.js index f2098c30647855..19fb82e00db5e1 100644 --- a/src/ui/public/scripting_languages/index.js +++ b/src/ui/public/scripting_languages/index.js @@ -18,9 +18,7 @@ */ import chrome from '../chrome'; -import { Notifier } from '../notify'; - -const notify = new Notifier({ location: 'Scripting Language Service' }); +import { toastNotifications } from '../notify'; export function getSupportedScriptingLanguages() { return ['painless']; @@ -35,7 +33,7 @@ export function GetEnabledScriptingLanguagesProvider($http) { return $http.get(chrome.addBasePath('/api/kibana/scripts/languages')) .then((res) => res.data) .catch(() => { - notify.error('Error getting available scripting languages from Elasticsearch'); + toastNotifications.addDanger('Error getting available scripting languages from Elasticsearch'); return []; }); }; diff --git a/src/ui/public/styles/_styling_constants.scss b/src/ui/public/styles/_styling_constants.scss index b86af2288e268b..af595341fde327 100644 --- a/src/ui/public/styles/_styling_constants.scss +++ b/src/ui/public/styles/_styling_constants.scss @@ -1,13 +1,21 @@ // EUI global scope -@import '../../../../node_modules/@elastic/eui/src/themes/k6/k6_globals'; -@import '../../../../node_modules/@elastic/eui/src/themes/k6/k6_colors_light'; -@import '../../../../node_modules/@elastic/eui/src/global_styling/functions/index'; -@import '../../../../node_modules/@elastic/eui/src/global_styling/variables/index'; -@import '../../../../node_modules/@elastic/eui/src/global_styling/mixins/index'; +@import '@elastic/eui/src/themes/k6/k6_globals'; +@import '@elastic/eui/src/themes/k6/k6_colors_light'; +@import '@elastic/eui/src/global_styling/functions/index'; +@import '@elastic/eui/src/global_styling/variables/index'; +@import '@elastic/eui/src/global_styling/mixins/index'; // EUI TODO: Add this -@mixin truncate($style: ellipsis) { - white-space: nowrap; - text-overflow: $style; - overflow: hidden; +@mixin kibanaCircleLogo() { + display: inline-block; + @include size($euiSizeXXL * 2); + line-height: $euiSizeXXL * 2; + text-align: center; + background-color: $euiColorEmptyShade; + border-radius: 100%; + padding: $euiSize; + + .euiIcon { + vertical-align: baseline; + } } diff --git a/src/ui/public/styles/disable_animations/disable_animations.css b/src/ui/public/styles/disable_animations/disable_animations.css new file mode 100644 index 00000000000000..9cf9d9eb4e5f22 --- /dev/null +++ b/src/ui/public/styles/disable_animations/disable_animations.css @@ -0,0 +1,14 @@ +*:not(.essentialAnimation), +*:not(.essentialAnimation):before, +*:not(.essentialAnimation):after { + /** + * set the animation/transition duration to 0s so that animation callbacks are + * still triggered, allowing components that require them to remain functional + */ + + -webkit-animation-duration: 0s !important; + animation-duration: 0s !important; + + -webkit-transition-duration: 0s !important; + transition-duration: 0s !important; +} diff --git a/src/ui/public/styles/disable_animations/disable_animations.js b/src/ui/public/styles/disable_animations/disable_animations.js new file mode 100644 index 00000000000000..067bbd040df33e --- /dev/null +++ b/src/ui/public/styles/disable_animations/disable_animations.js @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import chrome from 'ui/chrome'; +import disableAnimationsCss from '!!raw-loader!./disable_animations.css'; + +const uiSettings = chrome.getUiSettingsClient(); + +// rather than silently ignore when the style element is missing in the tests +// like ui/theme does, we automatically create a style tag because ordering doesn't +// really matter for these styles, they should really take top priority because +// they all use `!important`, and we don't want to silently ignore the possibility +// of accidentally removing the style element from the chrome template. +const styleElement = document.createElement('style'); +styleElement.setAttribute('id', 'disableAnimationsCss'); +document.head.appendChild(styleElement); + +function updateStyleSheet() { + styleElement.textContent = uiSettings.get('accessibility:disableAnimations') + ? disableAnimationsCss + : ''; +} + +updateStyleSheet(); +uiSettings.subscribe(({ key }) => { + if (key === 'accessibility:disableAnimations') { + updateStyleSheet(); + } +}); + diff --git a/src/ui/public/styles/disable_animations/index.js b/src/ui/public/styles/disable_animations/index.js new file mode 100644 index 00000000000000..2c459667d59969 --- /dev/null +++ b/src/ui/public/styles/disable_animations/index.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. + */ + +import './disable_animations'; diff --git a/src/ui/public/tooltip/tooltip.js b/src/ui/public/tooltip/tooltip.js index d53070fd6a6eaa..7ae8c54375040a 100644 --- a/src/ui/public/tooltip/tooltip.js +++ b/src/ui/public/tooltip/tooltip.js @@ -18,12 +18,16 @@ */ import html from './tooltip.html'; +import chrome from 'ui/chrome'; require('ui-bootstrap') .config(function ($tooltipProvider) { + // we use the uiSettings client because the config service is not available in the config phase + const uiSettings = chrome.getUiSettingsClient(); + $tooltipProvider.options({ placement: 'bottom', - animation: true, + animation: !uiSettings.get('accessibility:disableAnimations'), popupDelay: 150, appendToBody: false }); diff --git a/src/ui/public/vis/editors/default/agg_group.html b/src/ui/public/vis/editors/default/agg_group.html index dfa04e4de9dc55..965390fd86091f 100644 --- a/src/ui/public/vis/editors/default/agg_group.html +++ b/src/ui/public/vis/editors/default/agg_group.html @@ -10,6 +10,6 @@
- +
diff --git a/src/ui/public/vis/editors/default/sidebar.html b/src/ui/public/vis/editors/default/sidebar.html index 6b04bb29819762..511a21ca0aa91d 100644 --- a/src/ui/public/vis/editors/default/sidebar.html +++ b/src/ui/public/vis/editors/default/sidebar.html @@ -139,10 +139,10 @@
- + - +
diff --git a/src/ui/public/vislib/lib/axis/axis.js b/src/ui/public/vislib/lib/axis/axis.js index a2e61a1803c6b4..87d98aae37ac0b 100644 --- a/src/ui/public/vislib/lib/axis/axis.js +++ b/src/ui/public/vislib/lib/axis/axis.js @@ -209,6 +209,7 @@ export function VislibLibAxisProvider(Private) { if (config.get('show')) { const svg = div.append('svg') + .attr('focusable', 'false') .attr('width', width) .attr('height', height); diff --git a/src/ui/public/vislib/lib/axis/axis_title.js b/src/ui/public/vislib/lib/axis/axis_title.js index e66f2c5f89a3d7..eaa0a22eb8fbec 100644 --- a/src/ui/public/vislib/lib/axis/axis_title.js +++ b/src/ui/public/vislib/lib/axis/axis_title.js @@ -50,6 +50,7 @@ export function VislibLibAxisTitleProvider() { const axisPrefix = config.isHorizontal() ? 'x' : 'y'; const svg = div.append('svg') + .attr('focusable', 'false') .attr('width', width) .attr('height', height) .attr('class', `axis-title ${axisPrefix}-axis-title`); diff --git a/src/ui/public/vislib/lib/chart_title.js b/src/ui/public/vislib/lib/chart_title.js index df28428dcd03e2..0f8ade993ccb4d 100644 --- a/src/ui/public/vislib/lib/chart_title.js +++ b/src/ui/public/vislib/lib/chart_title.js @@ -90,6 +90,7 @@ export function VislibLibChartTitleProvider(Private) { self.validateWidthandHeight(width, height); div.append('svg') + .attr('focusable', 'false') .attr('width', width) .attr('height', height) .append('text') diff --git a/src/ui/public/vislib/visualizations/gauge_chart.js b/src/ui/public/vislib/visualizations/gauge_chart.js index 113f5e21751fdb..c8a02b6a6fa204 100644 --- a/src/ui/public/vislib/visualizations/gauge_chart.js +++ b/src/ui/public/vislib/visualizations/gauge_chart.js @@ -66,6 +66,7 @@ export function GaugeChartProvider(Private) { const svg = div.append('svg') .style('display', 'inline-block') .style('overflow', 'hidden') + .attr('focusable', 'false') .attr('width', width); const g = svg.append('g'); diff --git a/src/ui/public/vislib/visualizations/pie_chart.js b/src/ui/public/vislib/visualizations/pie_chart.js index 0889bd820dc597..8f648f4cc4ce6c 100644 --- a/src/ui/public/vislib/visualizations/pie_chart.js +++ b/src/ui/public/vislib/visualizations/pie_chart.js @@ -370,6 +370,7 @@ export function VislibVisualizationsPieChartProvider(Private) { const svg = div.append('svg') .attr('width', width) .attr('height', height) + .attr('focusable', 'false') .append('g') .attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')'); diff --git a/src/ui/public/vislib/visualizations/point_series.js b/src/ui/public/vislib/visualizations/point_series.js index 93bd4bae5dc4f3..98e59d33da4ca6 100644 --- a/src/ui/public/vislib/visualizations/point_series.js +++ b/src/ui/public/vislib/visualizations/point_series.js @@ -235,6 +235,7 @@ export function VislibVisualizationsPointSeriesProvider(Private) { div = d3.select(el); svg = div.append('svg') + .attr('focusable', 'false') .attr('width', width) .attr('height', height); diff --git a/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_integration.js b/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_integration.js index 2fb7a325d61c1a..f83c4ba8805d6c 100644 --- a/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_integration.js +++ b/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_integration.js @@ -22,7 +22,7 @@ import expect from 'expect.js'; import { createEsTestCluster } from '@kbn/test'; import { createServerWithCorePlugins } from '../../../../test_utils/kbn_server'; -import { createToolingLog } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; import { createOrUpgradeSavedConfig } from '../create_or_upgrade_saved_config'; describe('createOrUpgradeSavedConfig()', () => { @@ -31,8 +31,10 @@ describe('createOrUpgradeSavedConfig()', () => { const cleanup = []; before(async function () { - const log = createToolingLog('debug'); - log.pipe(process.stdout); + const log = new ToolingLog({ + level: 'debug', + writeTo: process.stdout + }); log.indent(6); log.info('starting elasticsearch'); diff --git a/src/ui/ui_settings/routes/__tests__/lib/servers.js b/src/ui/ui_settings/routes/__tests__/lib/servers.js index 825c5c85ca7d51..f27afb54c808f7 100644 --- a/src/ui/ui_settings/routes/__tests__/lib/servers.js +++ b/src/ui/ui_settings/routes/__tests__/lib/servers.js @@ -18,7 +18,7 @@ */ import { createEsTestCluster } from '@kbn/test'; -import { createToolingLog } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; import * as kbnTestServer from '../../../../../test_utils/kbn_server'; let kbnServer; @@ -26,8 +26,10 @@ let services; let es; export async function startServers() { - const log = createToolingLog('debug'); - log.pipe(process.stdout); + const log = new ToolingLog({ + level: 'debug', + writeTo: process.stdout + }); log.indent(6); log.info('starting elasticsearch'); diff --git a/src/utils/package_json.js b/src/utils/package_json.ts similarity index 93% rename from src/utils/package_json.js rename to src/utils/package_json.ts index e8130345a6f9da..edacb04c6b1daf 100644 --- a/src/utils/package_json.js +++ b/src/utils/package_json.ts @@ -22,5 +22,6 @@ import { dirname } from 'path'; export const pkg = { __filename: require.resolve('../../package.json'), __dirname: dirname(require.resolve('../../package.json')), - ...require('../../package.json') + // tslint:disable no-var-requires + ...require('../../package.json'), }; diff --git a/tasks/config/run.js b/tasks/config/run.js index 1e5f8c114b5524..8facd9e0822fdb 100644 --- a/tasks/config/run.js +++ b/tasks/config/run.js @@ -175,14 +175,17 @@ module.exports = function (grunt) { ], }, - panelActionTests: { + pluginFunctionalTestsRelease: { cmd: process.execPath, args: [ 'scripts/functional_tests', - '--config', 'test/panel_actions/config.js', + '--config', 'test/plugin_functional/config.js', '--esFrom', 'source', '--bail', '--debug', + '--kibana-install-dir', `./build/oss/kibana-${PKG_VERSION}-${process.platform}-x86_64`, + '--', + '--server.maxPayloadBytes=1648576', ], }, diff --git a/tasks/functional_test_runner.js b/tasks/functional_test_runner.js index 1c72b50d849f87..91c49e090ed587 100644 --- a/tasks/functional_test_runner.js +++ b/tasks/functional_test_runner.js @@ -18,7 +18,7 @@ */ import { createFunctionalTestRunner } from '../src/functional_test_runner'; -import { createToolingLog } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; export default function (grunt) { grunt.registerMultiTask('functional_test_runner', 'run tests with the functional test runner', function () { @@ -28,8 +28,10 @@ export default function (grunt) { configOverrides } = this.options(); - const log = createToolingLog(logLevel); - log.pipe(process.stdout); + const log = new ToolingLog({ + level: logLevel, + writeTo: process.stdout, + }); const functionalTestRunner = createFunctionalTestRunner({ log, diff --git a/tasks/jenkins.js b/tasks/jenkins.js index ce8d5f178ea3cc..a68f81f34020dd 100644 --- a/tasks/jenkins.js +++ b/tasks/jenkins.js @@ -42,5 +42,6 @@ module.exports = function (grunt) { grunt.registerTask('jenkins:selenium', [ 'checkPlugins', 'run:functionalTestsRelease', + 'run:pluginFunctionalTestsRelease', ]); }; diff --git a/test/common/services/es_archiver.js b/test/common/services/es_archiver.js index 8dd796e70bd4f4..57b0f5664d022f 100644 --- a/test/common/services/es_archiver.js +++ b/test/common/services/es_archiver.js @@ -18,8 +18,9 @@ */ import { EsArchiver } from '../../../src/es_archiver'; +import * as KibanaServer from './kibana_server'; -export async function EsArchiverProvider({ getService }) { +export function EsArchiverProvider({ getService, hasService }) { const config = getService('config'); const client = getService('es'); const log = getService('log'); @@ -30,9 +31,19 @@ export async function EsArchiverProvider({ getService }) { const dataDir = config.get('esArchiver.directory'); - return new EsArchiver({ + const esArchiver = new EsArchiver({ client, dataDir, log, }); + + if (hasService('kibanaServer')) { + KibanaServer.extendEsArchiver({ + esArchiver, + kibanaServer: getService('kibanaServer'), + defaults: config.get('uiSettings.defaults'), + }); + } + + return esArchiver; } diff --git a/test/common/services/kibana_server/extend_es_archiver.js b/test/common/services/kibana_server/extend_es_archiver.js new file mode 100644 index 00000000000000..1c804c630b0269 --- /dev/null +++ b/test/common/services/kibana_server/extend_es_archiver.js @@ -0,0 +1,45 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const ES_ARCHIVER_LOAD_METHODS = ['load', 'loadIfNeeded']; +const KIBANA_INDEX = '.kibana'; + +export function extendEsArchiver({ esArchiver, kibanaServer, defaults }) { + // only extend the esArchiver if there are default uiSettings to restore + if (!defaults) { + return; + } + + ES_ARCHIVER_LOAD_METHODS.forEach(method => { + const originalMethod = esArchiver[method]; + + esArchiver[method] = async (...args) => { + // esArchiver methods return a stats object, with information about the indexes created + const stats = await originalMethod.apply(esArchiver, args); + + // if the kibana index was created by the esArchiver then update the uiSettings + // with the defaults to make sure that they are always in place initially + if (stats[KIBANA_INDEX] && stats[KIBANA_INDEX].created) { + await kibanaServer.uiSettings.update(defaults); + } + + return stats; + }; + }); +} diff --git a/test/common/services/kibana_server/index.js b/test/common/services/kibana_server/index.js index 70b4e934e30dc1..6d9035008c6b71 100644 --- a/test/common/services/kibana_server/index.js +++ b/test/common/services/kibana_server/index.js @@ -18,3 +18,4 @@ */ export { KibanaServerProvider } from './kibana_server'; +export { extendEsArchiver } from './extend_es_archiver'; diff --git a/test/common/services/kibana_server/kibana_server.js b/test/common/services/kibana_server/kibana_server.js index 5f413fe0b58d1c..d5125d8eba256f 100644 --- a/test/common/services/kibana_server/kibana_server.js +++ b/test/common/services/kibana_server/kibana_server.js @@ -23,7 +23,7 @@ import { KibanaServerStatus } from './status'; import { KibanaServerUiSettings } from './ui_settings'; import { KibanaServerVersion } from './version'; -export async function KibanaServerProvider({ getService }) { +export function KibanaServerProvider({ getService }) { const log = getService('log'); const config = getService('config'); @@ -32,7 +32,7 @@ export async function KibanaServerProvider({ getService }) { const url = formatUrl(config.get('servers.kibana')); this.status = new KibanaServerStatus(url); this.version = new KibanaServerVersion(this.status); - this.uiSettings = new KibanaServerUiSettings(url, log, this.version); + this.uiSettings = new KibanaServerUiSettings(url, log, config.get('uiSettings.defaults')); } }; } diff --git a/test/common/services/kibana_server/ui_settings.js b/test/common/services/kibana_server/ui_settings.js index ff318fa5eeb160..ac7987bfb3717f 100644 --- a/test/common/services/kibana_server/ui_settings.js +++ b/test/common/services/kibana_server/ui_settings.js @@ -24,9 +24,9 @@ const MINUTE = 60 * 1000; const HOUR = 60 * MINUTE; export class KibanaServerUiSettings { - constructor(url, log, kibanaVersion) { + constructor(url, log, defaults) { this._log = log; - this._kibanaVersion = kibanaVersion; + this._defaults = defaults; this._wreck = Wreck.defaults({ headers: { 'kbn-xsrf': 'ftr/services/uiSettings' }, baseUrl: url, @@ -35,9 +35,9 @@ export class KibanaServerUiSettings { }); } - /* - ** Gets defaultIndex from the config doc. - */ + /** + * Gets defaultIndex from the config doc. + */ async getDefaultIndex() { const { payload } = await this._wreck.get('/api/kibana/settings'); const defaultIndex = get(payload, 'settings.defaultIndex.userValue'); @@ -75,15 +75,18 @@ export class KibanaServerUiSettings { await this._wreck.post('/api/kibana/settings', { payload: { - changes: doc + changes: { + ...this._defaults, + ...doc, + } } }); } /** - * Add fields to the config doc (like setting timezone and defaultIndex) - * @return {Promise} A promise that is resolved when elasticsearch has a response - */ + * Add fields to the config doc (like setting timezone and defaultIndex) + * @return {Promise} A promise that is resolved when elasticsearch has a response + */ async update(updates) { this._log.debug('applying update to kibana config: %j', updates); await this._wreck.post('/api/kibana/settings', { diff --git a/test/functional/apps/dashboard/_create_and_add_embeddables.js b/test/functional/apps/dashboard/_create_and_add_embeddables.js index 6a6b4a539a39c1..007050c6d3b32e 100644 --- a/test/functional/apps/dashboard/_create_and_add_embeddables.js +++ b/test/functional/apps/dashboard/_create_and_add_embeddables.js @@ -37,7 +37,7 @@ export default function ({ getService, getPageObjects }) { describe('add new visualization link', () => { it('adds a new visualization', async () => { const originalPanelCount = await PageObjects.dashboard.getPanelCount(); - await PageObjects.dashboard.clickEdit(); + await PageObjects.dashboard.switchToEditMode(); await dashboardAddPanel.ensureAddPanelIsShowing(); await dashboardAddPanel.clickAddNewEmbeddableLink(); await PageObjects.visualize.clickAreaChart(); diff --git a/test/functional/apps/dashboard/_dashboard_grid.js b/test/functional/apps/dashboard/_dashboard_grid.js index 933d40bc390fcc..2840424e7f0172 100644 --- a/test/functional/apps/dashboard/_dashboard_grid.js +++ b/test/functional/apps/dashboard/_dashboard_grid.js @@ -28,7 +28,7 @@ export default function ({ getService, getPageObjects }) { before(async () => { await PageObjects.dashboard.gotoDashboardLandingPage(); await PageObjects.dashboard.loadSavedDashboard('few panels'); - await PageObjects.dashboard.clickEdit(); + await PageObjects.dashboard.switchToEditMode(); }); describe('move panel', () => { diff --git a/test/functional/apps/dashboard/_dashboard_options.js b/test/functional/apps/dashboard/_dashboard_options.js index c3e4587feb8b32..02dc1e7a4af7ba 100644 --- a/test/functional/apps/dashboard/_dashboard_options.js +++ b/test/functional/apps/dashboard/_dashboard_options.js @@ -28,7 +28,7 @@ export default function ({ getService, getPageObjects }) { before(async () => { await PageObjects.dashboard.loadSavedDashboard('few panels'); - await PageObjects.dashboard.clickEdit(); + await PageObjects.dashboard.switchToEditMode(); originalTitles = await PageObjects.dashboard.getPanelTitles(); }); diff --git a/test/functional/apps/dashboard/_dashboard_save.js b/test/functional/apps/dashboard/_dashboard_save.js index 7549457fc1f6c8..53dc69fd145c70 100644 --- a/test/functional/apps/dashboard/_dashboard_save.js +++ b/test/functional/apps/dashboard/_dashboard_save.js @@ -76,7 +76,7 @@ export default function ({ getService, getPageObjects }) { async function () { await PageObjects.dashboard.selectDashboard(dashboardName); await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); - await PageObjects.dashboard.clickEdit(); + await PageObjects.dashboard.switchToEditMode(); await PageObjects.dashboard.saveDashboard(dashboardName); const isWarningDisplayed = await PageObjects.dashboard.isDuplicateTitleWarningDisplayed(); @@ -85,7 +85,7 @@ export default function ({ getService, getPageObjects }) { ); it('Warns you when you Save as New Dashboard, and the title is a duplicate', async function () { - await PageObjects.dashboard.clickEdit(); + await PageObjects.dashboard.switchToEditMode(); await PageObjects.dashboard.enterDashboardTitleAndClickSave(dashboardName, { saveAsNew: true }); const isWarningDisplayed = await PageObjects.dashboard.isDuplicateTitleWarningDisplayed(); @@ -102,7 +102,7 @@ export default function ({ getService, getPageObjects }) { }); it('Warns when case is different', async function () { - await PageObjects.dashboard.clickEdit(); + await PageObjects.dashboard.switchToEditMode(); await PageObjects.dashboard.enterDashboardTitleAndClickSave(dashboardName.toUpperCase()); // We expect isWarningDisplayed to be open, hence the retry if not found. diff --git a/test/functional/apps/dashboard/_dashboard_state.js b/test/functional/apps/dashboard/_dashboard_state.js index ea7439c1dbd829..1c9054111074e7 100644 --- a/test/functional/apps/dashboard/_dashboard_state.js +++ b/test/functional/apps/dashboard/_dashboard_state.js @@ -52,7 +52,7 @@ export default function ({ getService, getPageObjects }) { await dashboardAddPanel.addVisualization(AREA_CHART_VIS_NAME); await PageObjects.dashboard.saveDashboard('Overridden colors'); - await PageObjects.dashboard.clickEdit(); + await PageObjects.dashboard.switchToEditMode(); await PageObjects.visualize.openLegendOptionColors('Count'); await PageObjects.visualize.selectNewLegendColorChoice('#EA6460'); @@ -100,7 +100,7 @@ export default function ({ getService, getPageObjects }) { it('Saved search with column changes will not update when the saved object changes', async () => { await PageObjects.discover.removeHeaderColumn('bytes'); - await PageObjects.dashboard.clickEdit(); + await PageObjects.dashboard.switchToEditMode(); await PageObjects.dashboard.saveDashboard('Has local edits'); await PageObjects.header.clickDiscover(); @@ -129,7 +129,7 @@ export default function ({ getService, getPageObjects }) { const tileMapData = await PageObjects.visualize.getInspectorTableData(); await PageObjects.visualize.closeInspector(); - await PageObjects.dashboard.clickEdit(); + await PageObjects.dashboard.switchToEditMode(); await dashboardPanelActions.clickEdit(); await PageObjects.visualize.clickMapZoomIn(); diff --git a/test/functional/apps/dashboard/_dashboard_time.js b/test/functional/apps/dashboard/_dashboard_time.js index 285da16bfb443d..5e6ff78fb2079a 100644 --- a/test/functional/apps/dashboard/_dashboard_time.js +++ b/test/functional/apps/dashboard/_dashboard_time.js @@ -59,7 +59,7 @@ export default function ({ getPageObjects, getService }) { describe('dashboard with stored timed', async function () { it('is saved with quick time', async function () { - await PageObjects.dashboard.clickEdit(); + await PageObjects.dashboard.switchToEditMode(); await PageObjects.header.setQuickTime('Today'); await PageObjects.dashboard.saveDashboard(dashboardName, { storeTimeWithDashboard: true }); }); @@ -74,7 +74,7 @@ export default function ({ getPageObjects, getService }) { }); it('is saved with absolute time', async function () { - await PageObjects.dashboard.clickEdit(); + await PageObjects.dashboard.switchToEditMode(); await PageObjects.header.setAbsoluteRange(fromTime, toTime); await PageObjects.dashboard.saveDashboard(dashboardName, { storeTimeWithDashboard: true }); }); diff --git a/test/functional/apps/dashboard/_data_shared_attributes.js b/test/functional/apps/dashboard/_data_shared_attributes.js index 0a50ed3db398ac..27ac2184071f85 100644 --- a/test/functional/apps/dashboard/_data_shared_attributes.js +++ b/test/functional/apps/dashboard/_data_shared_attributes.js @@ -55,7 +55,7 @@ export default function ({ getService, getPageObjects }) { }); it('data-shared-item title should update a viz when using a custom panel title', async () => { - await PageObjects.dashboard.clickEdit(); + await PageObjects.dashboard.switchToEditMode(); const CUSTOM_VIS_TITLE = 'ima custom title for a vis!'; await dashboardPanelActions.setCustomPanelTitle(CUSTOM_VIS_TITLE); await retry.try(async () => { diff --git a/test/functional/apps/dashboard/_full_screen_mode.js b/test/functional/apps/dashboard/_full_screen_mode.js index 35af03ae05ac3e..94ff8323f14dee 100644 --- a/test/functional/apps/dashboard/_full_screen_mode.js +++ b/test/functional/apps/dashboard/_full_screen_mode.js @@ -31,7 +31,7 @@ export default function ({ getService, getPageObjects }) { }); it('option not available in edit mode', async () => { - await PageObjects.dashboard.clickEdit(); + await PageObjects.dashboard.switchToEditMode(); const exists = await PageObjects.dashboard.fullScreenModeMenuItemExists(); expect(exists).to.be(false); }); diff --git a/test/functional/apps/dashboard/_panel_controls.js b/test/functional/apps/dashboard/_panel_controls.js index 158efae00e0ccf..20c781e0ac3829 100644 --- a/test/functional/apps/dashboard/_panel_controls.js +++ b/test/functional/apps/dashboard/_panel_controls.js @@ -69,7 +69,7 @@ export default function ({ getService, getPageObjects }) { }); it('are shown in edit mode', async function () { - await PageObjects.dashboard.clickEdit(); + await PageObjects.dashboard.switchToEditMode(); const isContextMenuIconVisible = await dashboardPanelActions.isContextMenuIconVisible(); expect(isContextMenuIconVisible).to.equal(true); @@ -115,7 +115,7 @@ export default function ({ getService, getPageObjects }) { }); it('in edit mode hides remove icons ', async function () { - await PageObjects.dashboard.clickEdit(); + await PageObjects.dashboard.switchToEditMode(); await dashboardPanelActions.openContextMenu(); const editLinkExists = await dashboardPanelActions.editPanelActionExists(); const removeExists = await dashboardPanelActions.removePanelActionExists(); diff --git a/test/functional/apps/dashboard/_view_edit.js b/test/functional/apps/dashboard/_view_edit.js index f99e59323aa28f..7d26dbe27daebd 100644 --- a/test/functional/apps/dashboard/_view_edit.js +++ b/test/functional/apps/dashboard/_view_edit.js @@ -67,7 +67,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.dashboard.saveDashboard(dashboardName, { storeTimeWithDashboard: true }); - await PageObjects.dashboard.clickEdit(); + await PageObjects.dashboard.switchToEditMode(); await PageObjects.header.setAbsoluteRange('2013-09-19 06:31:44.000', '2013-09-19 06:31:44.000'); await PageObjects.dashboard.clickCancelOutOfEditMode(); @@ -99,7 +99,7 @@ export default function ({ getService, getPageObjects }) { // This may seem like a pointless line but there was a bug that only arose when the dashboard // was loaded initially await PageObjects.dashboard.loadSavedDashboard(dashboardName); - await PageObjects.dashboard.clickEdit(); + await PageObjects.dashboard.switchToEditMode(); let hasFilter = await filterBar.hasFilter('animal', 'dog'); expect(hasFilter).to.be(true); @@ -157,7 +157,7 @@ export default function ({ getService, getPageObjects }) { const newToTime = '2015-09-19 06:31:44.000'; await PageObjects.header.setAbsoluteRange('2013-09-19 06:31:44.000', '2013-09-19 06:31:44.000'); await PageObjects.dashboard.saveDashboard(dashboardName, true); - await PageObjects.dashboard.clickEdit(); + await PageObjects.dashboard.switchToEditMode(); await PageObjects.header.setAbsoluteRange(newToTime, newToTime); await PageObjects.dashboard.clickCancelOutOfEditMode(); @@ -180,7 +180,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.dashboard.gotoDashboardEditMode(dashboardName); await PageObjects.dashboard.setTimepickerInDataRange(); await PageObjects.dashboard.saveDashboard(dashboardName, true); - await PageObjects.dashboard.clickEdit(); + await PageObjects.dashboard.switchToEditMode(); await PageObjects.header.setAbsoluteRange('2013-09-19 06:31:44.000', '2013-09-19 06:31:44.000'); const newFromTime = await PageObjects.header.getFromTime(); @@ -205,7 +205,7 @@ export default function ({ getService, getPageObjects }) { it('when time changed is not stored with dashboard', async function () { await PageObjects.dashboard.gotoDashboardEditMode(dashboardName); await PageObjects.dashboard.saveDashboard(dashboardName, { storeTimeWithDashboard: false }); - await PageObjects.dashboard.clickEdit(); + await PageObjects.dashboard.switchToEditMode(); await PageObjects.header.setAbsoluteRange('2014-10-19 06:31:44.000', '2014-12-19 06:31:44.000'); await PageObjects.dashboard.clickCancelOutOfEditMode(); diff --git a/test/functional/apps/dashboard/index.js b/test/functional/apps/dashboard/index.js index d53364ecdd186d..8fa65369b021fd 100644 --- a/test/functional/apps/dashboard/index.js +++ b/test/functional/apps/dashboard/index.js @@ -52,7 +52,10 @@ export default function ({ getService, loadTestFile, getPageObjects }) { loadTestFile(require.resolve('./_embed_mode')); loadTestFile(require.resolve('./_full_screen_mode')); loadTestFile(require.resolve('./_dashboard_filter_bar')); - loadTestFile(require.resolve('./_dashboard_filtering')); + + // TODO: unskip when https://github.com/elastic/kibana/issues/20442 is fixed + //loadTestFile(require.resolve('./_dashboard_filtering')); + loadTestFile(require.resolve('./_panel_expand_toggle')); loadTestFile(require.resolve('./_dashboard_grid')); loadTestFile(require.resolve('./_dashboard_snapshots')); diff --git a/test/functional/apps/getting_started/_shakespeare.js b/test/functional/apps/getting_started/_shakespeare.js index 6a16d98e3815c5..d421aa66167254 100644 --- a/test/functional/apps/getting_started/_shakespeare.js +++ b/test/functional/apps/getting_started/_shakespeare.js @@ -107,7 +107,6 @@ export default function ({ getService, getPageObjects }) { */ it('should configure Terms aggregation on play_name', async function () { await PageObjects.visualize.clickBucket('X-Axis'); - await PageObjects.common.sleep(1000); log.debug('Aggregation = Terms'); await PageObjects.visualize.selectAggregation('Terms'); aggIndex = aggIndex + 1; @@ -139,7 +138,7 @@ export default function ({ getService, getPageObjects }) { */ it('should configure Max aggregation metric on speech_number', async function () { await PageObjects.visualize.clickAddMetric(); - await PageObjects.visualize.clickBucket('Y-Axis'); + await PageObjects.visualize.clickBucket('Y-Axis', 'metric'); log.debug('Aggregation = Max'); await PageObjects.visualize.selectYAxisAggregation('Max', 'speech_number', 'Max Speaking Parts', aggIndex); await PageObjects.visualize.clickGo(); diff --git a/test/functional/apps/home/_sample_data.js b/test/functional/apps/home/_sample_data.js index 48cc3cf5099360..ea8174e279ab65 100644 --- a/test/functional/apps/home/_sample_data.js +++ b/test/functional/apps/home/_sample_data.js @@ -41,11 +41,6 @@ export default function ({ getService, getPageObjects }) { it('should install sample data set', async ()=> { await PageObjects.home.addSampleDataSet('flights'); - await retry.try(async () => { - const successToastExists = await PageObjects.home.doesSampleDataSetSuccessfulInstallToastExist(); - expect(successToastExists).to.be(true); - }); - const isInstalled = await PageObjects.home.isSampleDataSetInstalled('flights'); expect(isInstalled).to.be(true); }); @@ -98,11 +93,6 @@ export default function ({ getService, getPageObjects }) { describe('uninstall', () => { it('should uninstall sample data set', async ()=> { await PageObjects.home.removeSampleDataSet('flights'); - await retry.try(async () => { - const successToastExists = await PageObjects.home.doesSampleDataSetSuccessfulUninstallToastExist(); - expect(successToastExists).to.be(true); - }); - const isInstalled = await PageObjects.home.isSampleDataSetInstalled('flights'); expect(isInstalled).to.be(false); }); diff --git a/test/functional/apps/management/_test_huge_fields.js b/test/functional/apps/management/_test_huge_fields.js index 15ff739899f226..4b137def5b8e83 100644 --- a/test/functional/apps/management/_test_huge_fields.js +++ b/test/functional/apps/management/_test_huge_fields.js @@ -21,6 +21,7 @@ import expect from 'expect.js'; export default function ({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); + const log = getService('log'); const PageObjects = getPageObjects(['common', 'home', 'settings']); describe('test large number of fields @skipcloud', function () { @@ -34,6 +35,8 @@ export default function ({ getService, getPageObjects }) { it('test_huge data should have expected number of fields', async function () { const tabCount = await PageObjects.settings.getFieldsTabCount(); + //default : maxPayloadBytes is 1048576 + log.info('if there is a failure, start the server with "node scripts/functional_tests_server -- --server.maxPayloadBytes=1648576"'); expect(tabCount).to.be(EXPECTED_FIELD_COUNT); }); diff --git a/test/functional/apps/visualize/_data_table.js b/test/functional/apps/visualize/_data_table.js index 062686827b060c..0941b02244de8e 100644 --- a/test/functional/apps/visualize/_data_table.js +++ b/test/functional/apps/visualize/_data_table.js @@ -60,7 +60,7 @@ export default function ({ getService, getPageObjects }) { expect(isApplyButtonEnabled).to.be(true); }); - it('should allow resseting changed params', async () => { + it('should allow reseting changed params', async () => { await PageObjects.visualize.clickReset(); const interval = await PageObjects.visualize.getInputTypeParam('interval'); expect(interval).to.be('2000'); @@ -110,7 +110,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.visualize.clickNewSearch(); await PageObjects.header.setAbsoluteRange(fromTime, toTime); await PageObjects.visualize.clickAddMetric(); - await PageObjects.visualize.clickBucket('Metric'); + await PageObjects.visualize.clickBucket('Metric', 'metric'); await PageObjects.visualize.selectAggregation('Average Bucket', 'metrics'); await PageObjects.visualize.selectAggregation('Terms', 'metrics', 'buckets'); await PageObjects.visualize.selectField('geo.src', 'metrics', 'buckets'); diff --git a/test/functional/apps/visualize/_data_table_nontimeindex.js b/test/functional/apps/visualize/_data_table_nontimeindex.js new file mode 100644 index 00000000000000..557003f18a1cfd --- /dev/null +++ b/test/functional/apps/visualize/_data_table_nontimeindex.js @@ -0,0 +1,174 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from 'expect.js'; + +export default function ({ getService, getPageObjects }) { + const log = getService('log'); + const retry = getService('retry'); + const filterBar = getService('filterBar'); + const renderable = getService('renderable'); + const PageObjects = getPageObjects(['common', 'visualize', 'header']); + + describe.skip('data table with index without time filter', function indexPatternCreation() { + const vizName1 = 'Visualization DataTable without time filter'; + + before(async function () { + log.debug('navigateToApp visualize'); + await PageObjects.visualize.navigateToNewVisualization(); + log.debug('clickDataTable'); + await PageObjects.visualize.clickDataTable(); + log.debug('clickNewSearch'); + await PageObjects.visualize.clickNewSearch(PageObjects.visualize.index.LOGSTASH_NON_TIME_BASED); + log.debug('Bucket = Split Rows'); + await PageObjects.visualize.clickBucket('Split Rows'); + log.debug('Aggregation = Histogram'); + await PageObjects.visualize.selectAggregation('Histogram'); + log.debug('Field = bytes'); + await PageObjects.visualize.selectField('bytes'); + log.debug('Interval = 2000'); + await PageObjects.visualize.setNumericInterval('2000'); + await PageObjects.visualize.clickGo(); + }); + + it('should allow applying changed params', async () => { + await PageObjects.visualize.setNumericInterval('1', { append: true }); + const interval = await PageObjects.visualize.getInputTypeParam('interval'); + expect(interval).to.be('20001'); + const isApplyButtonEnabled = await PageObjects.visualize.isApplyEnabled(); + expect(isApplyButtonEnabled).to.be(true); + }); + + it('should allow reseting changed params', async () => { + await PageObjects.visualize.clickReset(); + const interval = await PageObjects.visualize.getInputTypeParam('interval'); + expect(interval).to.be('2000'); + }); + + it('should be able to save and load', async function () { + await PageObjects.visualize.saveVisualization(vizName1); + const pageTitle = await PageObjects.common.getBreadcrumbPageTitle(); + log.debug(`Save viz page title is ${pageTitle}`); + expect(pageTitle).to.contain(vizName1); + await PageObjects.visualize.waitForVisualizationSavedToastGone(); + await PageObjects.visualize.loadSavedVisualization(vizName1); + await PageObjects.visualize.waitForVisualization(); + }); + + it('should have inspector enabled', async function () { + const spyToggleExists = await PageObjects.visualize.isInspectorButtonEnabled(); + expect(spyToggleExists).to.be(true); + }); + + it('should show correct data', function () { + const expectedChartData = [ + [ '0B', '2,088' ], + [ '1.953KB', '2,748' ], + [ '3.906KB', '2,707' ], + [ '5.859KB', '2,876' ], + [ '7.813KB', '2,863' ], + [ '9.766KB', '147' ], + [ '11.719KB', '148' ], + [ '13.672KB', '129' ], + [ '15.625KB', '161' ], + [ '17.578KB', '137' ] + ]; + + return retry.try(async function () { + await PageObjects.visualize.openInspector(); + const data = await PageObjects.visualize.getInspectorTableData(); + await PageObjects.visualize.closeInspector(); + log.debug(data); + expect(data).to.eql(expectedChartData); + }); + }); + + it('should show correct data when using average pipeline aggregation', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickDataTable(); + await PageObjects.visualize.clickNewSearch(PageObjects.visualize.index.LOGSTASH_NON_TIME_BASED); + await PageObjects.visualize.clickAddMetric(); + await PageObjects.visualize.clickBucket('Metric', 'metric'); + await PageObjects.visualize.selectAggregation('Average Bucket', 'metrics'); + await PageObjects.visualize.selectAggregation('Terms', 'metrics', 'buckets'); + await PageObjects.visualize.selectField('geo.src', 'metrics', 'buckets'); + await PageObjects.visualize.clickGo(); + const data = await PageObjects.visualize.getTableVisData(); + log.debug(data.split('\n')); + expect(data.trim().split('\n')).to.be.eql(['14,004 1,412.6']); + }); + + it('should show correct data for a data table with date histogram', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickDataTable(); + await PageObjects.visualize.clickNewSearch(PageObjects.visualize.index.LOGSTASH_NON_TIME_BASED); + await PageObjects.visualize.clickBucket('Split Rows'); + await PageObjects.visualize.selectAggregation('Date Histogram'); + await PageObjects.visualize.selectField('@timestamp'); + await PageObjects.visualize.setInterval('Daily'); + await PageObjects.visualize.clickGo(); + await PageObjects.header.waitUntilLoadingHasFinished(); + const data = await PageObjects.visualize.getTableVisData(); + log.debug(data.split('\n')); + expect(data.trim().split('\n')).to.be.eql([ + '2015-09-20', '4,757', + '2015-09-21', '4,614', + '2015-09-22', '4,633', + ]); + }); + + it('should show correct data for a data table with date histogram', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickDataTable(); + await PageObjects.visualize.clickNewSearch(PageObjects.visualize.index.LOGSTASH_NON_TIME_BASED); + await PageObjects.visualize.clickBucket('Split Rows'); + await PageObjects.visualize.selectAggregation('Date Histogram'); + await PageObjects.visualize.selectField('@timestamp'); + await PageObjects.visualize.setInterval('Daily'); + await PageObjects.visualize.clickGo(); + await PageObjects.header.waitUntilLoadingHasFinished(); + const data = await PageObjects.visualize.getTableVisData(); + expect(data.trim().split('\n')).to.be.eql([ + '2015-09-20', '4,757', + '2015-09-21', '4,614', + '2015-09-22', '4,633', + ]); + }); + + it('should correctly filter for applied time filter on the main timefield', async () => { + await filterBar.addFilter('@timestamp', 'is between', '2015-09-19', '2015-09-21'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await renderable.waitForRender(); + const data = await PageObjects.visualize.getTableVisData(); + expect(data.trim().split('\n')).to.be.eql([ + '2015-09-20', '4,757', + ]); + }); + + it('should correctly filter for pinned filters', async () => { + await filterBar.toggleFilterPinned('@timestamp'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await renderable.waitForRender(); + const data = await PageObjects.visualize.getTableVisData(); + expect(data.trim().split('\n')).to.be.eql([ + '2015-09-20', '4,757', + ]); + }); + }); +} diff --git a/test/functional/apps/visualize/_gauge_chart.js b/test/functional/apps/visualize/_gauge_chart.js index da54b826976dc9..f0aeb8fbdb4a4d 100644 --- a/test/functional/apps/visualize/_gauge_chart.js +++ b/test/functional/apps/visualize/_gauge_chart.js @@ -76,12 +76,11 @@ export default function ({ getService, getPageObjects }) { const expectedTexts = [ '2,904\nwin 8: Count', '0B\nwin 8: Min bytes' ]; await PageObjects.visualize.clickMetricEditor(); - await PageObjects.visualize.clickBucket('Split Group'); await PageObjects.visualize.selectAggregation('Terms'); await PageObjects.visualize.selectField('machine.os.raw'); await PageObjects.visualize.setSize('1'); await PageObjects.visualize.clickAddMetric(); - await PageObjects.visualize.clickBucket('Metric'); + await PageObjects.visualize.clickBucket('Metric', 'metric'); await PageObjects.visualize.selectAggregation('Min', 'metrics'); await PageObjects.visualize.selectField('bytes', 'metrics'); await PageObjects.visualize.clickGo(); diff --git a/test/functional/apps/visualize/_input_control_vis.js b/test/functional/apps/visualize/_input_control_vis.js index a1aa01b1663fcc..bfa1aa8f1088de 100644 --- a/test/functional/apps/visualize/_input_control_vis.js +++ b/test/functional/apps/visualize/_input_control_vis.js @@ -37,7 +37,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.header.setAbsoluteRange('2017-01-01', '2017-01-02'); await PageObjects.visualize.clickVisEditorTab('controls'); await PageObjects.visualize.addInputControl(); - await comboBox.set('indexPatternSelect-0', 'logstash'); + await comboBox.set('indexPatternSelect-0', 'logstash- '); await comboBox.set('fieldSelect-0', FIELD_NAME); await PageObjects.visualize.clickGo(); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -190,7 +190,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.visualize.clickVisEditorTab('controls'); await PageObjects.visualize.addInputControl(); - await comboBox.set('indexPatternSelect-0', 'logstash'); + await comboBox.set('indexPatternSelect-0', 'logstash- '); await comboBox.set('fieldSelect-0', 'geo.src'); await PageObjects.visualize.clickGo(); @@ -233,11 +233,11 @@ export default function ({ getService, getPageObjects }) { await PageObjects.visualize.clickVisEditorTab('controls'); await PageObjects.visualize.addInputControl(); - await comboBox.set('indexPatternSelect-0', 'logstash'); + await comboBox.set('indexPatternSelect-0', 'logstash- '); await comboBox.set('fieldSelect-0', 'geo.src'); await PageObjects.visualize.addInputControl(); - await comboBox.set('indexPatternSelect-1', 'logstash'); + await comboBox.set('indexPatternSelect-1', 'logstash- '); await comboBox.set('fieldSelect-1', 'clientip'); await PageObjects.visualize.setSelectByOptionText('parentSelect-1', 'geo.src'); diff --git a/test/functional/apps/visualize/_inspector.js b/test/functional/apps/visualize/_inspector.js index 3c816eafaf6ec8..7ddf7d817471d1 100644 --- a/test/functional/apps/visualize/_inspector.js +++ b/test/functional/apps/visualize/_inspector.js @@ -48,7 +48,7 @@ export default function ({ getService, getPageObjects }) { log.debug('Add Average Metric on machine.ram field'); await PageObjects.visualize.clickAddMetric(); - await PageObjects.visualize.clickBucket('Y-Axis'); + await PageObjects.visualize.clickBucket('Y-Axis', 'metric'); await PageObjects.visualize.selectAggregation('Average', 'metrics'); await PageObjects.visualize.selectField('machine.ram', 'metrics'); await PageObjects.visualize.clickGo(); diff --git a/test/functional/apps/visualize/_linked_saved_searches.js b/test/functional/apps/visualize/_linked_saved_searches.js index 3e0a49a2f96348..634de7e99dc127 100644 --- a/test/functional/apps/visualize/_linked_saved_searches.js +++ b/test/functional/apps/visualize/_linked_saved_searches.js @@ -21,6 +21,7 @@ import expect from 'expect.js'; export default function ({ getService, getPageObjects }) { const filterBar = getService('filterBar'); + const retry = getService('retry'); const PageObjects = getPageObjects(['common', 'discover', 'visualize', 'header']); describe('visualize app', function describeIndexTests() { @@ -46,28 +47,36 @@ export default function ({ getService, getPageObjects }) { await PageObjects.visualize.clickSavedSearch(savedSearchName); await PageObjects.header.setAbsoluteRange(fromTime, toTime); await PageObjects.header.waitUntilLoadingHasFinished(); - const data = await PageObjects.visualize.getTableVisData(); - expect(data.trim()).to.be('9,109'); + await retry.waitFor('wait for count to equal 9,109', async () => { + const data = await PageObjects.visualize.getTableVisData(); + return data.trim() === '9,109'; + }); }); it('should respect the time filter when linked to a saved search', async () => { await PageObjects.header.setAbsoluteRange('2015-09-19 06:31:44.000', '2015-09-21 10:00:00.000'); await PageObjects.header.waitUntilLoadingHasFinished(); - const data = await PageObjects.visualize.getTableVisData(); - expect(data.trim()).to.be('3,950'); + await retry.waitFor('wait for count to equal 3,950', async () => { + const data = await PageObjects.visualize.getTableVisData(); + return data.trim() === '3,950'; + }); }); it('should allow adding filters while having a linked saved search', async () => { await filterBar.addFilter('bytes', 'is between', '100', '3000'); await PageObjects.header.waitUntilLoadingHasFinished(); - const data = await PageObjects.visualize.getTableVisData(); - expect(data.trim()).to.be('707'); + await retry.waitFor('wait for count to equal 707', async () => { + const data = await PageObjects.visualize.getTableVisData(); + return data.trim() === '707'; + }); }); it('should allow unlinking from a linked search', async () => { await PageObjects.visualize.clickUnlinkSavedSearch(); - const data = await PageObjects.visualize.getTableVisData(); - expect(data.trim()).to.be('707'); + await retry.waitFor('wait for count to equal 707', async () => { + const data = await PageObjects.visualize.getTableVisData(); + return data.trim() === '707'; + }); // The filter on the saved search should now be in the editor expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(true); @@ -75,15 +84,19 @@ export default function ({ getService, getPageObjects }) { // the visualization should not be linked anymore with the saved search. await filterBar.toggleFilterEnabled('extension.raw'); await PageObjects.header.waitUntilLoadingHasFinished(); - const unfilteredData = await PageObjects.visualize.getTableVisData(); - expect(unfilteredData.trim()).to.be('1,293'); + await retry.waitFor('wait for count to equal 1,293', async () => { + const unfilteredData = await PageObjects.visualize.getTableVisData(); + return unfilteredData.trim() === '1,293'; + }); }); it('should not break when saving after unlinking', async () => { await PageObjects.visualize.saveVisualization('Unlinked before saved'); await PageObjects.header.waitUntilLoadingHasFinished(); - const data = await PageObjects.visualize.getTableVisData(); - expect(data.trim()).to.be('1,293'); + await retry.waitFor('wait for count to equal 1,293', async () => { + const data = await PageObjects.visualize.getTableVisData(); + return data.trim() === '1,293'; + }); }); }); }); diff --git a/test/functional/apps/visualize/_point_series_options.js b/test/functional/apps/visualize/_point_series_options.js index dd19531a4d6df3..91e9e4e05a2bf1 100644 --- a/test/functional/apps/visualize/_point_series_options.js +++ b/test/functional/apps/visualize/_point_series_options.js @@ -47,7 +47,7 @@ export default function ({ getService, getPageObjects }) { log.debug('Add Metric'); await PageObjects.visualize.clickAddMetric(); log.debug('Metric = Value Axis'); - await PageObjects.visualize.clickBucket('Y-Axis'); + await PageObjects.visualize.clickBucket('Y-Axis', 'metric'); log.debug('Aggregation = Average'); await PageObjects.visualize.selectAggregation('Average', 'metrics'); log.debug('Field = memory'); diff --git a/test/functional/apps/visualize/_tag_cloud.js b/test/functional/apps/visualize/_tag_cloud.js index 6fcdfa1c13d799..3d559066b0a006 100644 --- a/test/functional/apps/visualize/_tag_cloud.js +++ b/test/functional/apps/visualize/_tag_cloud.js @@ -41,10 +41,8 @@ export default function ({ getService, getPageObjects }) { await PageObjects.visualize.clickNewSearch(); log.debug('Set absolute time range from \"' + fromTime + '\" to \"' + toTime + '\"'); await PageObjects.header.setAbsoluteRange(fromTime, toTime); - await PageObjects.common.sleep(1000); log.debug('select Tags'); await PageObjects.visualize.clickBucket('Tags'); - await PageObjects.common.sleep(1000); log.debug('Click aggregation Terms'); await PageObjects.visualize.selectAggregation('Terms'); log.debug('Click field machine.ram'); diff --git a/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.js b/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.js new file mode 100644 index 00000000000000..65ab39569612ed --- /dev/null +++ b/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.js @@ -0,0 +1,275 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from 'expect.js'; + +export default function ({ getService, getPageObjects }) { + const log = getService('log'); + const retry = getService('retry'); + const PageObjects = getPageObjects(['common', 'visualize', 'header']); + + describe.skip('vertical bar chart with index without time filter', function () { + const vizName1 = 'Visualization VerticalBarChart without time filter'; + + const initBarChart = async () => { + log.debug('navigateToApp visualize'); + await PageObjects.visualize.navigateToNewVisualization(); + log.debug('clickVerticalBarChart'); + await PageObjects.visualize.clickVerticalBarChart(); + await PageObjects.visualize.clickNewSearch(PageObjects.visualize.index.LOGSTASH_NON_TIME_BASED); + await PageObjects.common.sleep(500); + log.debug('Bucket = X-Axis'); + await PageObjects.visualize.clickBucket('X-Axis'); + log.debug('Aggregation = Date Histogram'); + await PageObjects.visualize.selectAggregation('Date Histogram'); + log.debug('Field = @timestamp'); + await PageObjects.visualize.selectField('@timestamp'); + await PageObjects.visualize.setCustomInterval('3h'); + await PageObjects.visualize.clickGo(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.visualize.waitForVisualization(); + }; + + + before(initBarChart); + + it('should save and load', async function () { + await PageObjects.visualize.saveVisualization(vizName1); + const pageTitle = await PageObjects.common.getBreadcrumbPageTitle(); + log.debug(`Save viz page title is ${pageTitle}`); + expect(pageTitle).to.contain(vizName1); + await PageObjects.visualize.waitForVisualizationSavedToastGone(); + await PageObjects.visualize.loadSavedVisualization(vizName1); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.visualize.waitForVisualization(); + }); + + it('should have inspector enabled', async function () { + const spyToggleExists = await PageObjects.visualize.isInspectorButtonEnabled(); + expect(spyToggleExists).to.be(true); + }); + + it('should show correct chart', async function () { + const expectedChartValues = [37, 202, 740, 1437, 1371, 751, 188, 31, 42, 202, 683, + 1361, 1415, 707, 177, 27, 32, 175, 707, 1408, 1355, 726, 201, 29 + ]; + + // Most recent failure on Jenkins usually indicates the bar chart is still being drawn? + // return arguments[0].getAttribute(arguments[1]);","args":[{"ELEMENT":"592"},"fill"]}] arguments[0].getAttribute is not a function + // try sleeping a bit before getting that data + await retry.try(async () => { + const data = await PageObjects.visualize.getBarChartData(); + log.debug('data=' + data); + log.debug('data.length=' + data.length); + expect(data).to.eql(expectedChartValues); + }); + }); + + it('should show correct data', async function () { + // this is only the first page of the tabular data. + const expectedChartData = [ + ['2015-09-20 00:00', '37'], + ['2015-09-20 03:00', '202'], + ['2015-09-20 06:00', '740'], + ['2015-09-20 09:00', '1,437'], + ['2015-09-20 12:00', '1,371'], + ['2015-09-20 15:00', '751'], + ['2015-09-20 18:00', '188'], + ['2015-09-20 21:00', '31'], + ['2015-09-21 00:00', '42'], + ['2015-09-21 03:00', '202'], + [ '2015-09-21 06:00', '683' ], + [ '2015-09-21 09:00', '1,361' ], + [ '2015-09-21 12:00', '1,415' ], + [ '2015-09-21 15:00', '707' ], + [ '2015-09-21 18:00', '177' ], + [ '2015-09-21 21:00', '27' ], + [ '2015-09-22 00:00', '32' ], + [ '2015-09-22 03:00', '175' ], + [ '2015-09-22 06:00', '707' ], + [ '2015-09-22 09:00', '1,408' ], + ]; + + await PageObjects.visualize.openInspector(); + const data = await PageObjects.visualize.getInspectorTableData(); + log.debug(data); + expect(data).to.eql(expectedChartData); + }); + + describe.skip('switch between Y axis scale types', () => { + before(initBarChart); + const axisId = 'ValueAxis-1'; + + it('should show ticks on selecting log scale', async () => { + await PageObjects.visualize.clickMetricsAndAxes(); + await PageObjects.visualize.clickYAxisOptions(axisId); + await PageObjects.visualize.selectYAxisScaleType(axisId, 'log'); + await PageObjects.visualize.clickYAxisAdvancedOptions(axisId); + await PageObjects.visualize.changeYAxisFilterLabelsCheckbox(axisId, false); + await PageObjects.visualize.clickGo(); + const labels = await PageObjects.visualize.getYAxisLabels(); + const expectedLabels = [ + '2', '3', '5', '7', '10', '20', '30', '50', '70', '100', '200', + '300', '500', '700', '1,000', '2,000', '3,000', '5,000', '7,000', + ]; + expect(labels).to.eql(expectedLabels); + }); + + it('should show filtered ticks on selecting log scale', async () => { + await PageObjects.visualize.changeYAxisFilterLabelsCheckbox(axisId, true); + await PageObjects.visualize.clickGo(); + const labels = await PageObjects.visualize.getYAxisLabels(); + const expectedLabels = [ + '2', '3', '5', '7', '10', '20', '30', '50', '70', '100', '200', + '300', '500', '700', '1,000', '2,000', '3,000', '5,000', '7,000', + ]; + expect(labels).to.eql(expectedLabels); + }); + + it('should show ticks on selecting square root scale', async () => { + await PageObjects.visualize.selectYAxisScaleType(axisId, 'square root'); + await PageObjects.visualize.changeYAxisFilterLabelsCheckbox(axisId, false); + await PageObjects.visualize.clickGo(); + const labels = await PageObjects.visualize.getYAxisLabels(); + const expectedLabels = [ + '0', '200', '400', '600', '800', '1,000', '1,200', '1,400', '1,600', + ]; + expect(labels).to.eql(expectedLabels); + }); + + it('should show filtered ticks on selecting square root scale', async () => { + await PageObjects.visualize.changeYAxisFilterLabelsCheckbox(axisId, true); + await PageObjects.visualize.clickGo(); + const labels = await PageObjects.visualize.getYAxisLabels(); + const expectedLabels = [ + '200', '400', '600', '800', '1,000', '1,200', '1,400', + ]; + expect(labels).to.eql(expectedLabels); + }); + + it('should show ticks on selecting linear scale', async () => { + await PageObjects.visualize.selectYAxisScaleType(axisId, 'linear'); + await PageObjects.visualize.changeYAxisFilterLabelsCheckbox(axisId, false); + await PageObjects.visualize.clickGo(); + const labels = await PageObjects.visualize.getYAxisLabels(); + log.debug(labels); + const expectedLabels = [ + '0', '200', '400', '600', '800', '1,000', '1,200', '1,400', '1,600', + ]; + expect(labels).to.eql(expectedLabels); + }); + + it('should show filtered ticks on selecting linear scale', async () => { + await PageObjects.visualize.changeYAxisFilterLabelsCheckbox(axisId, true); + await PageObjects.visualize.clickGo(); + const labels = await PageObjects.visualize.getYAxisLabels(); + const expectedLabels = [ + '200', '400', '600', '800', '1,000', '1,200', '1,400', + ]; + expect(labels).to.eql(expectedLabels); + }); + }); + + describe('vertical bar with split series', function () { + before(initBarChart); + + it('should show correct series', async function () { + await PageObjects.visualize.toggleOpenEditor(2, 'false'); + await PageObjects.visualize.clickAddBucket(); + await PageObjects.visualize.clickBucket('Split Series'); + await PageObjects.visualize.selectAggregation('Terms'); + await PageObjects.visualize.selectField('response.raw'); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await PageObjects.common.sleep(1003); + await PageObjects.visualize.clickGo(); + await PageObjects.header.waitUntilLoadingHasFinished(); + + const expectedEntries = ['200', '404', '503']; + const legendEntries = await PageObjects.visualize.getLegendEntries(); + expect(legendEntries).to.eql(expectedEntries); + }); + }); + + describe('vertical bar with multiple splits', function () { + before(initBarChart); + + it('should show correct series', async function () { + await PageObjects.visualize.toggleOpenEditor(2, 'false'); + await PageObjects.visualize.clickAddBucket(); + await PageObjects.visualize.clickBucket('Split Series'); + await PageObjects.visualize.selectAggregation('Terms'); + await PageObjects.visualize.selectField('response.raw'); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await PageObjects.visualize.toggleOpenEditor(3, 'false'); + await PageObjects.visualize.clickAddBucket(); + await PageObjects.visualize.clickBucket('Split Series'); + await PageObjects.visualize.selectAggregation('Terms'); + await PageObjects.visualize.selectField('machine.os'); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await PageObjects.common.sleep(1003); + await PageObjects.visualize.clickGo(); + await PageObjects.header.waitUntilLoadingHasFinished(); + + const expectedEntries = [ + '200 - win 8', '200 - win xp', '200 - ios', '200 - osx', '200 - win 7', + '404 - ios', '503 - ios', '503 - osx', '503 - win 7', '503 - win 8', + '503 - win xp', '404 - osx', '404 - win 7', '404 - win 8', '404 - win xp' + ]; + const legendEntries = await PageObjects.visualize.getLegendEntries(); + expect(legendEntries).to.eql(expectedEntries); + }); + + it('should show correct series when disabling first agg', async function () { + await PageObjects.visualize.toggleDisabledAgg(3); + await PageObjects.visualize.clickGo(); + await PageObjects.header.waitUntilLoadingHasFinished(); + + const expectedEntries = [ 'win 8', 'win xp', 'ios', 'osx', 'win 7' ]; + const legendEntries = await PageObjects.visualize.getLegendEntries(); + expect(legendEntries).to.eql(expectedEntries); + }); + }); + + describe('vertical bar with derivative', function () { + before(initBarChart); + + it('should show correct series', async function () { + await PageObjects.visualize.toggleOpenEditor(2, 'false'); + await PageObjects.visualize.toggleOpenEditor(1); + await PageObjects.visualize.selectAggregation('Derivative', 'metrics'); + await PageObjects.header.waitUntilLoadingHasFinished(); + + + await PageObjects.common.sleep(1003); + await PageObjects.visualize.clickGo(); + await PageObjects.header.waitUntilLoadingHasFinished(); + + const expectedEntries = [ + 'Derivative of Count' + ]; + const legendEntries = await PageObjects.visualize.getLegendEntries(); + expect(legendEntries).to.eql(expectedEntries); + }); + + }); + }); +} diff --git a/test/functional/apps/visualize/index.js b/test/functional/apps/visualize/index.js index e34c33352858c3..808d7fdfe5bf94 100644 --- a/test/functional/apps/visualize/index.js +++ b/test/functional/apps/visualize/index.js @@ -40,11 +40,13 @@ export default function ({ getService, loadTestFile }) { loadTestFile(require.resolve('./_area_chart')); loadTestFile(require.resolve('./_line_chart')); loadTestFile(require.resolve('./_data_table')); + loadTestFile(require.resolve('./_data_table_nontimeindex')); loadTestFile(require.resolve('./_pie_chart')); loadTestFile(require.resolve('./_tag_cloud')); loadTestFile(require.resolve('./_tile_map')); loadTestFile(require.resolve('./_region_map')); loadTestFile(require.resolve('./_vertical_bar_chart')); + loadTestFile(require.resolve('./_vertical_bar_chart_nontimeindex')); loadTestFile(require.resolve('./_heatmap_chart')); loadTestFile(require.resolve('./_point_series_options')); loadTestFile(require.resolve('./_markdown_vis')); diff --git a/test/functional/config.js b/test/functional/config.js index 147431a184fe4f..c142d46ccd29b7 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -122,6 +122,12 @@ export default async function ({ readConfigFile }) { ], }, + uiSettings: { + defaults: { + 'accessibility:disableAnimations': true, + }, + }, + apps: { status_page: { pathname: '/status', diff --git a/test/functional/fixtures/es_archiver/visualize/data.json.gz b/test/functional/fixtures/es_archiver/visualize/data.json.gz index 600898451583e6..fc520fc2b682d4 100644 Binary files a/test/functional/fixtures/es_archiver/visualize/data.json.gz and b/test/functional/fixtures/es_archiver/visualize/data.json.gz differ diff --git a/test/functional/page_objects/dashboard_page.js b/test/functional/page_objects/dashboard_page.js index e9c0ddde2ba5cb..bb7611c233ae5d 100644 --- a/test/functional/page_objects/dashboard_page.js +++ b/test/functional/page_objects/dashboard_page.js @@ -173,9 +173,12 @@ export function DashboardPageProvider({ getService, getPageObjects }) { return await testSubjects.exists('titleDupicateWarnMsg'); } - async clickEdit() { - log.debug('Clicking edit'); - return await testSubjects.click('dashboardEditMode'); + async switchToEditMode() { + log.debug('Switching to edit mode'); + await testSubjects.click('dashboardEditMode'); + await retry.waitFor('not in view mode', async () => ( + !await this.getIsInViewMode() + )); } async getIsInViewMode() { @@ -278,7 +281,7 @@ export function DashboardPageProvider({ getService, getPageObjects }) { async gotoDashboardEditMode(dashboardName) { await this.loadSavedDashboard(dashboardName); - await this.clickEdit(); + await this.switchToEditMode(); } async renameDashboard(dashName) { diff --git a/test/functional/page_objects/header_page.js b/test/functional/page_objects/header_page.js index b90d1a5e45be71..450c28add72440 100644 --- a/test/functional/page_objects/header_page.js +++ b/test/functional/page_objects/header_page.js @@ -268,6 +268,16 @@ export function HeaderPageProvider({ getService, getPageObjects }) { await testSubjects.find('globalLoadingIndicator-hidden', defaultFindTimeout * 10); } + async getGlobalNavigationLink(linkText) { + const nav = await testSubjects.find('globalNav'); + return await nav.findByPartialLinkText(linkText); + } + + async clickGlobalNavigationLink(appTitle) { + const link = await this.getGlobalNavigationLink(appTitle); + await link.click(); + } + async getPrettyDuration() { return await testSubjects.getVisibleText('globalTimepickerRange'); } diff --git a/test/functional/page_objects/home_page.js b/test/functional/page_objects/home_page.js index 77205e7bc33230..78dae43bf06844 100644 --- a/test/functional/page_objects/home_page.js +++ b/test/functional/page_objects/home_page.js @@ -53,10 +53,22 @@ export function HomePageProvider({ getService }) { async addSampleDataSet(id) { await testSubjects.click(`addSampleDataSet${id}`); + await this._waitForSampleDataLoadingAction(id); } async removeSampleDataSet(id) { await testSubjects.click(`removeSampleDataSet${id}`); + await this._waitForSampleDataLoadingAction(id); + } + + // loading action is either uninstall and install + async _waitForSampleDataLoadingAction(id) { + const sampleDataCard = await testSubjects.find(`sampleDataSetCard${id}`); + await retry.try(async () => { + // waitForDeletedByClassName needs to be inside retry because it will timeout at least once + // before action is complete + await sampleDataCard.waitForDeletedByClassName('euiLoadingSpinner'); + }); } async launchSampleDataSet(id) { diff --git a/test/functional/page_objects/visualize_page.js b/test/functional/page_objects/visualize_page.js index bb1bcdcd29bf55..a1103eb1762fcf 100644 --- a/test/functional/page_objects/visualize_page.js +++ b/test/functional/page_objects/visualize_page.js @@ -35,6 +35,13 @@ export function VisualizePageProvider({ getService, getPageObjects }) { class VisualizePage { + get index() { + return { + LOGSTASH_TIME_BASED: 'logstash-*', + LOGSTASH_NON_TIME_BASED: 'logstash*' + }; + } + async navigateToNewVisualization() { log.debug('navigateToApp visualize new'); await PageObjects.common.navigateToUrl('visualize', 'new'); @@ -353,7 +360,7 @@ export function VisualizePageProvider({ getService, getPageObjects }) { await find.clickByCssSelector('button[data-test-subj="toggleEditor"]'); } - async clickNewSearch(indexPattern = 'logstash-*') { + async clickNewSearch(indexPattern = this.index.LOGSTASH_TIME_BASED) { await testSubjects.click(`paginatedListItem-${indexPattern}`); await PageObjects.header.waitUntilLoadingHasFinished(); } @@ -385,20 +392,26 @@ export function VisualizePageProvider({ getService, getPageObjects }) { } // clickBucket(bucketType) 'X-Axis', 'Split Area', 'Split Chart' - async clickBucket(bucketName) { - const chartTypes = await retry.try( - async () => await find.allByCssSelector('li.list-group-item.list-group-menu-item')); - log.debug('found bucket types ' + chartTypes.length); - - async function getChartType(chart) { - const chartString = await chart.getVisibleText(); - if (chartString === bucketName) { - await chart.click(); - await PageObjects.common.sleep(500); + async clickBucket(bucketName, type = 'bucket') { + const testSubject = type === 'bucket' ? 'bucketsAggGroup' : 'metricsAggGroup'; + await retry.try(async () => { + const chartTypes = await retry.try( + async () => await find.allByCssSelector(`[data-test-subj="${testSubject}"] .list-group-menu-item`)); + log.debug('found bucket types ' + chartTypes.length); + + async function getChartType(chart) { + const chartString = await chart.getVisibleText(); + if (chartString === bucketName) { + await chart.click(); + return true; + } } - } - const getChartTypesPromises = chartTypes.map(getChartType); - await Promise.all(getChartTypesPromises); + const getChartTypesPromises = chartTypes.map(getChartType); + const clickResult = await Promise.all(getChartTypesPromises); + if (!clickResult.some(result => result === true)) { + throw new Error(`bucket ${bucketName} not found`); + } + }); } async selectAggregation(myString, groupName = 'buckets', childAggregationType = null) { @@ -541,6 +554,14 @@ export function VisualizePageProvider({ getService, getPageObjects }) { async setInterval(newValue) { const input = await find.byCssSelector('select[ng-model="agg.params.interval"]'); await input.type(newValue); + await remote.pressKeys(Keys.RETURN); + } + + async setCustomInterval(newValue) { + await this.setInterval('Custom'); + const input = await find.byCssSelector('input[name="customInterval"]'); + await input.clearValue(); + await input.type(newValue); } async setNumericInterval(newValue, { append } = {}) { diff --git a/test/functional/services/dashboard/add_panel.js b/test/functional/services/dashboard/add_panel.js index 4d11f805edf2a0..d6d3ad6dd282f7 100644 --- a/test/functional/services/dashboard/add_panel.js +++ b/test/functional/services/dashboard/add_panel.js @@ -24,6 +24,7 @@ export function DashboardAddPanelProvider({ getService, getPageObjects }) { const testSubjects = getService('testSubjects'); const flyout = getService('flyout'); const PageObjects = getPageObjects(['header', 'common']); + const find = getService('find'); return new class DashboardAddPanel { async clickOpenAddPanel() { @@ -94,8 +95,10 @@ export function DashboardAddPanelProvider({ getService, getPageObjects }) { } async waitForEuiTableLoading() { - const addPanel = await testSubjects.find('dashboardAddPanel'); - await addPanel.waitForDeletedByClassName('euiBasicTable-loading'); + await retry.waitFor('dashboard add panel loading to complete', async () => { + const table = await find.byClassName('euiBasicTable'); + return !((await table.getAttribute('class')).includes('loading')); + }); } async closeAddPanel() { @@ -169,8 +172,6 @@ export function DashboardAddPanelProvider({ getService, getPageObjects }) { async addVisualization(vizName) { log.debug(`DashboardAddPanel.addVisualization(${vizName})`); await this.ensureAddPanelIsShowing(); - // workaround for timing issue with slideout animation - await PageObjects.common.sleep(500); await this.filterEmbeddableNames(`"${vizName.replace('-', ' ')}"`); await testSubjects.click(`addPanel${vizName.split(' ').join('-')}`); await this.closeAddPanel(); diff --git a/test/functional/services/dashboard/visualizations.js b/test/functional/services/dashboard/visualizations.js index 0d3ea8cf4d2574..918aa404fe0e6b 100644 --- a/test/functional/services/dashboard/visualizations.js +++ b/test/functional/services/dashboard/visualizations.js @@ -30,7 +30,7 @@ export function DashboardVisualizationProvider({ getService, getPageObjects }) { log.debug(`createAndAddTSVBVisualization(${name})`); const inViewMode = await PageObjects.dashboard.getIsInViewMode(); if (inViewMode) { - await PageObjects.dashboard.clickEdit(); + await PageObjects.dashboard.switchToEditMode(); } await dashboardAddPanel.ensureAddPanelIsShowing(); await dashboardAddPanel.clickAddNewEmbeddableLink(); @@ -68,7 +68,7 @@ export function DashboardVisualizationProvider({ getService, getPageObjects }) { const inViewMode = await PageObjects.dashboard.getIsInViewMode(); if (inViewMode) { - await PageObjects.dashboard.clickEdit(); + await PageObjects.dashboard.switchToEditMode(); } await dashboardAddPanel.addSavedSearch(name); } @@ -77,7 +77,7 @@ export function DashboardVisualizationProvider({ getService, getPageObjects }) { log.debug(`createAndAddMarkdown(${markdown})`); const inViewMode = await PageObjects.dashboard.getIsInViewMode(); if (inViewMode) { - await PageObjects.dashboard.clickEdit(); + await PageObjects.dashboard.switchToEditMode(); } await dashboardAddPanel.ensureAddPanelIsShowing(); await dashboardAddPanel.clickAddNewEmbeddableLink(); diff --git a/test/functional/services/find.js b/test/functional/services/find.js index f6d008cc384801..59029fc61b3aa6 100644 --- a/test/functional/services/find.js +++ b/test/functional/services/find.js @@ -72,6 +72,13 @@ export function FindProvider({ getService }) { }); } + async byClassName(selector, timeout = defaultFindTimeout) { + log.debug(`findByCssSelector ${selector}`); + return await this._ensureElementWithTimeout(timeout, async remote => { + return await remote.findByClassName(selector); + }); + } + async setValue(selector, text) { return await retry.try(async () => { const element = await this.byCssSelector(selector); diff --git a/test/functional/services/remote/browser_driver_api/browser_driver_local_api.js b/test/functional/services/remote/browser_driver_api/browser_driver_local_api.js index 9cbb029cf4014a..f51dfdaa1a883c 100644 --- a/test/functional/services/remote/browser_driver_api/browser_driver_local_api.js +++ b/test/functional/services/remote/browser_driver_api/browser_driver_local_api.js @@ -62,7 +62,7 @@ export function createLocalBrowserDriverApi(log, url, browser) { }); proc.on('exit', (code) => { - if (!api.isStopped() || code > 0) { + if (!api.isStopped() && code > 0) { api.emit('error', new Error(driverName + ` exited with code ${code}`)); } }); diff --git a/test/plugin_functional/README.md b/test/plugin_functional/README.md new file mode 100644 index 00000000000000..99026f22b14c25 --- /dev/null +++ b/test/plugin_functional/README.md @@ -0,0 +1,23 @@ +# Plugin Functional Tests + +This folder contains plugin functional tests, i.e. functional tests that should be executed +against a Kibana instance with specific test plugins available. + +To add a plugin to the instance, just place the plugin folder in the `plugins` +directory. + +Add new test suites into the `test_suites` folder and reference them from the +`config.js` file. These test suites work the same as regular functional test +except that they are executed against a Kibana with all plugins (from the +`plugins` directory) installed. + +## Run the test + +To run these tests during development you can use the following commands: + +``` +# Start the test server (can continue running) +node scripts/functional_tests_server.js --config test/plugin_functional/config.js +# Start a test run +node scripts/functional_test_runner.js --config test/plugin_functional/config.js +``` diff --git a/test/panel_actions/config.js b/test/plugin_functional/config.js similarity index 74% rename from test/panel_actions/config.js rename to test/plugin_functional/config.js index 8493c56ae86877..2a6607d126e6f4 100644 --- a/test/panel_actions/config.js +++ b/test/plugin_functional/config.js @@ -18,18 +18,23 @@ */ import path from 'path'; +import fs from 'fs'; export default async function ({ readConfigFile }) { const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + // Find all folders in ./plugins since we treat all them as plugin folder + const allFiles = fs.readdirSync(path.resolve(__dirname, 'plugins')); + const plugins = allFiles.filter(file => fs.statSync(path.resolve(__dirname, 'plugins', file)).isDirectory()); + return { testFiles: [ - require.resolve('./index'), + require.resolve('./test_suites/app_plugins'), + require.resolve('./test_suites/panel_actions'), ], services: functionalConfig.get('services'), pageObjects: functionalConfig.get('pageObjects'), servers: functionalConfig.get('servers'), - env: functionalConfig.get('env'), esTestCluster: functionalConfig.get('esTestCluster'), apps: functionalConfig.get('apps'), esArchiver: { @@ -37,13 +42,13 @@ export default async function ({ readConfigFile }) { }, screenshots: functionalConfig.get('screenshots'), junit: { - reportName: 'Panel Actions Functional Tests', + reportName: 'Plugin Functional Tests', }, kbnTestServer: { ...functionalConfig.get('kbnTestServer'), serverArgs: [ ...functionalConfig.get('kbnTestServer.serverArgs'), - `--plugin-path=${path.resolve(__dirname, './sample_panel_action')}`, + ...plugins.map(pluginDir => `--plugin-path=${path.resolve(__dirname, 'plugins', pluginDir)}`), ], }, }; diff --git a/src/core/server/legacy_compat/legacy_kbn_server.ts b/test/plugin_functional/plugins/sample_app_plugin/index.js similarity index 65% rename from src/core/server/legacy_compat/legacy_kbn_server.ts rename to test/plugin_functional/plugins/sample_app_plugin/index.js index 4f4cd53677d3e4..b52ff7c7d58f08 100644 --- a/src/core/server/legacy_compat/legacy_kbn_server.ts +++ b/test/plugin_functional/plugins/sample_app_plugin/index.js @@ -17,18 +17,14 @@ * under the License. */ -/** - * Represents a wrapper around legacy `kbnServer` instance that exposes only - * a subset of `kbnServer` APIs used by the new platform. - * @internal - */ -export class LegacyKbnServer { - constructor(private readonly rawKbnServer: any) {} - - /** - * Custom HTTP Listener used by HapiJS server in the legacy platform. - */ - get newPlatformProxyListener() { - return this.rawKbnServer.newPlatform.proxyListener; - } +export default function (kibana) { + return new kibana.Plugin({ + uiExports: { + app: { + title: 'Test Plugin App', + description: 'This is a sample plugin for the functional tests.', + main: 'plugins/sample_app_plugin/app', + } + } + }); } diff --git a/test/plugin_functional/plugins/sample_app_plugin/package.json b/test/plugin_functional/plugins/sample_app_plugin/package.json new file mode 100644 index 00000000000000..f61a46cb5e09bb --- /dev/null +++ b/test/plugin_functional/plugins/sample_app_plugin/package.json @@ -0,0 +1,4 @@ +{ + "name": "sample_app_plugin", + "version": "kibana" +} diff --git a/test/plugin_functional/plugins/sample_app_plugin/public/app.js b/test/plugin_functional/plugins/sample_app_plugin/public/app.js new file mode 100644 index 00000000000000..a7a516bb0cdbd1 --- /dev/null +++ b/test/plugin_functional/plugins/sample_app_plugin/public/app.js @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import 'ui/autoload/all'; + +import chrome from 'ui/chrome'; + +chrome.setRootTemplate('
Super simple app plugin
'); diff --git a/test/panel_actions/sample_panel_action/index.js b/test/plugin_functional/plugins/sample_panel_action/index.js similarity index 100% rename from test/panel_actions/sample_panel_action/index.js rename to test/plugin_functional/plugins/sample_panel_action/index.js diff --git a/test/panel_actions/sample_panel_action/package.json b/test/plugin_functional/plugins/sample_panel_action/package.json similarity index 79% rename from test/panel_actions/sample_panel_action/package.json rename to test/plugin_functional/plugins/sample_panel_action/package.json index f1603503a439ba..1e9b81be558e43 100644 --- a/test/panel_actions/sample_panel_action/package.json +++ b/test/plugin_functional/plugins/sample_panel_action/package.json @@ -1,6 +1,6 @@ { "name": "sample_panel_action", - "version": "7.0.0-alpha1", + "version": "kibana", "dependencies": { "@elastic/eui": "0.0.55", "react": "^16.4.1" diff --git a/test/panel_actions/sample_panel_action/public/sample_panel_action.js b/test/plugin_functional/plugins/sample_panel_action/public/sample_panel_action.js similarity index 93% rename from test/panel_actions/sample_panel_action/public/sample_panel_action.js rename to test/plugin_functional/plugins/sample_panel_action/public/sample_panel_action.js index 3f5cbf8f18f436..de5cdbc6628971 100644 --- a/test/panel_actions/sample_panel_action/public/sample_panel_action.js +++ b/test/plugin_functional/plugins/sample_panel_action/public/sample_panel_action.js @@ -18,12 +18,12 @@ */ import { EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eui'; import React from 'react'; -import { openFlyout } from '../../../../src/ui/public/flyout'; +import { openFlyout } from 'ui/flyout'; import { DashboardPanelAction, DashboardPanelActionsRegistryProvider, -} from '../../../../src/ui/public/dashboard_panel_actions'; +} from 'ui/dashboard_panel_actions'; class SamplePanelAction extends DashboardPanelAction { constructor() { diff --git a/test/plugin_functional/test_suites/app_plugins/app_navigation.js b/test/plugin_functional/test_suites/app_plugins/app_navigation.js new file mode 100644 index 00000000000000..33de534e6354cd --- /dev/null +++ b/test/plugin_functional/test_suites/app_plugins/app_navigation.js @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from 'expect.js'; + +export default function ({ getService, getPageObjects }) { + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common', 'header', 'home']); + + describe('app navigation', function describeIndexTests() { + + before(async () => { + await PageObjects.common.navigateToApp('settings'); + }); + + it('should show in navigation', async () => { + const link = await PageObjects.header.getGlobalNavigationLink('Test Plugin App'); + expect(link).not.to.be(undefined); + }); + + it('should navigate to the app', async () => { + await PageObjects.header.clickGlobalNavigationLink('Test Plugin App'); + const pluginContent = await testSubjects.find('pluginContent'); + expect(await pluginContent.getVisibleText()).to.be('Super simple app plugin'); + }); + }); + +} diff --git a/test/plugin_functional/test_suites/app_plugins/index.js b/test/plugin_functional/test_suites/app_plugins/index.js new file mode 100644 index 00000000000000..0a810fded75090 --- /dev/null +++ b/test/plugin_functional/test_suites/app_plugins/index.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export default function ({ loadTestFile }) { + describe('app plugins', () => { + loadTestFile(require.resolve('./app_navigation')); + }); +} diff --git a/test/panel_actions/index.js b/test/plugin_functional/test_suites/panel_actions/index.js similarity index 86% rename from test/panel_actions/index.js rename to test/plugin_functional/test_suites/panel_actions/index.js index 48f743371f3264..42b059084aba56 100644 --- a/test/panel_actions/index.js +++ b/test/plugin_functional/test_suites/panel_actions/index.js @@ -19,8 +19,8 @@ import path from 'path'; -export const KIBANA_ARCHIVE_PATH = path.resolve(__dirname, '../functional/fixtures/es_archiver/dashboard/current/kibana'); -export const DATA_ARCHIVE_PATH = path.resolve(__dirname, '../functional/fixtures/es_archiver/dashboard/current/data'); +export const KIBANA_ARCHIVE_PATH = path.resolve(__dirname, '../../../functional/fixtures/es_archiver/dashboard/current/kibana'); +export const DATA_ARCHIVE_PATH = path.resolve(__dirname, '../../../functional/fixtures/es_archiver/dashboard/current/data'); export default function ({ getService, getPageObjects, loadTestFile }) { diff --git a/test/panel_actions/panel_actions.js b/test/plugin_functional/test_suites/panel_actions/panel_actions.js similarity index 100% rename from test/panel_actions/panel_actions.js rename to test/plugin_functional/test_suites/panel_actions/panel_actions.js diff --git a/x-pack/gulpfile.js b/x-pack/gulpfile.js index 503adf91cb476b..9d45efa40f0d53 100644 --- a/x-pack/gulpfile.js +++ b/x-pack/gulpfile.js @@ -15,7 +15,7 @@ const path = require('path'); const del = require('del'); const runSequence = require('run-sequence'); const pluginHelpers = require('@kbn/plugin-helpers'); -const { createToolingLog } = require('@kbn/dev-utils'); +const { ToolingLog } = require('@kbn/dev-utils'); const logger = require('./gulp_helpers/logger'); const buildVersion = require('./gulp_helpers/build_version')(); @@ -76,8 +76,10 @@ gulp.task('build', ['clean', 'report', 'prepare'], async () => { }); const buildRoot = path.resolve(buildTarget, 'kibana/x-pack'); - const log = createToolingLog('info'); - log.pipe(process.stdout); + const log = new ToolingLog({ + level: 'info', + writeTo: process.stdout + }); writeFileSync( path.resolve(buildRoot, 'NOTICE.txt'), diff --git a/x-pack/package.json b/x-pack/package.json index c9ebc165708ee8..edb4d4c5313fdf 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -79,7 +79,7 @@ "yargs": "4.7.1" }, "dependencies": { - "@elastic/eui": "3.6.1", + "@elastic/eui": "3.7.0", "@elastic/node-crypto": "0.1.2", "@elastic/node-phantom-simple": "2.2.4", "@elastic/numeral": "2.3.2", diff --git a/x-pack/plugins/apm/public/components/app/Main/Breadcrumbs.js b/x-pack/plugins/apm/public/components/app/Main/Breadcrumbs.js index d072a69d2cb8c0..88d66795d05c00 100644 --- a/x-pack/plugins/apm/public/components/app/Main/Breadcrumbs.js +++ b/x-pack/plugins/apm/public/components/app/Main/Breadcrumbs.js @@ -11,8 +11,6 @@ import { routes } from './routeConfig'; import { flatten, capitalize } from 'lodash'; class Breadcrumbs extends React.Component { - componentWillUpdate() {} - render() { const { breadcrumbs, location } = this.props; const { _g = '', kuery = '' } = toQuery(location.search); diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.js b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.js new file mode 100644 index 00000000000000..120196541c04ba --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.js @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import ServiceOverview from '../view'; +import { STATUS } from '../../../../constants'; +import * as apmRestServices from '../../../../services/rest/apm'; + +jest.mock('../../../../services/rest/apm'); + +describe('Service Overview -> View', () => { + let wrapper; + let instance; + + beforeEach(() => { + wrapper = shallow(); + instance = wrapper.instance(); + }); + + it('should render when historical data is found', () => { + expect(wrapper).toMatchSnapshot(); + const List = wrapper + .find('ServiceListRequest') + .props() + .render({}); + expect(List.props).toMatchSnapshot(); + }); + + it('should render when historical data is not found', () => { + wrapper.setState({ historicalDataFound: false }); + expect(wrapper).toMatchSnapshot(); + const List = wrapper + .find('ServiceListRequest') + .props() + .render({}); + expect(List.props).toMatchSnapshot(); + }); + + describe('checking for historical data', () => { + let mockAgentStatus; + + beforeEach(() => { + mockAgentStatus = { + dataFound: true + }; + // eslint-disable-next-line import/namespace + apmRestServices.loadAgentStatus = jest.fn(() => + Promise.resolve(mockAgentStatus) + ); + }); + + it('should happen if service list status is success and data is empty', async () => { + const props = { + serviceList: { + status: STATUS.SUCCESS, + data: [] + } + }; + await instance.checkForHistoricalData(props); + expect(apmRestServices.loadAgentStatus).toHaveBeenCalledTimes(1); + }); + + it('should not happen if sevice list status is not success', async () => { + const props = { + serviceList: { + status: STATUS.FAILURE, + data: [] + } + }; + await instance.checkForHistoricalData(props); + expect(apmRestServices.loadAgentStatus).not.toHaveBeenCalled(); + }); + + it('should not happen if service list data is not empty', async () => { + const props = { + serviceList: { + status: STATUS.SUCCESS, + data: [1, 2, 3] + } + }; + await instance.checkForHistoricalData(props); + expect(apmRestServices.loadAgentStatus).not.toHaveBeenCalled(); + }); + + it('should leave historical data state as true if data is found', async () => { + const props = { + serviceList: { + status: STATUS.SUCCESS, + data: [] + } + }; + await instance.checkForHistoricalData(props); + expect(wrapper.state('historicalDataFound')).toEqual(true); + }); + + it('should set historical data state to false if data is NOT found', async () => { + const props = { + serviceList: { + status: STATUS.SUCCESS, + data: [] + } + }; + mockAgentStatus.dataFound = false; + await instance.checkForHistoricalData(props); + expect(wrapper.state('historicalDataFound')).toEqual(false); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.js.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.js.snap new file mode 100644 index 00000000000000..19bf212986f01e --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.js.snap @@ -0,0 +1,57 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Service Overview -> View should render when historical data is found 1`] = ` +
+ +

+ Services +

+ +
+ + +
+`; + +exports[`Service Overview -> View should render when historical data is found 2`] = ` +Object { + "items": Array [], + "noItemsMessage": , +} +`; + +exports[`Service Overview -> View should render when historical data is not found 1`] = ` +
+ +

+ Services +

+ +
+ + +
+`; + +exports[`Service Overview -> View should render when historical data is not found 2`] = ` +Object { + "items": Array [], + "noItemsMessage": + } + />, +} +`; diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/view.js b/x-pack/plugins/apm/public/components/app/ServiceOverview/view.js index bb86d9d40e2903..f8de5290d21bc9 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/view.js +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/view.js @@ -35,8 +35,9 @@ class ServiceOverview extends Component { this.checkForHistoricalData(this.props); } - componentWillReceiveProps(nextProps) { - this.checkForHistoricalData(nextProps); + componentDidUpdate() { + // QUESTION: Do we want to check on ANY update, or only if serviceList status/data have changed? + this.checkForHistoricalData(this.props); } render() { diff --git a/x-pack/plugins/apm/public/components/shared/ConnectRouterToRedux/view.js b/x-pack/plugins/apm/public/components/shared/ConnectRouterToRedux/view.js index 37115d06230653..8543dabe34f8b1 100644 --- a/x-pack/plugins/apm/public/components/shared/ConnectRouterToRedux/view.js +++ b/x-pack/plugins/apm/public/components/shared/ConnectRouterToRedux/view.js @@ -23,8 +23,10 @@ class ConnectRouterToRedux extends Component { this.props.updateLocation(this.props.location); } - componentWillReceiveProps(nextProps) { - this.props.updateLocation(nextProps.location); + componentDidUpdate() { + // this component is wrapped in a react-router Route to get access + // to the location prop, so no need to check for prop change here + this.props.updateLocation(this.props.location); } render() { diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestions.js b/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestions.js index 8df5168471336e..7060be264b7bdb 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestions.js +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestions.js @@ -10,12 +10,14 @@ import styled from 'styled-components'; import { isEmpty } from 'lodash'; import Suggestion from './Suggestion'; import { units, colors, px, unit } from '../../../../style/variables'; +import { rgba } from 'polished'; const List = styled.ul` width: 100%; border: 1px solid ${colors.gray4}; border-radius: ${px(units.quarter)}; - box-shadow: 0px ${px(units.quarter)} ${px(units.double)} rgba(0, 0, 0, 0.1); + box-shadow: 0px ${px(units.quarter)} ${px(units.double)} + ${rgba(colors.black, 0.1)}; position: absolute; background: #fff; z-index: 10; diff --git a/x-pack/plugins/apm/public/components/shared/Modal.js b/x-pack/plugins/apm/public/components/shared/Modal.js index f588b015e856a5..07e8d9ada99613 100644 --- a/x-pack/plugins/apm/public/components/shared/Modal.js +++ b/x-pack/plugins/apm/public/components/shared/Modal.js @@ -9,7 +9,8 @@ import Portal from 'react-portal'; import PropTypes from 'prop-types'; import styled from 'styled-components'; import { Close } from './Icons'; -import { fontSizes, units } from '../../style/variables'; +import { fontSizes, units, colors } from '../../style/variables'; +import { rgba } from 'polished'; const Header = styled.div` display: flex; @@ -36,7 +37,7 @@ const ModalFixed = styled.div` const ModalOverlay = styled(ModalFixed)` z-index: 10; - background: rgba(45, 45, 45, 0.8); + background: ${rgba(colors.gray2, 0.8)}; height: 100%; `; diff --git a/x-pack/plugins/apm/public/components/shared/PropertiesTable/index.js b/x-pack/plugins/apm/public/components/shared/PropertiesTable/index.js index 49112653217860..329b9e4941f734 100644 --- a/x-pack/plugins/apm/public/components/shared/PropertiesTable/index.js +++ b/x-pack/plugins/apm/public/components/shared/PropertiesTable/index.js @@ -6,7 +6,6 @@ import React from 'react'; import styled from 'styled-components'; -import { KuiTableInfo } from '@kbn/ui-framework/components'; import _ from 'lodash'; import STATIC_PROPS from './staticProperties.json'; import { @@ -58,10 +57,12 @@ const Cell = styled.td` } `; -const TableInfo = styled(KuiTableInfo)` +const TableInfo = styled.div` padding: ${px(unit)} 0 0; text-align: center; font-size: ${fontSize}; + color: ${colors.gray2}; + line-height: 1.5; `; const EmptyValue = styled.span` diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/index.js b/x-pack/plugins/apm/public/components/shared/Stacktrace/index.js index 6aeb3b0e6a857a..2e93f5c17c0e3b 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/index.js +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/index.js @@ -45,7 +45,7 @@ class Stacktrace extends PureComponent { libraryframes: {} }; - componentWillMount() { + componentDidMount() { if (!this.props.stackframes) { // Don't do anything, if there are no stackframes return false; diff --git a/x-pack/plugins/apm/public/store/selectors/chartSelectors.js b/x-pack/plugins/apm/public/store/selectors/chartSelectors.js index ef5d918476a0f0..cc898d6b667799 100644 --- a/x-pack/plugins/apm/public/store/selectors/chartSelectors.js +++ b/x-pack/plugins/apm/public/store/selectors/chartSelectors.js @@ -12,6 +12,7 @@ import { asDecimal, tpmUnit } from '../../utils/formatters'; +import { rgba } from 'polished'; export const getEmptySerie = memoize( (start = Date.now() - 3600000, end = Date.now()) => { @@ -91,7 +92,7 @@ export function getResponseTimeSeries(chartsData) { ), type: 'area', color: 'none', - areaColor: 'rgba(49, 133, 252, 0.1)' // apmBlue + areaColor: rgba(colors.apmBlue, 0.1) }); series.splice(1, 0, { @@ -105,7 +106,7 @@ export function getResponseTimeSeries(chartsData) { ), type: 'areaMaxHeight', color: 'none', - areaColor: 'rgba(146, 0, 0, 0.1)' // apmRed + areaColor: rgba(colors.apmRed, 0.1) }); } diff --git a/x-pack/plugins/apm/readme.md b/x-pack/plugins/apm/readme.md index d0545109dcb99c..844b914cb69064 100644 --- a/x-pack/plugins/apm/readme.md +++ b/x-pack/plugins/apm/readme.md @@ -1,5 +1,7 @@ # Documentation for APM in x-pack-kibana +_Note: Be sure to run the following commands from inside the `x-pack` directory._ + ### Format with Prettier ``` npx prettier "./plugins/apm/**/*.js" --write @@ -15,10 +17,9 @@ node scripts/jest.js plugins/apm --watch node scripts/jest.js plugins/apm --updateSnapshot ``` - -### Lint code +### Run ESLint (run from kibana root) ``` -npx eslint ./plugins/apm +./node_modules/.bin/eslint ./x-pack/plugins/apm ``` ### Ensure everything from master has been backported to 6.x diff --git a/x-pack/plugins/console_extensions/spec/generated/xpack.security.clear_cached_roles.json b/x-pack/plugins/console_extensions/spec/generated/xpack.security.clear_cached_roles.json index de47d676010ffd..47baad3ef989f1 100644 --- a/x-pack/plugins/console_extensions/spec/generated/xpack.security.clear_cached_roles.json +++ b/x-pack/plugins/console_extensions/spec/generated/xpack.security.clear_cached_roles.json @@ -6,6 +6,6 @@ "patterns": [ "_xpack/security/role/{name}/_clear_cache" ], - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-roles.html#security-api-clear-role-cache" + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-clear-role-cache.html" } } diff --git a/x-pack/plugins/console_extensions/spec/generated/xpack.security.delete_role.json b/x-pack/plugins/console_extensions/spec/generated/xpack.security.delete_role.json index be852122685dfb..ad95ac444ef987 100644 --- a/x-pack/plugins/console_extensions/spec/generated/xpack.security.delete_role.json +++ b/x-pack/plugins/console_extensions/spec/generated/xpack.security.delete_role.json @@ -13,6 +13,6 @@ "patterns": [ "_xpack/security/role/{name}" ], - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-roles.html#security-api-delete-role" + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-delete-role.html" } } diff --git a/x-pack/plugins/console_extensions/spec/generated/xpack.security.delete_role_mapping.json b/x-pack/plugins/console_extensions/spec/generated/xpack.security.delete_role_mapping.json index 5aa3a84333f631..0734c784b80f88 100644 --- a/x-pack/plugins/console_extensions/spec/generated/xpack.security.delete_role_mapping.json +++ b/x-pack/plugins/console_extensions/spec/generated/xpack.security.delete_role_mapping.json @@ -13,6 +13,6 @@ "patterns": [ "_xpack/security/role_mapping/{name}" ], - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-role-mapping.html#security-api-delete-role-mapping" + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-delete-role-mapping.html" } } diff --git a/x-pack/plugins/console_extensions/spec/generated/xpack.security.delete_user.json b/x-pack/plugins/console_extensions/spec/generated/xpack.security.delete_user.json index 6f7ce3da1d8e67..732721143503e1 100644 --- a/x-pack/plugins/console_extensions/spec/generated/xpack.security.delete_user.json +++ b/x-pack/plugins/console_extensions/spec/generated/xpack.security.delete_user.json @@ -13,6 +13,6 @@ "patterns": [ "_xpack/security/user/{username}" ], - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-users.html#security-api-delete-user" + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-delete-user.html" } } diff --git a/x-pack/plugins/console_extensions/spec/generated/xpack.security.disable_user.json b/x-pack/plugins/console_extensions/spec/generated/xpack.security.disable_user.json index 1799b8b34c02a3..b9151cf3685a10 100644 --- a/x-pack/plugins/console_extensions/spec/generated/xpack.security.disable_user.json +++ b/x-pack/plugins/console_extensions/spec/generated/xpack.security.disable_user.json @@ -14,6 +14,6 @@ "patterns": [ "_xpack/security/user/{username}/_disable" ], - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-users.html#security-api-disable-user" + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-disable-user.html" } } diff --git a/x-pack/plugins/console_extensions/spec/generated/xpack.security.enable_user.json b/x-pack/plugins/console_extensions/spec/generated/xpack.security.enable_user.json index 44d9cf47857c6e..12d1b3c7cf0bbb 100644 --- a/x-pack/plugins/console_extensions/spec/generated/xpack.security.enable_user.json +++ b/x-pack/plugins/console_extensions/spec/generated/xpack.security.enable_user.json @@ -14,6 +14,6 @@ "patterns": [ "_xpack/security/user/{username}/_enable" ], - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-users.html#security-api-enable-user" + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-enable-user.html" } } diff --git a/x-pack/plugins/console_extensions/spec/generated/xpack.security.get_role.json b/x-pack/plugins/console_extensions/spec/generated/xpack.security.get_role.json index b37c43af0c0c0c..e4b887e3d9329e 100644 --- a/x-pack/plugins/console_extensions/spec/generated/xpack.security.get_role.json +++ b/x-pack/plugins/console_extensions/spec/generated/xpack.security.get_role.json @@ -7,6 +7,6 @@ "_xpack/security/role/{name}", "_xpack/security/role" ], - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-roles.html#security-api-get-role" + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-get-role.html" } } diff --git a/x-pack/plugins/console_extensions/spec/generated/xpack.security.get_role_mapping.json b/x-pack/plugins/console_extensions/spec/generated/xpack.security.get_role_mapping.json index 60e51262fa38c5..45c935277c865d 100644 --- a/x-pack/plugins/console_extensions/spec/generated/xpack.security.get_role_mapping.json +++ b/x-pack/plugins/console_extensions/spec/generated/xpack.security.get_role_mapping.json @@ -7,6 +7,6 @@ "_xpack/security/role_mapping/{name}", "_xpack/security/role_mapping" ], - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-role-mapping.html#security-api-get-role-mapping" + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-get-role-mapping.html" } } diff --git a/x-pack/plugins/console_extensions/spec/generated/xpack.security.get_token.json b/x-pack/plugins/console_extensions/spec/generated/xpack.security.get_token.json index ca13d79832ec12..208d6e31346b72 100644 --- a/x-pack/plugins/console_extensions/spec/generated/xpack.security.get_token.json +++ b/x-pack/plugins/console_extensions/spec/generated/xpack.security.get_token.json @@ -6,6 +6,6 @@ "patterns": [ "_xpack/security/oauth2/token" ], - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-tokens.html#security-api-get-token" + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-get-token.html" } } diff --git a/x-pack/plugins/console_extensions/spec/generated/xpack.security.get_user.json b/x-pack/plugins/console_extensions/spec/generated/xpack.security.get_user.json index 77ad4667f66644..d42d1afbb91c9e 100644 --- a/x-pack/plugins/console_extensions/spec/generated/xpack.security.get_user.json +++ b/x-pack/plugins/console_extensions/spec/generated/xpack.security.get_user.json @@ -7,6 +7,6 @@ "_xpack/security/user/{username}", "_xpack/security/user" ], - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-users.html#security-api-get-user" + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-get-user.html" } } diff --git a/x-pack/plugins/console_extensions/spec/generated/xpack.security.invalidate_token.json b/x-pack/plugins/console_extensions/spec/generated/xpack.security.invalidate_token.json index 70434690fbda3b..e9ffac7bd5f920 100644 --- a/x-pack/plugins/console_extensions/spec/generated/xpack.security.invalidate_token.json +++ b/x-pack/plugins/console_extensions/spec/generated/xpack.security.invalidate_token.json @@ -6,6 +6,6 @@ "patterns": [ "_xpack/security/oauth2/token" ], - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-tokens.html#security-api-invalidate-token" + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-invalidate-token.html" } } diff --git a/x-pack/plugins/console_extensions/spec/generated/xpack.security.put_role.json b/x-pack/plugins/console_extensions/spec/generated/xpack.security.put_role.json index 4a0854b56cd13a..6cc3523f8b6471 100644 --- a/x-pack/plugins/console_extensions/spec/generated/xpack.security.put_role.json +++ b/x-pack/plugins/console_extensions/spec/generated/xpack.security.put_role.json @@ -14,6 +14,6 @@ "patterns": [ "_xpack/security/role/{name}" ], - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-roles.html#security-api-put-role" + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-role.html" } } diff --git a/x-pack/plugins/console_extensions/spec/generated/xpack.security.put_role_mapping.json b/x-pack/plugins/console_extensions/spec/generated/xpack.security.put_role_mapping.json index 6ce2034998ba50..7a426c420fffd7 100644 --- a/x-pack/plugins/console_extensions/spec/generated/xpack.security.put_role_mapping.json +++ b/x-pack/plugins/console_extensions/spec/generated/xpack.security.put_role_mapping.json @@ -14,6 +14,6 @@ "patterns": [ "_xpack/security/role_mapping/{name}" ], - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-role-mapping.html#security-api-put-role-mapping" + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-role-mapping.html" } } diff --git a/x-pack/plugins/console_extensions/spec/generated/xpack.security.put_user.json b/x-pack/plugins/console_extensions/spec/generated/xpack.security.put_user.json index 0b0b8244bed96d..19a13b37c7d91d 100644 --- a/x-pack/plugins/console_extensions/spec/generated/xpack.security.put_user.json +++ b/x-pack/plugins/console_extensions/spec/generated/xpack.security.put_user.json @@ -14,6 +14,6 @@ "patterns": [ "_xpack/security/user/{username}" ], - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-users.html#security-api-put-user" + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-user.html" } } diff --git a/x-pack/plugins/console_extensions/spec/overrides/xpack.security.put_user.json b/x-pack/plugins/console_extensions/spec/overrides/xpack.security.put_user.json index ed6b37643564c7..6aff00817e4943 100644 --- a/x-pack/plugins/console_extensions/spec/overrides/xpack.security.put_user.json +++ b/x-pack/plugins/console_extensions/spec/overrides/xpack.security.put_user.json @@ -6,6 +6,6 @@ "fullname": "", "roles": [] }, - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-users.html#security-api-put-user" + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-user.html" } } diff --git a/x-pack/plugins/dashboard_mode/public/index.scss b/x-pack/plugins/dashboard_mode/public/index.scss index d7b2f63f86e27f..940cb113b679f4 100644 --- a/x-pack/plugins/dashboard_mode/public/index.scss +++ b/x-pack/plugins/dashboard_mode/public/index.scss @@ -1,3 +1,3 @@ -@import '../../../../src/ui/public/styles/styling_constants'; +@import 'ui/public/styles/styling_constants'; -@import '../../../../src/core_plugins/kibana/public/dashboard/index'; +@import 'core_plugins/kibana/public/dashboard/index'; diff --git a/x-pack/plugins/graph/public/app.js b/x-pack/plugins/graph/public/app.js index 867279eddde888..577a537af3fef1 100644 --- a/x-pack/plugins/graph/public/app.js +++ b/x-pack/plugins/graph/public/app.js @@ -101,7 +101,7 @@ uiRoutes return savedGraphWorkspaces.get($route.current.params.id) .catch( function () { - notify.error('Missing workspace'); + toastNotifications.addDanger('Missing workspace'); } ); @@ -830,7 +830,7 @@ app.controller('graphuiPlugin', function ($scope, $route, $interval, $http, kbnU } }); if(!savedObjectIndexPattern) { - notify.error('Missing index pattern:' + wsObj.indexPattern); + toastNotifications.addDanger(`'Missing index pattern ${wsObj.indexPattern}`); return; } diff --git a/x-pack/plugins/index_management/public/sections/index_list/components/index_actions_context_menu/index_actions_context_menu.js b/x-pack/plugins/index_management/public/sections/index_list/components/index_actions_context_menu/index_actions_context_menu.js index da6446aee26b96..9c0a381a1d19c8 100644 --- a/x-pack/plugins/index_management/public/sections/index_list/components/index_actions_context_menu/index_actions_context_menu.js +++ b/x-pack/plugins/index_management/public/sections/index_list/components/index_actions_context_menu/index_actions_context_menu.js @@ -376,7 +376,7 @@ class IndexActionsContextMenuUi extends Component {

@@ -467,4 +467,4 @@ class IndexActionsContextMenuUi extends Component { } } -export const IndexActionsContextMenu = injectI18n(IndexActionsContextMenuUi); \ No newline at end of file +export const IndexActionsContextMenu = injectI18n(IndexActionsContextMenuUi); diff --git a/x-pack/plugins/index_management/public/sections/index_list/components/index_table/index_table.js b/x-pack/plugins/index_management/public/sections/index_list/components/index_table/index_table.js index c34ffdacf74dc2..49ba4ef3bedb26 100644 --- a/x-pack/plugins/index_management/public/sections/index_list/components/index_table/index_table.js +++ b/x-pack/plugins/index_management/public/sections/index_list/components/index_table/index_table.js @@ -68,6 +68,24 @@ const HEADERS = { }; export class IndexTableUi extends Component { + static getDerivedStateFromProps(props, state) { + // Deselct any indices which no longer exist, e.g. they've been deleted. + const { selectedIndicesMap } = state; + const indexNames = props.indices.map(index => index.name); + const selectedIndexNames = Object.keys(selectedIndicesMap); + const missingIndexNames = selectedIndexNames.filter(selectedIndexName => { + return !indexNames.includes(selectedIndexName); + }); + + if (missingIndexNames.length) { + const newMap = { ...selectedIndicesMap }; + missingIndexNames.forEach(missingIndexName => delete newMap[missingIndexName]); + return { selectedIndicesMap: newMap }; + } + + return null; + } + constructor(props) { super(props); @@ -82,6 +100,7 @@ export class IndexTableUi extends Component { const newIsSortAscending = sortField === column ? !isSortAscending : true; sortChanged(column, newIsSortAscending); }; + toggleAll = () => { const allSelected = this.areAllItemsSelected(); if (allSelected) { @@ -96,6 +115,7 @@ export class IndexTableUi extends Component { selectedIndicesMap }); }; + toggleItem = name => { this.setState(({ selectedIndicesMap }) => { const newMap = { ...selectedIndicesMap }; @@ -109,6 +129,7 @@ export class IndexTableUi extends Component { }; }); }; + isItemSelected = name => { return !!this.state.selectedIndicesMap[name]; }; @@ -159,6 +180,7 @@ export class IndexTableUi extends Component { } return value; } + buildRowCells(index) { return Object.keys(HEADERS).map(fieldName => { const { name } = index; @@ -174,6 +196,7 @@ export class IndexTableUi extends Component { ); }); } + buildRows() { const { indices = [], detailPanelIndexName } = this.props; return indices.map(index => { @@ -221,7 +244,6 @@ export class IndexTableUi extends Component { }; render() { - const { filterChanged, filter, @@ -336,4 +358,4 @@ export class IndexTableUi extends Component { } } -export const IndexTable = injectI18n(IndexTableUi); \ No newline at end of file +export const IndexTable = injectI18n(IndexTableUi); diff --git a/x-pack/plugins/ml/public/components/rule_editor/__snapshots__/condition_expression.test.js.snap b/x-pack/plugins/ml/public/components/rule_editor/__snapshots__/condition_expression.test.js.snap index 1f69cc7657c1d9..640f5fbf5e5d88 100644 --- a/x-pack/plugins/ml/public/components/rule_editor/__snapshots__/condition_expression.test.js.snap +++ b/x-pack/plugins/ml/public/components/rule_editor/__snapshots__/condition_expression.test.js.snap @@ -25,6 +25,7 @@ exports[`ConditionExpression renders with appliesTo, operator and value supplied /> } closePopover={[Function]} + hasArrow={true} id="appliesToPopover" isOpen={false} ownFocus={true} @@ -91,6 +92,7 @@ exports[`ConditionExpression renders with appliesTo, operator and value supplied /> } closePopover={[Function]} + hasArrow={true} id="operatorValuePopover" isOpen={false} ownFocus={true} @@ -223,6 +225,7 @@ exports[`ConditionExpression renders with only value supplied 1`] = ` /> } closePopover={[Function]} + hasArrow={true} id="appliesToPopover" isOpen={false} ownFocus={true} @@ -288,6 +291,7 @@ exports[`ConditionExpression renders with only value supplied 1`] = ` /> } closePopover={[Function]} + hasArrow={true} id="operatorValuePopover" isOpen={false} ownFocus={true} diff --git a/x-pack/plugins/ml/public/components/rule_editor/__snapshots__/scope_expression.test.js.snap b/x-pack/plugins/ml/public/components/rule_editor/__snapshots__/scope_expression.test.js.snap index dcbec7d00959be..10cf78eb68d1b6 100644 --- a/x-pack/plugins/ml/public/components/rule_editor/__snapshots__/scope_expression.test.js.snap +++ b/x-pack/plugins/ml/public/components/rule_editor/__snapshots__/scope_expression.test.js.snap @@ -90,6 +90,7 @@ exports[`ScopeExpression renders when enabled set to false 1`] = ` /> } closePopover={[Function]} + hasArrow={true} id="operatorValuePopover" isOpen={false} ownFocus={true} @@ -244,6 +245,7 @@ exports[`ScopeExpression renders when filter ID and type supplied 1`] = ` /> } closePopover={[Function]} + hasArrow={true} id="operatorValuePopover" isOpen={false} ownFocus={true} @@ -398,6 +400,7 @@ exports[`ScopeExpression renders when no filter ID or type supplied 1`] = ` /> } closePopover={[Function]} + hasArrow={true} id="operatorValuePopover" isOpen={false} ownFocus={true} diff --git a/x-pack/plugins/ml/public/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap b/x-pack/plugins/ml/public/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap index fc98b2e69df753..f2b45cd4d0cbd4 100644 --- a/x-pack/plugins/ml/public/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap +++ b/x-pack/plugins/ml/public/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap @@ -18,6 +18,7 @@ exports[`AddItemPopover calls addItems with multiple items on clicking Add butto } closePopover={[Function]} + hasArrow={true} id="add_item_popover" isOpen={false} ownFocus={true} @@ -95,6 +96,7 @@ exports[`AddItemPopover opens the popover onButtonClick 1`] = ` } closePopover={[Function]} + hasArrow={true} id="add_item_popover" isOpen={true} ownFocus={true} @@ -172,6 +174,7 @@ exports[`AddItemPopover renders the popover 1`] = ` } closePopover={[Function]} + hasArrow={true} id="add_item_popover" isOpen={false} ownFocus={true} diff --git a/x-pack/plugins/ml/public/settings/filter_lists/components/edit_description_popover/__snapshots__/edit_description_popover.test.js.snap b/x-pack/plugins/ml/public/settings/filter_lists/components/edit_description_popover/__snapshots__/edit_description_popover.test.js.snap index e59216401033bf..401b5a9337032d 100644 --- a/x-pack/plugins/ml/public/settings/filter_lists/components/edit_description_popover/__snapshots__/edit_description_popover.test.js.snap +++ b/x-pack/plugins/ml/public/settings/filter_lists/components/edit_description_popover/__snapshots__/edit_description_popover.test.js.snap @@ -15,6 +15,7 @@ exports[`FilterListUsagePopover opens the popover onButtonClick 1`] = ` /> } closePopover={[Function]} + hasArrow={true} id="filter_list_description_popover" isOpen={true} ownFocus={true} @@ -64,6 +65,7 @@ exports[`FilterListUsagePopover renders the popover with a description 1`] = ` /> } closePopover={[Function]} + hasArrow={true} id="filter_list_description_popover" isOpen={false} ownFocus={true} @@ -113,6 +115,7 @@ exports[`FilterListUsagePopover renders the popover with no description 1`] = ` /> } closePopover={[Function]} + hasArrow={true} id="filter_list_description_popover" isOpen={false} ownFocus={true} diff --git a/x-pack/plugins/ml/public/settings/filter_lists/components/filter_list_usage_popover/__snapshots__/filter_list_usage_popover.test.js.snap b/x-pack/plugins/ml/public/settings/filter_lists/components/filter_list_usage_popover/__snapshots__/filter_list_usage_popover.test.js.snap index d66c33bf095e96..a954fc65a4039f 100644 --- a/x-pack/plugins/ml/public/settings/filter_lists/components/filter_list_usage_popover/__snapshots__/filter_list_usage_popover.test.js.snap +++ b/x-pack/plugins/ml/public/settings/filter_lists/components/filter_list_usage_popover/__snapshots__/filter_list_usage_popover.test.js.snap @@ -16,6 +16,7 @@ exports[`FilterListUsagePopover opens the popover onButtonClick 1`] = ` } closePopover={[Function]} + hasArrow={true} id="detector_filter_list_usage" isOpen={true} ownFocus={true} @@ -59,6 +60,7 @@ exports[`FilterListUsagePopover renders the popover for 1 job 1`] = ` } closePopover={[Function]} + hasArrow={true} id="job_filter_list_usage" isOpen={false} ownFocus={true} @@ -92,6 +94,7 @@ exports[`FilterListUsagePopover renders the popover for 2 detectors 1`] = ` } closePopover={[Function]} + hasArrow={true} id="detector_filter_list_usage" isOpen={false} ownFocus={true} diff --git a/x-pack/plugins/monitoring/common/constants.js b/x-pack/plugins/monitoring/common/constants.js index 30b700c48ae0eb..718606d8b6be27 100644 --- a/x-pack/plugins/monitoring/common/constants.js +++ b/x-pack/plugins/monitoring/common/constants.js @@ -22,7 +22,7 @@ export const MONITORING_SYSTEM_API_VERSION = '6'; * The type name used within the Monitoring index to publish Kibana ops stats. * @type {string} */ -export const KIBANA_STATS_TYPE_MONITORING = 'kibana_stats_monitoring'; // similar to KIBANA_STATS_TYPE but rolled up into 10s stats from 5s intervals through ops_buffer +export const KIBANA_STATS_TYPE_MONITORING = 'kibana_stats'; // similar to KIBANA_STATS_TYPE but rolled up into 10s stats from 5s intervals through ops_buffer /** * The type name used within the Monitoring index to publish Kibana stats. * @type {string} diff --git a/x-pack/plugins/monitoring/public/index.scss b/x-pack/plugins/monitoring/public/index.scss index 1bec333e15b4ce..67974b71cd72ff 100644 --- a/x-pack/plugins/monitoring/public/index.scss +++ b/x-pack/plugins/monitoring/public/index.scss @@ -1,5 +1,5 @@ // Import the EUI global scope so we can use EUI constants -@import '../../../../src/ui/public/styles/_styling_constants'; +@import 'ui/public/styles/_styling_constants'; // Temporary hacks @import 'hacks'; diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/__tests__/bulk_uploader.js b/x-pack/plugins/monitoring/server/kibana_monitoring/__tests__/bulk_uploader.js index 0b69370b8e4c1e..fe17f205c65b95 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/__tests__/bulk_uploader.js +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/__tests__/bulk_uploader.js @@ -14,8 +14,15 @@ const CHECK_DELAY = 500; class MockCollectorSet { constructor(_mockServer, mockCollectors) { + this.mockServer = _mockServer; this.mockCollectors = mockCollectors; } + getCollectorByType(type) { + return this.mockCollectors.find(collector => collector.type === type) || this.mockCollectors[0]; + } + getFilteredCollectorSet(filter) { + return new MockCollectorSet(this.mockServer, this.mockCollectors.filter(filter)); + } async bulkFetch() { return this.mockCollectors.map(({ fetch }) => fetch()); } @@ -47,7 +54,8 @@ describe('BulkUploader', () => { const collectors = new MockCollectorSet(server, [ { type: 'type_collector_test', - fetch: noop, // empty payloads + fetch: noop, // empty payloads, + formatForBulkUpload: result => result, } ]); @@ -82,7 +90,10 @@ describe('BulkUploader', () => { it('should run the bulk upload handler', done => { const collectors = new MockCollectorSet(server, [ - { fetch: () => ({ type: 'type_collector_test', result: { testData: 12345 } }) } + { + fetch: () => ({ type: 'type_collector_test', result: { testData: 12345 } }), + formatForBulkUpload: result => result + } ]); const uploader = new BulkUploader(server, { interval: FETCH_INTERVAL diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.combine_stats_legacy.test.js b/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.combine_stats_legacy.test.js deleted file mode 100644 index 2ecd9a8329eab4..00000000000000 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.combine_stats_legacy.test.js +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { KIBANA_STATS_TYPE_MONITORING, KIBANA_USAGE_TYPE, KIBANA_SETTINGS_TYPE } from '../../common/constants'; -import { KIBANA_REPORTING_TYPE } from '../../../reporting/common/constants'; -import { BulkUploader } from './bulk_uploader'; - -const getInitial = () => { - return [ - [ - { 'index': { '_type': KIBANA_STATS_TYPE_MONITORING } }, - { - 'host': 'tsullivan.local', - 'concurrent_connections': 0, - 'os': { - 'load': { '1m': 2.28857421875, '5m': 2.45068359375, '15m': 2.29248046875 }, - 'memory': { 'total_in_bytes': 17179869184, 'free_in_bytes': 527749120, 'used_in_bytes': 16652120064 }, - 'uptime_in_millis': 1211027000 - }, - 'process': { - 'event_loop_delay': 4.222616970539093, - 'memory': { - 'heap': { 'total_in_bytes': 219455488, 'used_in_bytes': 152622064, 'size_limit': 1501560832 }, - 'resident_set_size_in_bytes': 245923840 - }, - 'uptime_in_millis': 18467 - }, - 'requests': { - 'disconnects': 0, - 'total': 2, - }, - 'response_times': { 'average': 47, 'max': 47 }, - 'timestamp': '2017-07-26T00:14:20.771Z', - } - ], - [ - { 'index': { '_type': KIBANA_USAGE_TYPE } }, - { - 'dashboard': { 'total': 0 }, - 'visualization': { 'total': 0 }, - 'search': { 'total': 0 }, - 'index_pattern': { 'total': 2 }, - 'index': '.kibana' - } - ], - [ - { 'index': { '_type': KIBANA_REPORTING_TYPE } }, - { - 'available': true, - 'enabled': false, - '_all': 55, - 'csv': { - 'available': true, - 'count': 25 - }, - 'printable_pdf': { - 'available': true, - 'count': 30 - } - } - ], - [ - { 'index': { '_type': KIBANA_SETTINGS_TYPE } }, - { 'xpack': { 'defaultAdminEmail': 'tim@elastic.co' } } - ] - ]; -}; - -// TODO use jest snapshotting -const getResult = () => { - return [ - [ - { 'index': { '_type': 'kibana_stats' } }, - { - 'host': 'tsullivan.local', - 'concurrent_connections': 0, - 'os': { - 'load': { '1m': 2.28857421875, '5m': 2.45068359375, '15m': 2.29248046875 }, - 'memory': { 'total_in_bytes': 17179869184, 'free_in_bytes': 527749120, 'used_in_bytes': 16652120064 }, - 'uptime_in_millis': 1211027000 - }, - 'process': { - 'event_loop_delay': 4.222616970539093, - 'memory': { - 'heap': { 'total_in_bytes': 219455488, 'used_in_bytes': 152622064, 'size_limit': 1501560832 }, - 'resident_set_size_in_bytes': 245923840 - }, - 'uptime_in_millis': 18467 - }, - 'requests': { - 'disconnects': 0, - 'total': 2, - }, - 'response_times': { 'average': 47, 'max': 47 }, - 'timestamp': '2017-07-26T00:14:20.771Z', - 'usage': { - 'dashboard': { 'total': 0 }, - 'visualization': { 'total': 0 }, - 'search': { 'total': 0 }, - 'index_pattern': { 'total': 2 }, - 'index': '.kibana', - 'xpack': { - 'reporting': { - '_all': 55, - 'available': true, - 'csv': { - 'available': true, - 'count': 25, - }, - 'enabled': false, - 'printable_pdf': { - 'available': true, - 'count': 30, - } - } - } - } - } - ], - [ - { 'index': { '_type': 'kibana_settings' } }, - { - 'xpack': { 'defaultAdminEmail': 'tim@elastic.co' }, - } - ] - ]; -}; - -describe('Collector Types Combiner', () => { - describe('with all the data types present', () => { - it('provides settings, and combined stats/usage data', () => { - // default gives all the data types - const initial = getInitial(); - const result = BulkUploader.combineStatsLegacy(initial); - expect(result).toEqual(getResult()); - }); - }); - describe('with settings data missing', () => { - it('provides combined stats/usage data', () => { - // default gives all the data types - const initial = getInitial(); - const trimmedInitial = [ initial[0], initial[1], initial[2] ]; // just stats, usage and reporting, no settings - const result = BulkUploader.combineStatsLegacy(trimmedInitial); - const expectedResult = getResult(); - const trimmedExpectedResult = [ expectedResult[0] ]; // single combined item - expect(result).toEqual(trimmedExpectedResult); - }); - }); - describe('with usage data missing', () => { - it('provides settings, and stats data', () => { - // default gives all the data types - const initial = getInitial(); - const trimmedInitial = [ initial[0], initial[3] ]; // just stats and settings, no usage or reporting - const result = BulkUploader.combineStatsLegacy(trimmedInitial); - const expectedResult = getResult(); - delete expectedResult[0][1].usage; // usage stats should not be present in the result - const trimmedExpectedResult = [ expectedResult[0], expectedResult[1] ]; - expect(result).toEqual(trimmedExpectedResult); - }); - }); - describe('with stats data missing', () => { - it('provides settings data', () => { - // default gives all the data types - const initial = getInitial(); - const trimmedInitial = [ initial[3] ]; // just settings - const result = BulkUploader.combineStatsLegacy(trimmedInitial); - const expectedResult = getResult(); - const trimmedExpectedResult = [ expectedResult[1] ]; // just settings - expect(result).toEqual(trimmedExpectedResult); - }); - }); - - it('throws an error if duplicate types are registered', () => { - const combineWithDuplicate = () => { - const initial = getInitial(); - const withDuplicate = [ initial[0] ].concat(initial); - return BulkUploader.combineStatsLegacy(withDuplicate); - }; - expect(combineWithDuplicate).toThrow( - 'Duplicate collector type identifiers found in payload! ' + - 'kibana_stats_monitoring,kibana_stats_monitoring,kibana,reporting,kibana_settings' - ); - }); -}); diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js b/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js index c95baa056c8d6a..b838ada1a86ca0 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js @@ -4,19 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get, set, isEmpty, flatten, uniq } from 'lodash'; +import { defaultsDeep, isEmpty, uniq, compact } from 'lodash'; import { callClusterFactory } from '../../../xpack_main'; import { LOGGING_TAG, KIBANA_MONITORING_LOGGING_TAG, - KIBANA_STATS_TYPE_MONITORING, - KIBANA_SETTINGS_TYPE, - KIBANA_USAGE_TYPE, } from '../../common/constants'; -import { KIBANA_REPORTING_TYPE } from '../../../reporting/common/constants'; import { sendBulkPayload, monitoringBulk, + getKibanaInfoForStats, } from './lib'; const LOGGING_TAGS = [LOGGING_TAG, KIBANA_MONITORING_LOGGING_TAG]; @@ -38,7 +35,7 @@ const LOGGING_TAGS = [LOGGING_TAG, KIBANA_MONITORING_LOGGING_TAG]; * @param {Object} xpackInfo server.plugins.xpack_main.info object */ export class BulkUploader { - constructor(server, { interval }) { + constructor(server, { kbnServer, interval }) { if (typeof interval !== 'number') { throw new Error('interval number of milliseconds is required'); } @@ -56,7 +53,7 @@ export class BulkUploader { }); this._callClusterWithInternalUser = callClusterFactory(server).getCallClusterInternal(); - + this._getKibanaInfoForStats = () => getKibanaInfoForStats(server, kbnServer); } /* @@ -66,9 +63,12 @@ export class BulkUploader { */ start(collectorSet) { this._log.info('Starting monitoring stats collection'); - this._fetchAndUpload(collectorSet); // initial fetch + + // this is internal bulk upload, so filter out API-only collectors + const filterThem = _collectorSet => _collectorSet.getFilteredCollectorSet(c => c.ignoreForInternalUploader !== true); + this._fetchAndUpload(filterThem(collectorSet)); // initial fetch this._timer = setInterval(() => { - this._fetchAndUpload(collectorSet); + this._fetchAndUpload(filterThem(collectorSet)); }, this._interval); } @@ -98,7 +98,7 @@ export class BulkUploader { */ async _fetchAndUpload(collectorSet) { const data = await collectorSet.bulkFetch(this._callClusterWithInternalUser); - const payload = BulkUploader.toBulkUploadFormat(data); + const payload = this.toBulkUploadFormat(compact(data), collectorSet); if (payload) { try { @@ -120,15 +120,69 @@ export class BulkUploader { /* * Bulk stats are transformed into a bulk upload format * Non-legacy transformation is done in CollectorSet.toApiStats + * + * Example: + * Before: + * [ + * { + * "type": "kibana_stats", + * "result": { + * "process": { ... }, + * "requests": { ... }, + * ... + * } + * }, + * ] + * + * After: + * [ + * { + * "index": { + * "_type": "kibana_stats" + * } + * }, + * { + * "kibana": { + * "host": "localhost", + * "uuid": "d619c5d1-4315-4f35-b69d-a3ac805489fb", + * "version": "7.0.0-alpha1", + * ... + * }, + * "process": { ... }, + * "requests": { ... }, + * ... + * } + * ] */ - static toBulkUploadFormat(uploadData) { - const payload = uploadData - .filter(d => Boolean(d) && !isEmpty(d.result)) - .map(({ result, type }) => [{ index: { _type: type } }, result]); - if (payload.length > 0) { - const combinedData = BulkUploader.combineStatsLegacy(payload); // arrange the usage data into the stats - return flatten(combinedData); + toBulkUploadFormat(rawData, collectorSet) { + if (rawData.length === 0) { + return; } + + // convert the raw data to a nested object by taking each payload through + // its formatter, organizing it per-type + const typesNested = rawData.reduce((accum, { type, result }) => { + if (isEmpty(result)) { + return accum; + } + const { type: uploadType, payload: uploadData } = collectorSet.getCollectorByType(type).formatForBulkUpload(result); + return defaultsDeep(accum, { [uploadType]: uploadData }); + }, {}); + + // convert the nested object into a flat array, with each payload prefixed + // with an 'index' instruction, for bulk upload + const flat = Object.keys(typesNested).reduce((accum, type) => { + return [ + ...accum, + { index: { _type: type } }, + { + kibana: this._getKibanaInfoForStats(), + ...typesNested[type], + } + ]; + }, []); + + return flat; } static checkPayloadTypesUnique(payload) { @@ -138,45 +192,4 @@ export class BulkUploader { throw new Error('Duplicate collector type identifiers found in payload! ' + ids.join(',')); } } - - static combineStatsLegacy(payload) { - BulkUploader.checkPayloadTypesUnique(payload); - - // default the item to [] to allow destructuring - const findItem = type => payload.find(item => get(item, '[0].index._type') === type) || []; - - // kibana usage and stats - let statsResult; - const [ statsHeader, statsPayload ] = findItem(KIBANA_STATS_TYPE_MONITORING); - const [ reportingHeader, reportingPayload ] = findItem(KIBANA_REPORTING_TYPE); - - if (statsHeader && statsPayload) { - statsHeader.index._type = 'kibana_stats'; // HACK to convert kibana_stats_monitoring to just kibana_stats for bwc - const [ usageHeader, usagePayload ] = findItem(KIBANA_USAGE_TYPE); - const kibanaUsage = (usageHeader && usagePayload) ? usagePayload : null; - const reportingUsage = (reportingHeader && reportingPayload) ? reportingPayload : null; - statsResult = [ statsHeader, statsPayload ]; - if (kibanaUsage) { - set(statsResult, '[1].usage', kibanaUsage); - } - if (reportingUsage) { - set(statsResult, '[1].usage.xpack.reporting', reportingUsage); - } - } - - // kibana settings - let settingsResult; - const [ settingsHeader, settingsPayload ] = findItem(KIBANA_SETTINGS_TYPE); - if (settingsHeader && settingsPayload) { - settingsResult = [ settingsHeader, settingsPayload ]; - } - - // return new payload with the combined data - // adds usage data to stats data - // strips usage out as a top-level type - const result = [ statsResult, settingsResult ]; - - // remove result items that are undefined - return result.filter(Boolean); - } } diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_kibana_usage_collector.js b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_kibana_usage_collector.js index 2742ea6da5714c..a81db5aca586a1 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_kibana_usage_collector.js +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_kibana_usage_collector.js @@ -5,7 +5,7 @@ */ import { get, snakeCase } from 'lodash'; -import { KIBANA_USAGE_TYPE } from '../../../common/constants'; +import { KIBANA_USAGE_TYPE, KIBANA_STATS_TYPE_MONITORING } from '../../../common/constants'; const TYPES = [ 'dashboard', @@ -17,12 +17,13 @@ const TYPES = [ ]; /** - * Fetches saved object client counts by querying the saved object index + * Fetches saved object counts by querying the .kibana index */ export function getKibanaUsageCollector(server) { const { collectorSet } = server.usage; return collectorSet.makeUsageCollector({ type: KIBANA_USAGE_TYPE, + async fetch(callCluster) { const index = server.config().get('kibana.index'); const savedObjectCountSearchParams = { @@ -60,6 +61,20 @@ export function getKibanaUsageCollector(server) { } }), {}) }; + }, + + /* + * Format the response data into a model for internal upload + * 1. Make this data part of the "kibana_stats" type + * 2. Organize the payload in the usage namespace of the data payload (usage.index, etc) + */ + formatForBulkUpload: result => { + return { + type: KIBANA_STATS_TYPE_MONITORING, + payload: { + usage: result + } + }; } }); } diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_ops_stats_collector.js b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_ops_stats_collector.js index 1c79de29dea407..baad3916934513 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_ops_stats_collector.js +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_ops_stats_collector.js @@ -6,12 +6,11 @@ import { KIBANA_STATS_TYPE_MONITORING } from '../../../common/constants'; import { opsBuffer } from './ops_buffer'; -import { getKibanaInfoForStats } from '../lib'; /* * Initialize a collector for Kibana Ops Stats */ -export function getOpsStatsCollector(server, kbnServer) { +export function getOpsStatsCollector(server) { let monitor; const buffer = opsBuffer(server); const onOps = event => buffer.push(event); @@ -48,10 +47,7 @@ export function getOpsStatsCollector(server, kbnServer) { type: KIBANA_STATS_TYPE_MONITORING, init: start, fetch: () => { - return { - kibana: getKibanaInfoForStats(server, kbnServer), - ...buffer.flush() - }; + return buffer.flush(); } }); } diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.js b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.js index 401ee9f4766cb2..c0793a4bb6b9d0 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.js +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.js @@ -7,7 +7,6 @@ import { get } from 'lodash'; import { XPACK_DEFAULT_ADMIN_EMAIL_UI_SETTING } from '../../../../../server/lib/constants'; import { KIBANA_SETTINGS_TYPE } from '../../../common/constants'; -import { getKibanaInfoForStats } from '../lib'; /* * Check if Cluster Alert email notifications is enabled in config @@ -54,23 +53,19 @@ export async function checkForEmailValue( } } -export function getSettingsCollector(server, kbnServer) { +export function getSettingsCollector(server) { const config = server.config(); - const { collectorSet } = server.usage; + return collectorSet.makeStatsCollector({ type: KIBANA_SETTINGS_TYPE, async fetch(callCluster) { - let kibanaSettingsData = null; + let kibanaSettingsData; const defaultAdminEmail = await checkForEmailValue(config, callCluster); // skip everything if defaultAdminEmail === undefined if (defaultAdminEmail || (defaultAdminEmail === null && shouldUseNull)) { - kibanaSettingsData = { - xpack: { - default_admin_email: defaultAdminEmail - } - }; + kibanaSettingsData = this.getEmailValueStructure(defaultAdminEmail); this.log.debug(`[${defaultAdminEmail}] default admin email setting found, sending [${KIBANA_SETTINGS_TYPE}] monitoring document.`); } else { this.log.debug(`not sending [${KIBANA_SETTINGS_TYPE}] monitoring document because [${defaultAdminEmail}] is null or invalid.`); @@ -79,16 +74,15 @@ export function getSettingsCollector(server, kbnServer) { // remember the current email so that we can mark it as successful if the bulk does not error out shouldUseNull = !!defaultAdminEmail; - // return nothing when there was no result - let settingsDoc; - if (kibanaSettingsData !== null) { - settingsDoc = { - kibana: getKibanaInfoForStats(server, kbnServer), - ...kibanaSettingsData - }; - } - - return settingsDoc; + // returns undefined if there was no result + return kibanaSettingsData; + }, + getEmailValueStructure(email) { + return { + xpack: { + default_admin_email: email + } + }; } }); } diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/ops_buffer/ops_buffer.js b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/ops_buffer/ops_buffer.js index bb7683899200c4..7799f27c87bd8a 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/ops_buffer/ops_buffer.js +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/ops_buffer/ops_buffer.js @@ -27,8 +27,14 @@ export function opsBuffer(server) { }, flush() { + let cloud; // a property that will be left out of the result if the details are undefined + const cloudDetails = cloudDetector.getCloudDetails(); + if (cloudDetails != null) { + cloud = { cloud: cloudDetails }; + } + return { - cloud: cloudDetector.getCloudDetails(), + ...cloud, ...eventRoller.flush() }; } diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/init.js b/x-pack/plugins/monitoring/server/kibana_monitoring/init.js index 6dcb0ae1fc076c..90d9a6b5de7d18 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/init.js +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/init.js @@ -15,11 +15,11 @@ import { BulkUploader } from './bulk_uploader'; * @param {Object} kbnServer manager of Kibana services - see `src/server/kbn_server` in Kibana core * @param {Object} server HapiJS server instance */ -export function initBulkUploader(_kbnServer, server) { - +export function initBulkUploader(kbnServer, server) { const config = server.config(); const interval = config.get('xpack.monitoring.kibana.collection.interval'); return new BulkUploader(server, { + kbnServer, interval }); } diff --git a/x-pack/plugins/reporting/server/lib/esqueue/__tests__/worker.js b/x-pack/plugins/reporting/server/lib/esqueue/__tests__/worker.js index 188bdf92ed651d..57698e81ff4370 100644 --- a/x-pack/plugins/reporting/server/lib/esqueue/__tests__/worker.js +++ b/x-pack/plugins/reporting/server/lib/esqueue/__tests__/worker.js @@ -7,7 +7,7 @@ import expect from 'expect.js'; import sinon from 'sinon'; import moment from 'moment'; -import { noop, random, get, find } from 'lodash'; +import { noop, random, get, find, identity } from 'lodash'; import { ClientMock } from './fixtures/elasticsearch'; import { QueueMock } from './fixtures/queue'; import { Worker } from '../worker'; @@ -494,25 +494,57 @@ describe('Worker class', function () { expect(msg).to.equal(false); }); - it('should return true on version errors', function () { + it('should reject the promise on version errors', function () { mockQueue.client.update.restore(); sinon.stub(mockQueue.client, 'update').returns(Promise.reject({ statusCode: 409 })); return worker._claimJob(job) - .then((res) => expect(res).to.equal(true)); + .catch(err => { + expect(err).to.eql({ statusCode: 409 }); + }); }); - it('should return false on other errors', function () { + it('should reject the promise on other errors', function () { mockQueue.client.update.restore(); sinon.stub(mockQueue.client, 'update').returns(Promise.reject({ statusCode: 401 })); return worker._claimJob(job) - .then((res) => expect(res).to.equal(false)); + .catch(err => { + expect(err).to.eql({ statusCode: 401 }); + }); }); + }); + + describe('find a pending job to claim', function () { + const getMockJobs = (status = 'pending') => ([{ + _index: 'myIndex', + _type: 'test', + _id: 12345, + _version: 3, + found: true, + _source: { + jobtype: 'jobtype', + created_by: false, + payload: { id: 'sample-job-1', now: 'Mon Apr 25 2016 14:13:04 GMT-0700 (MST)' }, + priority: 10, + timeout: 10000, + created_at: '2016-04-25T21:13:04.738Z', + attempts: 0, + max_attempts: 3, + status + }, + }]); - it('should emit on other errors', function (done) { + beforeEach(function () { + worker = new Worker(mockQueue, 'test', noop, defaultWorkerOptions); + }); + + afterEach(() => { mockQueue.client.update.restore(); + }); + + it('should emit for errors from claiming job', function (done) { sinon.stub(mockQueue.client, 'update').returns(Promise.reject({ statusCode: 401 })); - worker.on(constants.EVENT_WORKER_JOB_CLAIM_ERROR, function (err) { + worker.once(constants.EVENT_WORKER_JOB_CLAIM_ERROR, function (err) { try { expect(err).to.have.property('error'); expect(err).to.have.property('job'); @@ -523,7 +555,30 @@ describe('Worker class', function () { done(e); } }); - worker._claimJob(job); + + worker._claimPendingJobs(getMockJobs()); + }); + + it('should reject the promise if an error claiming the job', function () { + sinon.stub(mockQueue.client, 'update').returns(Promise.reject({ statusCode: 409 })); + return worker._claimPendingJobs(getMockJobs()) + .catch(err => { + expect(err).to.eql({ statusCode: 409 }); + }); + }); + + it('should get the pending job', function () { + sinon.stub(mockQueue.client, 'update').returns(Promise.resolve({ test: 'cool' })); + sinon.stub(worker, '_performJob').callsFake(identity); + return worker._claimPendingJobs(getMockJobs()) + .then(claimedJob => { + expect(claimedJob._index).to.be('myIndex'); + expect(claimedJob._type).to.be('test'); + expect(claimedJob._source.jobtype).to.be('jobtype'); + expect(claimedJob._source.status).to.be('processing'); + expect(claimedJob.test).to.be('cool'); + worker._performJob.restore(); + }); }); }); diff --git a/x-pack/plugins/reporting/server/lib/esqueue/worker.js b/x-pack/plugins/reporting/server/lib/esqueue/worker.js index 20d487c9128c75..02f9c054c2e3bd 100644 --- a/x-pack/plugins/reporting/server/lib/esqueue/worker.js +++ b/x-pack/plugins/reporting/server/lib/esqueue/worker.js @@ -22,6 +22,22 @@ function formatJobObject(job) { }; } +function getLogger(opts, id, logLevel) { + return (msg, err) => { + const logger = opts.logger || function () {}; + + const message = `${id} - ${msg}`; + const tags = ['worker', logLevel]; + + if (err) { + logger(`${message}: ${err.stack ? err.stack : err }`, tags); + return; + } + + logger(message, tags); + }; +} + export class Worker extends events.EventEmitter { constructor(queue, type, workerFn, opts) { if (typeof type !== 'string') throw new Error('type must be a string'); @@ -40,19 +56,8 @@ export class Worker extends events.EventEmitter { this.checkSize = opts.size || 10; this.doctype = opts.doctype || constants.DEFAULT_SETTING_DOCTYPE; - this.debug = (msg, err) => { - const logger = opts.logger || function () {}; - - const message = `${this.id} - ${msg}`; - const tags = ['worker', 'debug']; - - if (err) { - logger(`${message}: ${err.stack ? err.stack : err }`, tags); - return; - } - - logger(message, tags); - }; + this.debug = getLogger(opts, this.id, 'debug'); + this.warn = getLogger(opts, this.id, 'warn'); this._running = true; this.debug(`Created worker for job type ${this.jobtype}`); @@ -134,17 +139,11 @@ export class Worker extends events.EventEmitter { ...doc }; return updatedJob; - }) - .catch((err) => { - if (err.statusCode === 409) return true; - this.debug(`_claimJob failed on job ${job._id}`, err); - this.emit(constants.EVENT_WORKER_JOB_CLAIM_ERROR, this._formatErrorParams(err, job)); - return false; }); } _failJob(job, output = false) { - this.debug(`Failing job ${job._id}`); + this.warn(`Failing job ${job._id}`); const completedTime = moment().toISOString(); const docOutput = this._formatOutput(output); @@ -170,7 +169,7 @@ export class Worker extends events.EventEmitter { .then(() => true) .catch((err) => { if (err.statusCode === 409) return true; - this.debug(`_failJob failed to update job ${job._id}`, err); + this.warn(`_failJob failed to update job ${job._id}`, err); this.emit(constants.EVENT_WORKER_FAIL_UPDATE_ERROR, this._formatErrorParams(err, job)); return false; }); @@ -215,7 +214,7 @@ export class Worker extends events.EventEmitter { if (isResolved) return; cancellationToken.cancel(); - this.debug(`Timeout processing job ${job._id}`); + this.warn(`Timeout processing job ${job._id}`); reject(new WorkerTimeoutError(`Worker timed out, timeout = ${job._source.timeout}`, { timeout: job._source.timeout, jobId: job._id, @@ -253,7 +252,7 @@ export class Worker extends events.EventEmitter { }) .catch((err) => { if (err.statusCode === 409) return false; - this.debug(`Failure saving job output ${job._id}`, err); + this.warn(`Failure saving job output ${job._id}`, err); this.emit(constants.EVENT_WORKER_JOB_UPDATE_ERROR, this._formatErrorParams(err, job)); }); }, (jobErr) => { @@ -265,7 +264,7 @@ export class Worker extends events.EventEmitter { // job execution failed if (jobErr.name === 'WorkerTimeoutError') { - this.debug(`Timeout on job ${job._id}`); + this.warn(`Timeout on job ${job._id}`); this.emit(constants.EVENT_WORKER_JOB_TIMEOUT, this._formatErrorParams(jobErr, job)); return; @@ -278,7 +277,7 @@ export class Worker extends events.EventEmitter { } } - this.debug(`Failure occurred on job ${job._id}`, jobErr); + this.warn(`Failure occurred on job ${job._id}`, jobErr); this.emit(constants.EVENT_WORKER_JOB_EXECUTION_ERROR, this._formatErrorParams(jobErr, job)); return this._failJob(job, (jobErr.toString) ? jobErr.toString() : false); }); @@ -316,23 +315,30 @@ export class Worker extends events.EventEmitter { return this._claimJob(job) .then((claimResult) => { - if (claimResult !== false) { - claimed = true; - return claimResult; + claimed = true; + return claimResult; + }) + .catch((err) => { + if (err.statusCode === 409) { + this.warn(`_claimPendingJobs encountered a version conflict on updating pending job ${job._id}`, err); + return; // continue reducing and looking for a different job to claim } + this.emit(constants.EVENT_WORKER_JOB_CLAIM_ERROR, this._formatErrorParams(err, job)); + return Promise.reject(err); }); }); }, Promise.resolve()) .then((claimedJob) => { if (!claimedJob) { - this.debug(`All ${jobs.length} jobs already claimed`); + this.debug(`Found no claimable jobs out of ${jobs.length} total`); return; } this.debug(`Claimed job ${claimedJob._id}`); return this._performJob(claimedJob); }) .catch((err) => { - this.debug('Error claiming jobs', err); + this.warn('Error claiming jobs', err); + return Promise.reject(err); }); } @@ -384,7 +390,7 @@ export class Worker extends events.EventEmitter { // ignore missing indices errors if (err && err.status === 404) return []; - this.debug('job querying failed', err); + this.warn('job querying failed', err); this.emit(constants.EVENT_WORKER_JOB_SEARCH_ERROR, this._formatErrorParams(err)); throw err; }); diff --git a/x-pack/plugins/reporting/server/usage/get_reporting_usage_collector.js b/x-pack/plugins/reporting/server/usage/get_reporting_usage_collector.js index b4a76767b3adf4..36a24a3c7b1b61 100644 --- a/x-pack/plugins/reporting/server/usage/get_reporting_usage_collector.js +++ b/x-pack/plugins/reporting/server/usage/get_reporting_usage_collector.js @@ -8,6 +8,7 @@ import { uniq } from 'lodash'; import { getExportTypesHandler } from './get_export_type_handler'; import { getReportCountsByParameter } from './get_reporting_type_counts'; import { KIBANA_REPORTING_TYPE } from '../../common/constants'; +import { KIBANA_STATS_TYPE_MONITORING } from '../../../monitoring/common/constants'; /** * @typedef {Object} ReportingUsageStats Almost all of these stats are optional. @@ -117,6 +118,7 @@ export function getReportingUsageCollector(server) { const { collectorSet } = server.usage; return collectorSet.makeUsageCollector({ type: KIBANA_REPORTING_TYPE, + fetch: async callCluster => { const xpackInfo = server.plugins.xpack_main.info; const config = server.config(); @@ -147,6 +149,24 @@ export function getReportingUsageCollector(server) { ...statsOverLast7Days } }; + }, + + /* + * Format the response data into a model for internal upload + * 1. Make this data part of the "kibana_stats" type + * 2. Organize the payload in the usage.xpack.reporting namespace of the data payload + */ + formatForBulkUpload: result => { + return { + type: KIBANA_STATS_TYPE_MONITORING, + payload: { + usage: { + xpack: { + reporting: result + } + } + } + }; } }); } diff --git a/x-pack/plugins/xpack_main/server/lib/telemetry/monitoring/__tests__/fixtures/beats_stats_results.json b/x-pack/plugins/xpack_main/server/lib/telemetry/monitoring/__tests__/fixtures/beats_stats_results.json index 108369dcba72a1..be6e4352514fe8 100644 --- a/x-pack/plugins/xpack_main/server/lib/telemetry/monitoring/__tests__/fixtures/beats_stats_results.json +++ b/x-pack/plugins/xpack_main/server/lib/telemetry/monitoring/__tests__/fixtures/beats_stats_results.json @@ -34,6 +34,26 @@ } } }, + { + "_source": { + "type": "beats_state", + "cluster_uuid": "W7hppdX7R229Oy3KQbZrTw", + "beats_state": { + "state": { + "host": { + "name": "Elastic-MBP.local", + "architecture": "x86_64", + "os": { + "platform": "darwin", + "version": "10.13.6", + "family": "darwin", + "build": "17G65" + } + } + } + } + } + }, { "_source": { "type": "beats_stats", diff --git a/x-pack/plugins/xpack_main/server/lib/telemetry/monitoring/__tests__/get_beats_stats.js b/x-pack/plugins/xpack_main/server/lib/telemetry/monitoring/__tests__/get_beats_stats.js index cbd9da52b44909..3cde267a1536bc 100644 --- a/x-pack/plugins/xpack_main/server/lib/telemetry/monitoring/__tests__/get_beats_stats.js +++ b/x-pack/plugins/xpack_main/server/lib/telemetry/monitoring/__tests__/get_beats_stats.js @@ -14,6 +14,7 @@ const getBaseOptions = () => ({ clusterHostSets: {}, clusterInputSets: {}, clusterModuleSets: {}, + clusterArchitectureMaps: {} }); describe('Get Beats Stats', () => { @@ -111,6 +112,10 @@ describe('Get Beats Stats', () => { count: 0, names: [], }, + architecture: { + count: 0, + architectures: [] + } }, }); }); @@ -140,6 +145,16 @@ describe('Get Beats Stats', () => { count: 1, names: [ 'firehose' ], }, + architecture: { + count: 1, + architectures: [ + { + architecture: 'x86_64', + count: 1, + name: 'darwin' + } + ] + } }, FlV4ckTxQ0a78hmBkzzc9A: { count: 405, @@ -165,6 +180,10 @@ describe('Get Beats Stats', () => { count: 0, names: [], }, + architecture: { + count: 0, + architectures: [] + } }, }); }); diff --git a/x-pack/plugins/xpack_main/server/lib/telemetry/monitoring/get_beats_stats.js b/x-pack/plugins/xpack_main/server/lib/telemetry/monitoring/get_beats_stats.js index 9b4b9733ee9538..ff0ddadac40f11 100644 --- a/x-pack/plugins/xpack_main/server/lib/telemetry/monitoring/get_beats_stats.js +++ b/x-pack/plugins/xpack_main/server/lib/telemetry/monitoring/get_beats_stats.js @@ -26,6 +26,10 @@ const getBaseStats = () => ({ count: 0, names: [] }, + architecture: { + count: 0, + architectures: [] + } }); @@ -37,7 +41,7 @@ const getBaseStats = () => ({ * @param {Object} clusterHostSets - the object keyed by cluster UUIDs to count the unique hosts * @param {Object} clusterModuleSets - the object keyed by cluster UUIDs to count the unique modules */ -export function processResults(results = [], { clusters, clusterHostSets, clusterInputSets, clusterModuleSets }) { +export function processResults(results = [], { clusters, clusterHostSets, clusterInputSets, clusterModuleSets, clusterArchitectureMaps }) { const currHits = get(results, 'hits.hits', []); currHits.forEach(hit => { const clusterUuid = get(hit, '_source.cluster_uuid'); @@ -46,6 +50,7 @@ export function processResults(results = [], { clusters, clusterHostSets, cluste clusterHostSets[clusterUuid] = new Set(); clusterInputSets[clusterUuid] = new Set(); clusterModuleSets[clusterUuid] = new Set(); + clusterArchitectureMaps[clusterUuid] = new Map(); } const processBeatsStatsResults = () => { @@ -97,6 +102,25 @@ export function processResults(results = [], { clusters, clusterHostSets, cluste clusters[clusterUuid].module.names = Array.from(moduleSet); clusters[clusterUuid].module.count += stateModule.count; } + + const stateHost = get(hit, '_source.beats_state.state.host'); + if (stateHost !== undefined) { + const hostMap = clusterArchitectureMaps[clusterUuid]; + const hostKey = `${stateHost.architecture}/${stateHost.os.platform}`; + let os = hostMap.get(hostKey); + + if (!os) { // undefined if new + os = { name: stateHost.os.platform, architecture: stateHost.architecture, count: 0 }; + hostMap.set(hostKey, os); + } + + // total per os/arch + os.count += 1; + + // overall total (which should be the same number as the sum of all os.count values) + clusters[clusterUuid].architecture.count += 1; + clusters[clusterUuid].architecture.architectures = Array.from(hostMap.values()); + } }; if (get(hit, '_source.type') === 'beats_stats') { @@ -136,6 +160,7 @@ async function fetchBeatsByType(server, callCluster, clusterUuids, start, end, { 'hits.hits._source.beats_stats.metrics.libbeat.output.type', 'hits.hits._source.beats_state.state.input', 'hits.hits._source.beats_state.state.module', + 'hits.hits._source.beats_state.state.host', ], body: { query: createQuery({ @@ -194,7 +219,8 @@ export async function getBeatsStats(server, callCluster, clusterUuids, start, en clusters: {}, // the result object to be built up clusterHostSets: {}, // passed to processResults for tracking state in the results generation clusterInputSets: {}, // passed to processResults for tracking state in the results generation - clusterModuleSets: {} // passed to processResults for tracking state in the results generation + clusterModuleSets: {}, // passed to processResults for tracking state in the results generation + clusterArchitectureMaps: {} // passed to processResults for tracking state in the results generation }; await Promise.all([ diff --git a/x-pack/plugins/xpack_main/server/routes/api/v1/settings.js b/x-pack/plugins/xpack_main/server/routes/api/v1/settings.js index 13edba4a7f18b5..b546054bfe7f17 100644 --- a/x-pack/plugins/xpack_main/server/routes/api/v1/settings.js +++ b/x-pack/plugins/xpack_main/server/routes/api/v1/settings.js @@ -26,7 +26,10 @@ export function settingsRoute(server, kbnServer) { const { collectorSet } = server.usage; const settingsCollector = collectorSet.getCollectorByType(KIBANA_SETTINGS_TYPE); - const settings = await settingsCollector.fetch(callCluster); + let settings = await settingsCollector.fetch(callCluster); + if (!settings) { + settings = settingsCollector.getEmailValueStructure(null); + } const uuid = await getClusterUuid(callCluster); const kibana = getKibanaInfoForStats(server, kbnServer); diff --git a/x-pack/test/api_integration/apis/xpack_main/settings/settings.js b/x-pack/test/api_integration/apis/xpack_main/settings/settings.js index b6e1979da65db6..9f7055c7b83329 100644 --- a/x-pack/test/api_integration/apis/xpack_main/settings/settings.js +++ b/x-pack/test/api_integration/apis/xpack_main/settings/settings.js @@ -35,6 +35,7 @@ export default function ({ getService }) { expect(body.settings.kibana.transport_address.length > 0).to.eql(true); expect(body.settings.kibana.version.length > 0).to.eql(true); expect(body.settings.kibana.status.length > 0).to.eql(true); + expect(body.settings.xpack.default_admin_email).to.eql(null); }); }); }); diff --git a/x-pack/test/reporting/functional/reporting.js b/x-pack/test/reporting/functional/reporting.js index 11e0f9716e3cfd..16501ec701b03c 100644 --- a/x-pack/test/reporting/functional/reporting.js +++ b/x-pack/test/reporting/functional/reporting.js @@ -86,7 +86,7 @@ export default function ({ getService, getPageObjects }) { // report than phantom. this.timeout(360000); - await PageObjects.dashboard.clickEdit(); + await PageObjects.dashboard.switchToEditMode(); await PageObjects.reporting.setTimepickerInDataRange(); const visualizations = PageObjects.dashboard.getTestVisualizationNames(); @@ -124,7 +124,7 @@ export default function ({ getService, getPageObjects }) { // report than phantom. this.timeout(360000); - await PageObjects.dashboard.clickEdit(); + await PageObjects.dashboard.switchToEditMode(); await PageObjects.dashboard.useMargins(true); await PageObjects.dashboard.saveDashboard('report test'); await PageObjects.reporting.openReportingPanel(); diff --git a/x-pack/yarn.lock b/x-pack/yarn.lock index 1c859041251d4f..f98f7fd87ee1be 100644 --- a/x-pack/yarn.lock +++ b/x-pack/yarn.lock @@ -10,9 +10,9 @@ esutils "^2.0.2" js-tokens "^3.0.0" -"@elastic/eui@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-3.6.1.tgz#59a719d4cd98a1efd0341e13b70b50119aabe2f5" +"@elastic/eui@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-3.7.0.tgz#276d2a8e724778d15192b3847247d3731c7ff738" dependencies: classnames "^2.2.5" core-js "^2.5.1" diff --git a/yarn.lock b/yarn.lock index 7f10b07ead2fe5..df005a261d848e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -81,9 +81,9 @@ version "0.0.0" uid "" -"@elastic/eui@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-3.6.1.tgz#59a719d4cd98a1efd0341e13b70b50119aabe2f5" +"@elastic/eui@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-3.7.0.tgz#276d2a8e724778d15192b3847247d3731c7ff738" dependencies: classnames "^2.2.5" core-js "^2.5.1" @@ -9338,6 +9338,10 @@ normalize-path@^2.0.0, normalize-path@^2.0.1, normalize-path@^2.1.1: dependencies: remove-trailing-separator "^1.0.1" +normalize-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + normalize-range@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" @@ -11342,6 +11346,10 @@ regenerate@^1.2.1: version "1.3.3" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.3.tgz#0c336d3980553d755c39b586ae3b20aa49c82b7f" +regenerate@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11" + regenerator-runtime@^0.10.0: version "0.10.5" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658"