diff --git a/.eslintignore b/.eslintignore index f4db73169cbb7..876c674d00fea 100644 --- a/.eslintignore +++ b/.eslintignore @@ -8,3 +8,4 @@ /src/ui/public/utils/decode_geo_hash.js /src/core_plugins/timelion/public/webpackShims/jquery.flot.* /ui_framework/doc_site/build +/tasks/vendor \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index e7ca77ccc22f0..f422e8138da6f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -8,6 +8,6 @@ attention. - Have you signed the [contributor license agreement](https://www.elastic.co/contributor-agreement)? - Have you followed the [contributor guidelines](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md)? - If submitting code, have you included unit tests that cover the changes? -- If submitting code, have you tested and built your code locally prior to submission with `npm test && npm run build`? +- If submitting code, have you tested and built your code locally prior to submission with `yarn test && yarn build`? - If submitting code, is your pull request against master? Unless there is a good reason otherwise, we prefer pull requests against master and will backport as needed. ---> +--> \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3aa94ee5228c3..d1324161c6074 100644 --- a/.gitignore +++ b/.gitignore @@ -35,7 +35,6 @@ selenium *.out ui_framework/doc_site/build !ui_framework/doc_site/build/index.html -yarn.lock package-lock.json .yo-rc.json /.vscode diff --git a/.npmrc b/.npmrc deleted file mode 100644 index cffe8cdef132f..0000000000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -save-exact=true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b92e5316d6deb..65d723cd1aff7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -158,16 +158,18 @@ Install the version of node.js listed in the `.node-version` file _(this can be nvm install "$(cat .node-version)" ``` -Install `npm` dependencies +Install the latest version of [yarn](https://yarnpkg.com). + +Install dependencies ```bash -npm install +yarn ``` Start elasticsearch. ```bash -npm run elasticsearch +yarn elasticsearch ``` > You'll need to have a `java` binary in `PATH` or set `JAVA_HOME`. @@ -182,12 +184,12 @@ node scripts/makelogs Start the development server. ```bash - npm start + yarn start ``` > On Windows, you'll need you use Git Bash, Cygwin, or a similar shell that exposes the `sh` command. And to successfully build you'll need Cygwin optional packages zip, tar, and shasum. -Now you can point your web browser to https://localhost:5601 and start using Kibana! When running `npm start`, Kibana will also log that it is listening on port 5603 due to the base path proxy, but you should still access Kibana on port 5601. +Now you can point your web browser to https://localhost:5601 and start using Kibana! When running `yarn start`, Kibana will also log that it is listening on port 5603 due to the base path proxy, but you should still access Kibana on port 5601. #### Customizing `config/kibana.dev.yml` @@ -203,7 +205,7 @@ In development mode, Kibana runs a customized version of [Webpack](http://webpac #### Setting Up SSL -Kibana includes a self-signed certificate that can be used for development purposes: `npm start -- --ssl`. +Kibana includes a self-signed certificate that can be used for development purposes: `yarn start --ssl`. ### Linting @@ -230,23 +232,23 @@ Before running the tests you will need to install the projects dependencies as d Once that's done, just run: ```bash -npm run test && npm run build -- --skip-os-packages +yarn test && yarn build --skip-os-packages ``` ### Debugging Unit Tests -The standard `npm run test` task runs several sub tasks and can take several minutes to complete, making debugging failures pretty painful. In order to ease the pain specialized tasks provide alternate methods for running the tests. +The standard `yarn test` task runs several sub tasks and can take several minutes to complete, making debugging failures pretty painful. In order to ease the pain specialized tasks provide alternate methods for running the tests. -To execute both server and browser tests, but skip linting, use `npm run test:quick`. +To execute both server and browser tests, but skip linting, use `yarn test:quick`. ```bash -npm run test:quick +yarn test:quick ``` -Use `npm run test:server` when you want to run only the server tests. +Use `yarn test:server` when you want to run only the server tests. ```bash -npm run test:server +yarn test:server ``` When you'd like to execute individual server-side test files, you can use the command below. Note that this command takes care of configuring Mocha with Babel compilation for you, and you'll be better off avoiding a globally installed `mocha` package. This command is great for development and for quickly identifying bugs. @@ -261,16 +263,16 @@ You could also add the `--debug` option so that `node` is run using the `--debug node scripts/mocha --debug ``` -With `npm run test:browser`, you can run only the browser tests. Coverage reports are available for browser tests by running `npm run test:coverage`. You can find the results under the `coverage/` directory that will be created upon completion. +With `yarn test:browser`, you can run only the browser tests. Coverage reports are available for browser tests by running `yarn test:coverage`. You can find the results under the `coverage/` directory that will be created upon completion. ```bash -npm run test:browser +yarn test:browser ``` -Using `npm run test:dev` initializes an environment for debugging the browser tests. Includes an dedicated instance of the kibana server for building the test bundle, and a karma server. When running this task the build is optimized for the first time and then a karma-owned instance of the browser is opened. Click the "debug" button to open a new tab that executes the unit tests. +Using `yarn test:dev` initializes an environment for debugging the browser tests. Includes an dedicated instance of the kibana server for building the test bundle, and a karma server. When running this task the build is optimized for the first time and then a karma-owned instance of the browser is opened. Click the "debug" button to open a new tab that executes the unit tests. ```bash -npm run test:dev +yarn test:dev ``` In the screenshot below, you'll notice the URL is `localhost:9876/debug.html`. You can append a `grep` query parameter to this URL and set it to a string value which will be used to exclude tests which don't match. For example, if you changed the URL to `localhost:9876/debug.html?query=my test` and then refreshed the browser, you'd only see tests run which contain "my test" in the test description. @@ -285,8 +287,8 @@ This should work super if you're using the [Kibana plugin generator](https://git To run the tests for just your particular plugin run the following command from your plugin: ```bash -npm run test:server -npm run test:browser -- --dev # remove the --dev flag to run them once and close +yarn test:server +yarn test:browser --dev # remove the --dev flag to run them once and close ``` ### Cross-browser Compatibility @@ -300,7 +302,7 @@ npm run test:browser -- --dev # remove the --dev flag to run them once and close * Open VMWare and go to Window > Virtual Machine Library. Unzip the virtual machine and drag the .vmx file into your Virtual Machine Library. * Right-click on the virtual machine you just added to your library and select "Snapshots...", and then click the "Take" button in the modal that opens. You can roll back to this snapshot when the VM expires in 90 days. * In System Preferences > Sharing, change your computer name to be something simple, e.g. "computer". -* Run Kibana with `npm start -- --host=computer.local` (substituting your computer name). +* Run Kibana with `yarn start --host=computer.local` (substituting your computer name). * Now you can run your VM, open the browser, and navigate to `http://computer.local:5601` to test Kibana. #### Running Browser Automation Tests @@ -316,13 +318,13 @@ Packages are built using fpm, pleaserun, dpkg, and rpm. fpm and pleaserun can b apt-get install ruby-dev rpm gem install fpm -v 1.5.0 gem install pleaserun -v 0.0.24 -npm run build -- --skip-archives +yarn build --skip-archives ``` To specify a package to build you can add `rpm` or `deb` as an argument. ```bash -npm run build -- --rpm +yarn build --rpm ``` Distributable packages can be found in `target/` after the build completes. diff --git a/docs/development/core/development-basepath.asciidoc b/docs/development/core/development-basepath.asciidoc index a4f2a2b57d8f0..b7e0dec88bf50 100644 --- a/docs/development/core/development-basepath.asciidoc +++ b/docs/development/core/development-basepath.asciidoc @@ -79,9 +79,9 @@ To accomplish this the `serve` task does a few things: This proxy can sometimes have unintended side effects in development, so when needed you can opt out by passing the `--no-base-path` flag to the `serve` task -or `npm start`. +or `yarn start`. ["source","shell"] ----------- -npm start -- --no-base-path ------------ +yarn start --no-base-path +----------- \ No newline at end of file diff --git a/docs/development/core/development-dependencies.asciidoc b/docs/development/core/development-dependencies.asciidoc index a403e9c2c7516..d430667449afa 100644 --- a/docs/development/core/development-dependencies.asciidoc +++ b/docs/development/core/development-dependencies.asciidoc @@ -10,14 +10,14 @@ Before you can use an external library with Kibana you have to install it. You do that using... [float] -==== npm (preferred method) +==== yarn (preferred method) Once you've http://npmsearch.com[found] a dependency you want to add, you can install it like so: ["source","shell"] ----------- -npm install --save some-neat-library +yarn add some-neat-library ----------- At the top of a javascript file, just import the library using it's name: @@ -28,13 +28,13 @@ import someNeatLibrary from 'some-neat-library'; ----------- Just like working in node.js, front-end code can require node modules installed -by npm without any additional configuration. +by yarn without any additional configuration. [float] ==== webpackShims When a library you want to use does use es6 or common.js modules but is not -available on npm, you can copy the source of the library into a webpackShim. +available with yarn, you can copy the source of the library into a webpackShim. ["source","shell"] ----------- @@ -97,7 +97,7 @@ module.exports = window.angular; What this shim does is fairly simple if you go line by line: . makes sure that jQuery is loaded before angular (which actually runs the shim above) -. load the angular.js file from the npm installation +. load the angular.js file from the node_modules directory . load the angular-elastic plugin, a plugin we want to always be included whenever we import angular . use the `ui/modules` module to add the module exported by angular-elastic as a dependency to the `kibana` angular module . finally, export the window.angular variable. This means that writing `import angular from 'angular';` will properly set the angular variable to the angular library, rather than undefined which is the default behavior. \ No newline at end of file diff --git a/docs/development/core/development-functional-tests.asciidoc b/docs/development/core/development-functional-tests.asciidoc index 2703c102c939a..936593ae5873f 100644 --- a/docs/development/core/development-functional-tests.asciidoc +++ b/docs/development/core/development-functional-tests.asciidoc @@ -12,20 +12,20 @@ There are three ways to run the tests depending on your goals: 1. Easiest option: ** Description: Starts up Kibana & Elasticsearch servers, followed by running tests. This is much slower when running the tests multiple times because slow startup time for the servers. Recommended for single-runs. -** `npm run test:ui` +** `yarn test:ui` *** does everything in a single command, including running Elasticsearch and Kibana locally *** tears down everything after the tests run *** exit code reports success/failure of the tests 2. Best for development: ** Description: Two commands, run in separate terminals, separate the components that are long-running and slow from those that are ephemeral and fast. Tests can be re-run much faster, and this still runs Elasticsearch & Kibana locally. -** `npm run test:ui:server` +** `yarn test:ui:server` *** starts Elasticsearch and Kibana servers *** slow to start *** can be reused for multiple executions of the tests, thereby saving some time when re-running tests *** automatically restarts the Kibana server when relevant changes are detected ** `node scripts/functional_test_runner` -*** runs the tests against Kibana & Elasticsearch servers that were started `npm run test:ui:server` +*** runs the tests against Kibana & Elasticsearch servers that were started `yarn test:ui:server` *** exit code reports success or failure of the tests 3. Custom option: @@ -405,4 +405,4 @@ const log = getService(‘log’); // log.debug only writes when using the `--debug` or `--verbose` flag. log.debug(‘done clicking menu’); ------------ +----------- \ No newline at end of file diff --git a/docs/development/plugin/development-plugin-functional-tests.asciidoc b/docs/development/plugin/development-plugin-functional-tests.asciidoc index aec24075f4368..f5fe3af27407c 100644 --- a/docs/development/plugin/development-plugin-functional-tests.asciidoc +++ b/docs/development/plugin/development-plugin-functional-tests.asciidoc @@ -13,20 +13,18 @@ To get started copy and paste this example to `test/functional/config.js`: ["source","js"] ----------- import { resolve } from 'path'; +import { resolveKibanaPath } from '@elastic/plugin-helpers'; + import { MyServiceProvider } from './services/my_service'; import { MyAppPageProvider } from './services/my_app_page; -// allow overriding the default kibana directory -// using the KIBANA_DIR environment variable -const KIBANA_CONFIG_PATH = resolve(process.env.KIBANA_DIR || '../kibana', 'test/functional/config.js'); - // the default export of config files must be a config provider // that returns an object with the projects config values export default async function ({ readConfigFile }) { // read the Kibana config file so that we can utilize some of // its services and PageObjects - const kibanaConfig = await readConfigFile(KIBANA_CONFIG_PATH); + const kibanaConfig = await readConfigFile(resolveKibanaPath('test/functional/config.js')); return { // list paths to the files that contain your plugins tests @@ -81,7 +79,7 @@ From the root of your repo you should now be able to run the `FunctionalTestRunn ["source","shell"] ----------- -node ../kibana/scripts/functional_test_runner +node ../../kibana/scripts/functional_test_runner ----------- [float] diff --git a/docs/development/plugin/development-plugin-resources.asciidoc b/docs/development/plugin/development-plugin-resources.asciidoc index 647ca4b321083..379dc097ecb28 100644 --- a/docs/development/plugin/development-plugin-resources.asciidoc +++ b/docs/development/plugin/development-plugin-resources.asciidoc @@ -20,7 +20,20 @@ Many Kibana developers hang out on `irc.freenode.net` in the `#kibana` channel. [float] ==== Plugin Generator -Check out the https://github.com/elastic/generator-kibana-plugin[plugin generator] to kick-start your plugin. +Check out the https://github.com/elastic/template-kibana-plugin/[plugin generator] to kick-start your plugin. + +[float] +==== Directory structure for plugins + +The Kibana directory must be named `kibana`, and your plugin directory must be located within the sibling `kibana-extra` folder, for example: + +["source","shell"] +----------- +. +├── kibana +├── kibana-extra/foo-plugin +└── kibana-extra/bar-plugin +----------- [float] ==== References in the code diff --git a/docs/development/visualize/development-create-visualization.asciidoc b/docs/development/visualize/development-create-visualization.asciidoc index a34598710bb5e..859c8dfb05558 100644 --- a/docs/development/visualize/development-create-visualization.asciidoc +++ b/docs/development/visualize/development-create-visualization.asciidoc @@ -91,7 +91,7 @@ options or data change, and a destroy method which will be called to cleanup. The render function receives the data object and status object which tells what actually changed. Render function needs to return a promise, which should be resolved once the visualization is done rendering. -Status object has the following properties: `vis`, `aggs`, `resize`, `data`. Each of them is set to true if the matching +Status object has the following properties: `aggs`, `data`, `params`, `resize`, `time`, `uiState`. Each of them is set to true if the matching object changed since last call to the render function or set to false otherwise. You can use it to make your visualization rendering more efficient. diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 6df22793cd296..9ee757791b70d 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -69,6 +69,7 @@ error messages. `logging.silent:`:: *Default: false* Set the value of this setting to `true` to suppress all logging output. `logging.verbose`:: *Default: false* Set the value of this setting to `true` to log all events, including system usage information and all requests. +`logging.useUTC`:: *Default: true* Set the value of this setting to `false` to log events using the timezone of the server, rather than UTC. `path.data`:: *Default: `data`* The path where Kibana stores persistent data not saved in Elasticsearch. diff --git a/package.json b/package.json index c9fa4081d4219..419d0e117d57f 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ }, "dependencies": { "@elastic/datemath": "2.3.0", - "@elastic/eui": "0.0.10", + "@elastic/eui": "0.0.11", "@elastic/filesaver": "1.1.2", "@elastic/numeral": "2.3.0", "@elastic/test-subj-selector": "0.2.1", @@ -151,25 +151,23 @@ "lodash": "3.10.1", "lru-cache": "4.1.1", "markdown-it": "8.3.2", - "mathjs": "3.16.2", "minimatch": "2.0.10", "mkdirp": "0.5.1", "moment": "2.13.0", "moment-timezone": "0.5.4", + "mustache": "2.3.0", "ngreact": "0.5.1", "no-ui-slider": "1.2.0", "node-fetch": "1.3.2", "pegjs": "0.9.0", + "pivotal-ui": "11.0.0", "postcss-loader": "2.0.6", "prop-types": "15.5.8", "proxy-from-env": "1.0.0", - "pui-react-overlay-trigger": "8.3.3", - "pui-react-tooltip": "8.3.3", "querystring-browser": "1.0.4", "raw-loader": "0.5.1", "react": "16.0.0", - "react-ace": "5.2.2", - "react-addons-test-utils": "15.6.2", + "react-ace": "5.5.0", "react-anything-sortable": "1.7.3", "react-color": "2.11.7", "react-dom": "16.0.0", @@ -212,6 +210,7 @@ "vision": "4.1.0", "webpack": "3.6.0", "webpack-merge": "4.1.0", + "whatwg-fetch": "^2.0.3", "wreck": "12.4.0", "yauzl": "2.7.0" }, @@ -250,7 +249,7 @@ "grunt-cli": "0.1.13", "grunt-contrib-clean": "1.0.0", "grunt-contrib-copy": "0.8.1", - "grunt-esvm": "3.2.11", + "grunt-esvm": "3.2.12", "grunt-karma": "2.0.0", "grunt-run": "0.7.0", "grunt-simple-mocha": "0.4.0", @@ -303,8 +302,13 @@ "yeoman-generator": "1.1.1", "yo": "2.0.0" }, + "resolutions": { + "angular": "1.6.5", + "moment": "2.13.0", + "react": "16.0.0" + }, "engines": { "node": "6.12.2", - "npm": "3.10.10" + "yarn": "^1.3.2" } } diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index fc8da7a8098a8..71e111cc889c7 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -39,7 +39,7 @@ function readServerSettings(opts, extraCliOptions) { if (opts.dev) { set('env', 'development'); - set('optimize.lazy', true); + set('optimize.watch', true); if (opts.ssl) { set('server.ssl.enabled', true); diff --git a/src/core_plugins/input_control_vis/public/components/vis/form_row.js b/src/core_plugins/input_control_vis/public/components/vis/form_row.js index ff2891f868731..4747afdb78750 100644 --- a/src/core_plugins/input_control_vis/public/components/vis/form_row.js +++ b/src/core_plugins/input_control_vis/public/components/vis/form_row.js @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; -import { Tooltip } from 'pui-react-tooltip'; -import { OverlayTrigger } from 'pui-react-overlay-trigger'; +import { Tooltip } from 'pivotal-ui/react/tooltip'; +import { OverlayTrigger } from 'pivotal-ui/react/overlay-trigger'; export function FormRow(props) { let control = props.children; diff --git a/src/core_plugins/kibana/common/tutorials/filebeat_cloud_instructions.js b/src/core_plugins/kibana/common/tutorials/filebeat_cloud_instructions.js new file mode 100644 index 0000000000000..d0fd6da8d2cf8 --- /dev/null +++ b/src/core_plugins/kibana/common/tutorials/filebeat_cloud_instructions.js @@ -0,0 +1,40 @@ +export const FILEBEAT_CLOUD_INSTRUCTIONS = { + 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.' + }, + 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.' + }, + 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.' + }, + 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.' + } + } +}; diff --git a/src/core_plugins/kibana/common/tutorials/filebeat_instructions.js b/src/core_plugins/kibana/common/tutorials/filebeat_instructions.js new file mode 100644 index 0000000000000..b941f3b3c67f1 --- /dev/null +++ b/src/core_plugins/kibana/common/tutorials/filebeat_instructions.js @@ -0,0 +1,166 @@ +export const FILEBEAT_INSTRUCTIONS = { + 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).', + 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).', + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-{config.kibana.version}-amd64.deb', + 'sudo dpkg -i filebeat-{config.kibana.version}-amd64.deb' + ], + textPost: 'Looking for the 32 bits 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).', + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-{config.kibana.version}-x86_64.rpm', + 'sudo rpm -vi filebeat-{config.kibana.version}-x86_64.rpm' + ], + textPost: 'Looking for the 32 bits 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 [downloads](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 may 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' + ], + 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', + ] + }, + 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', + ] + }, + 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', + ], + }, + WINDOWS: { + title: 'Start Filebeat', + 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> Service-Start filebeat', + ] + } + }, + CONFIG: { + OSX: { + title: 'Edit the configuration', + textPre: 'Modify `filebeat.yml` to set the connection information:', + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + 'setup.kibana:', + ' host: ""' + ], + 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', + textPre: 'Modify `/etc/filebeat/filebeat.yml` to set the connection information:', + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + 'setup.kibana:', + ' host: ""' + ], + 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', + textPre: 'Modify `/etc/filebeat/filebeat.yml` to set the connection information:', + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + 'setup.kibana:', + ' host: ""' + ], + 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:', + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + 'setup.kibana:', + ' host: ""' + ], + 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:', + commands: [ + 'bin/elasticsearch-plugin install ingest-geoip', + 'bin/elasticsearch-plugin install ingest-user-agent' + ] + }, + GEOIP: { + title: 'Install Elasticsearch GeoIP plugin', + textPre: 'This module requires an Elasticsearch plugin that is not ' + + 'installed by default.\n\nFrom the Elasticsearch installation folder, run:', + commands: [ + 'bin/elasticsearch-plugin install ingest-geoip' + ] + } + } +}; diff --git a/src/core_plugins/kibana/common/tutorials/filebeat_onprem_cloud_instructions.js b/src/core_plugins/kibana/common/tutorials/filebeat_onprem_cloud_instructions.js new file mode 100644 index 0000000000000..e385fb127b4a2 --- /dev/null +++ b/src/core_plugins/kibana/common/tutorials/filebeat_onprem_cloud_instructions.js @@ -0,0 +1,2 @@ +export const FILEBEAT_ONPREM_CLOUD_INSTRUCTIONS = { +}; diff --git a/src/core_plugins/kibana/common/tutorials/instruction_variant.js b/src/core_plugins/kibana/common/tutorials/instruction_variant.js new file mode 100644 index 0000000000000..909def999d2b8 --- /dev/null +++ b/src/core_plugins/kibana/common/tutorials/instruction_variant.js @@ -0,0 +1,34 @@ +export const INSTRUCTION_VARIANT = { + OSX: 'osx', + DEB: 'deb', + RPM: 'rpm', + DOCKER: 'docker', + WINDOWS: 'windows', + NODE: 'node', + DJANGO: 'django', + FLASK: 'flask' +}; + +const DISPLAY_MAP = { + [INSTRUCTION_VARIANT.OSX]: 'macOS', + [INSTRUCTION_VARIANT.DEB]: 'DEB', + [INSTRUCTION_VARIANT.RPM]: 'RPM', + [INSTRUCTION_VARIANT.DOCKER]: 'Docker', + [INSTRUCTION_VARIANT.WINDOWS]: 'Windows', + [INSTRUCTION_VARIANT.NODE]: 'Node.js', + [INSTRUCTION_VARIANT.DJANGO]: 'Django', + [INSTRUCTION_VARIANT.FLASK]: 'Flask', +}; + +/** + * Convert instruction variant id into display text. + * + * @params {String} id - instruction variant id as defined from INSTRUCTION_VARIANT + * @return {String} display name + */ +export function getDisplayText(id) { + if (id in DISPLAY_MAP) { + return DISPLAY_MAP[id]; + } + return id; +} diff --git a/src/core_plugins/kibana/common/tutorials/logstash_instructions.js b/src/core_plugins/kibana/common/tutorials/logstash_instructions.js new file mode 100644 index 0000000000000..5439e52fab653 --- /dev/null +++ b/src/core_plugins/kibana/common/tutorials/logstash_instructions.js @@ -0,0 +1,27 @@ +export const LOGSTASH_INSTRUCTIONS = { + INSTALL: { + OSX: [ + { + title: 'Download and install the Java Runtime Environment', + textPre: 'Follow the installation instructions [here](https://docs.oracle.com/javase/8/docs/technotes/guides/install/mac_jre.html).' + }, + { + title: 'Download and install Logstash', + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/logstash/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).' + }, + { + title: 'Download and install Logstash', + textPre: 'Download Logstash from [here](https://artifacts.elastic.co/downloads/logstash/logstash-{config.kibana.version}.zip) and unzip it.' + } + ], + } +}; diff --git a/src/core_plugins/kibana/common/tutorials/metricbeat_cloud_instructions.js b/src/core_plugins/kibana/common/tutorials/metricbeat_cloud_instructions.js new file mode 100644 index 0000000000000..a450045cd6436 --- /dev/null +++ b/src/core_plugins/kibana/common/tutorials/metricbeat_cloud_instructions.js @@ -0,0 +1,40 @@ +export const METRICBEAT_CLOUD_INSTRUCTIONS = { + 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.' + }, + 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.' + }, + 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.' + }, + 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.' + } + } +}; diff --git a/src/core_plugins/kibana/common/tutorials/metricbeat_instructions.js b/src/core_plugins/kibana/common/tutorials/metricbeat_instructions.js new file mode 100644 index 0000000000000..5bc7761c429d9 --- /dev/null +++ b/src/core_plugins/kibana/common/tutorials/metricbeat_instructions.js @@ -0,0 +1,147 @@ +export const METRICBEAT_INSTRUCTIONS = { + 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).', + 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).', + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/metricbeat/metricbeat-{config.kibana.version}-amd64.deb', + 'sudo dpkg -i metricbeat-{config.kibana.version}-amd64.deb' + ], + textPost: 'Looking for the 32 bits 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).', + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/metricbeat/metricbeat-{config.kibana.version}-x86_64.rpm', + 'sudo rpm -vi metricbeat-{config.kibana.version}-x86_64.rpm' + ], + textPost: 'Looking for the 32 bits 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 [downloads](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 may 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' + ], + 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', + ] + }, + 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', + ] + }, + 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', + ], + }, + WINDOWS: { + title: 'Start Metricbeat', + 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> Service-Start metricbeat', + ] + } + }, + CONFIG: { + OSX: { + title: 'Edit the configuration', + textPre: 'Modify `metricbeat.yml` to set the connection information:', + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + 'setup.kibana:', + ' host: ""' + ], + 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', + textPre: 'Modify `/etc/metricbeat/metricbeat.yml` to set the connection information:', + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + 'setup.kibana:', + ' host: ""' + ], + 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', + textPre: 'Modify `/etc/metricbeat/metricbeat.yml` to set the connection information:', + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + 'setup.kibana:', + ' host: ""' + ], + 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:', + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + 'setup.kibana:', + ' host: ""' + ], + textPost: 'Where `` is the password of the `elastic` user, ' + + '`` is the URL of Elasticsearch, and `` is the URL of Kibana.' + } + } +}; diff --git a/src/core_plugins/kibana/common/tutorials/onprem_cloud_instructions.js b/src/core_plugins/kibana/common/tutorials/onprem_cloud_instructions.js new file mode 100644 index 0000000000000..19720fb3d0e9b --- /dev/null +++ b/src/core_plugins/kibana/common/tutorials/onprem_cloud_instructions.js @@ -0,0 +1,16 @@ +export const TRYCLOUD_OPTION1 = { + title: 'Option 1: Try module in Elastic Cloud', + textPre: 'Go to [Elastic Cloud](https://cloud.elastic.co/). Register if you ' + + 'don\'t have an account.\n' + + ' * Select **Create Cluster**, leave size slider at 4 GB RAM, and click **Create**.\n' + + ' * Wait for the cluster plan to complete.\n' + + ' * Go to the new Cloud Kibana instance and follow the Kibana Home instructions.' + +}; + +export const TRYCLOUD_OPTION2 = { + 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' + + 'In **Overview >> Endpoints** note **Elasticsearch** as ``.' +}; diff --git a/src/core_plugins/kibana/common/tutorials/param_types.js b/src/core_plugins/kibana/common/tutorials/param_types.js new file mode 100644 index 0000000000000..f5e5b58ac2e00 --- /dev/null +++ b/src/core_plugins/kibana/common/tutorials/param_types.js @@ -0,0 +1,4 @@ +export const PARAM_TYPES = { + NUMBER: 'number', + STRING: 'string' +}; diff --git a/src/core_plugins/kibana/common/tutorials/tutorial_category.js b/src/core_plugins/kibana/common/tutorials/tutorial_category.js new file mode 100644 index 0000000000000..e112b3d36d251 --- /dev/null +++ b/src/core_plugins/kibana/common/tutorials/tutorial_category.js @@ -0,0 +1,5 @@ +export const TUTORIAL_CATEGORY = { + LOGGING: 'logging', + SECURITY: 'security', + METRICS: 'metrics' +}; diff --git a/src/core_plugins/kibana/common/tutorials/tutorial_schema.js b/src/core_plugins/kibana/common/tutorials/tutorial_schema.js new file mode 100644 index 0000000000000..ae83478224f4c --- /dev/null +++ b/src/core_plugins/kibana/common/tutorials/tutorial_schema.js @@ -0,0 +1,75 @@ +import Joi from 'joi'; +import { PARAM_TYPES } from './param_types'; +import { TUTORIAL_CATEGORY } from './tutorial_category'; + +const dashboardSchema = Joi.object({ + title: Joi.string().required(), + linkLabel: Joi.string().when('isOverview', { + is: true, + then: Joi.required(), + }), + // Is this an Overview / Entry Point dashboard? + isOverview: Joi.boolean().required() +}); + +const artifactsSchema = Joi.object({ + // Fields present in Elasticsearch documents created by this product. + exportedFields: Joi.object({ + documentationUrl: Joi.string() + }), + // Kibana dashboards created by this product. + dashboards: Joi.array().items(dashboardSchema).required() +}); + +const instructionSchema = Joi.object({ + title: Joi.string(), + textPre: Joi.string(), + commands: Joi.array().items(Joi.string()), + textPost: Joi.string() +}); + +const instructionVariantSchema = Joi.object({ + id: Joi.string().required(), + instructions: Joi.array().items(instructionSchema).required() +}); + +const instructionSetSchema = Joi.object({ + title: Joi.string(), + // Variants (OSes, languages, etc.) for which tutorial instructions are specified. + instructionVariants: Joi.array().items(instructionVariantSchema).required() +}); + +const paramSchema = Joi.object({ + defaultValue: Joi.required(), + id: Joi.string().regex(/^[a-zA-Z_]+$/).required(), + label: Joi.string().required(), + type: Joi.string().valid(Object.values(PARAM_TYPES)).required() +}); + +const instructionsSchema = Joi.object({ + instructionSets: Joi.array().items(instructionSetSchema).required(), + params: Joi.array().items(paramSchema) +}); + +export const tutorialSchema = { + id: Joi.string().regex(/^[a-zA-Z0-9-]+$/).required(), + category: Joi.string().valid(Object.values(TUTORIAL_CATEGORY)).required(), + name: Joi.string().required(), + shortDescription: Joi.string().required(), + iconPath: Joi.string(), + longDescription: Joi.string().required(), + completionTimeMinutes: Joi.number().integer(), + previewImagePath: Joi.string(), + + // kibana and elastic cluster running on prem + onPrem: instructionsSchema.required(), + + // kibana and elastic cluster running in elastic's cloud + elasticCloud: instructionsSchema.required(), + + // kibana running on prem and elastic cluster running in elastic's cloud + onPremElasticCloud: instructionsSchema.required(), + + // Elastic stack artifacts produced by product when it is setup and run. + artifacts: artifactsSchema, +}; diff --git a/src/core_plugins/kibana/index.js b/src/core_plugins/kibana/index.js index 7b24b12dc048d..6175bfd115599 100644 --- a/src/core_plugins/kibana/index.js +++ b/src/core_plugins/kibana/index.js @@ -8,9 +8,11 @@ import search from './server/routes/api/search'; import { scrollSearchApi } from './server/routes/api/scroll_search'; import { importApi } from './server/routes/api/import'; import { exportApi } from './server/routes/api/export'; +import { homeApi } from './server/routes/api/home'; import scripts from './server/routes/api/scripts'; import { registerSuggestionsApi } from './server/routes/api/suggestions'; import { registerFieldFormats } from './server/field_formats/register'; +import { registerTutorials } from './server/tutorials/register'; import * as systemApi from './server/lib/system_api'; import handleEsError from './server/lib/handle_es_error'; import mappings from './mappings.json'; @@ -147,9 +149,10 @@ export default function (kibana) { scrollSearchApi(server); importApi(server); exportApi(server); + homeApi(server); registerSuggestionsApi(server); registerFieldFormats(server); - + registerTutorials(server); server.expose('systemApi', systemApi); server.expose('handleEsError', handleEsError); server.expose('injectVars', injectVars); diff --git a/src/core_plugins/kibana/public/dashboard/dashboard_app.js b/src/core_plugins/kibana/public/dashboard/dashboard_app.js index 22e9295b4fbfd..cdb11f7686cd1 100644 --- a/src/core_plugins/kibana/public/dashboard/dashboard_app.js +++ b/src/core_plugins/kibana/public/dashboard/dashboard_app.js @@ -6,7 +6,7 @@ import { applyTheme } from 'ui/theme'; import 'ui/query_bar'; -import { getDashboardTitle, getUnsavedChangesWarningMessage } from './dashboard_strings'; +import { getDashboardTitle } from './dashboard_strings'; import { DashboardViewMode } from './dashboard_view_mode'; import { TopNavIds } from './top_nav/top_nav_ids'; import { ConfirmationButtonTypes } from 'ui/modals/confirm_modal'; @@ -106,6 +106,14 @@ app.directive('dashboardApp', function ($injector) { dirty: !dash.id }; + this.getSharingTitle = () => { + return dash.title; + }; + + this.getSharingType = () => { + return 'dashboard'; + }; + dashboardStateManager.registerChangeListener(status => { this.appStatus.dirty = status.dirty || !dash.id; updateState(); @@ -243,13 +251,14 @@ app.directive('dashboardApp', function ($injector) { } confirmModal( - getUnsavedChangesWarningMessage(dashboardStateManager.getChangedFilterTypes(timefilter)), + `Once you discard your changes, there's no getting them back.`, { onConfirm: revertChangesAndExitEditMode, onCancel: _.noop, - confirmButtonText: 'Yes, lose changes', - cancelButtonText: 'No, keep working', - defaultFocusedButton: ConfirmationButtonTypes.CANCEL + confirmButtonText: 'Discard changes', + cancelButtonText: 'Continue editing', + defaultFocusedButton: ConfirmationButtonTypes.CANCEL, + title: 'Discard changes to dashboard?' } ); }; diff --git a/src/core_plugins/kibana/public/dashboard/dashboard_strings.js b/src/core_plugins/kibana/public/dashboard/dashboard_strings.js index 5243ccdf49605..041bbddb81624 100644 --- a/src/core_plugins/kibana/public/dashboard/dashboard_strings.js +++ b/src/core_plugins/kibana/public/dashboard/dashboard_strings.js @@ -1,31 +1,4 @@ import { DashboardViewMode } from './dashboard_view_mode'; -import _ from 'lodash'; - -/** - * @param list {Array.} - * @returns {string} The list of strings concatenated with commas so it can be used in a message. - * E.g. ['a', 'b', 'c'] returns 'a, b, and c'. - */ -export function createStringList(list) { - const listClone = _.clone(list); - const isPlural = list.length > 1; - const lastEntry = isPlural ? `, and ${list[list.length - 1]}` : ''; - if (isPlural) listClone.splice(-1, 1); - - return `${listClone.join(', ')}${lastEntry}`; -} - -/** - * @param changedFilters {Array.} An optional list of filter types that have changed. - * @returns {string} A warning message to display to the user that they are going to lose changes. - */ -export function getUnsavedChangesWarningMessage(changedFilters) { - const changedFilterList = createStringList(changedFilters); - - return changedFilterList ? - `Are you sure you want to cancel and lose changes, including changes made to your ${changedFilterList}?` : - `Are you sure you want to cancel and lose changes?`; -} /** * @param title {string} the current title of the dashboard diff --git a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js index 5928ae6cd31ca..a6d7c6df98ab2 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js +++ b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js @@ -132,11 +132,12 @@ export function DashboardListingController($injector, $scope, $location) { }; confirmModal( - 'Are you sure you want to delete the selected dashboards? This action is irreversible!', + `You can't recover deleted dashboards.`, { confirmButtonText: 'Delete', onConfirm: doDelete, - defaultFocusedButton: ConfirmationButtonTypes.CANCEL + defaultFocusedButton: ConfirmationButtonTypes.CANCEL, + title: 'Delete selected dashboards?' }); }; diff --git a/src/core_plugins/kibana/public/dashboard/styles/index.less b/src/core_plugins/kibana/public/dashboard/styles/index.less index 2646068c8efea..413cf8661984c 100644 --- a/src/core_plugins/kibana/public/dashboard/styles/index.less +++ b/src/core_plugins/kibana/public/dashboard/styles/index.less @@ -378,6 +378,10 @@ dashboard-viewport-provider { z-index: 1; /* 1. */ padding: 0 8px 8px 8px; } + + .panel-content--fullWidth { + width: 100%; + } } /** diff --git a/src/core_plugins/kibana/public/dashboard/top_nav/__snapshots__/clone_modal.test.js.snap b/src/core_plugins/kibana/public/dashboard/top_nav/__snapshots__/clone_modal.test.js.snap index e8ba766015382..99533bc4638eb 100644 --- a/src/core_plugins/kibana/public/dashboard/top_nav/__snapshots__/clone_modal.test.js.snap +++ b/src/core_plugins/kibana/public/dashboard/top_nav/__snapshots__/clone_modal.test.js.snap @@ -1,71 +1,57 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders DashboardCloneModal 1`] = ` -
-
-
-
-
- Clone Dashboard -
-
-
-

+ + + + + Clone Dashboard + + + + +

Please enter a new name for your dashboard.

-
- -
-
-
+ + + + + + Cancel + + - - -
-
-
-
+ Confirm Clone + + + + `; diff --git a/src/core_plugins/kibana/public/dashboard/top_nav/clone_modal.js b/src/core_plugins/kibana/public/dashboard/top_nav/clone_modal.js index 13ab0666e279c..3d73b46eb20d4 100644 --- a/src/core_plugins/kibana/public/dashboard/top_nav/clone_modal.js +++ b/src/core_plugins/kibana/public/dashboard/top_nav/clone_modal.js @@ -2,14 +2,17 @@ import React from 'react'; import PropTypes from 'prop-types'; import { - KuiModal, - KuiModalHeader, - KuiModalHeaderTitle, - KuiModalBody, - KuiModalFooter, - KuiButton, - KuiModalOverlay -} from 'ui_framework/components'; + EuiButton, + EuiFieldText, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiOverlayMask, + EuiSpacer, + EuiText, +} from '@elastic/eui'; export class DashboardCloneModal extends React.Component { constructor(props) { @@ -30,53 +33,53 @@ export class DashboardCloneModal extends React.Component { render() { return ( - - + - - + + Clone Dashboard - - + + - -

- Please enter a new name for your dashboard. -

+ + +

+ Please enter a new name for your dashboard. +

+
-
- -
-
+ - - + + + + Cancel - + - Confirm Clone - - -
-
+ + + + ); } } diff --git a/src/core_plugins/kibana/public/dashboard/top_nav/clone_modal.test.js b/src/core_plugins/kibana/public/dashboard/top_nav/clone_modal.test.js index 59310258cf623..3e80afdd77f43 100644 --- a/src/core_plugins/kibana/public/dashboard/top_nav/clone_modal.test.js +++ b/src/core_plugins/kibana/public/dashboard/top_nav/clone_modal.test.js @@ -1,6 +1,6 @@ import React from 'react'; import sinon from 'sinon'; -import { mount, render } from 'enzyme'; +import { mount, shallow } from 'enzyme'; import { findTestSubject, } from '@elastic/eui/lib/test'; @@ -29,7 +29,7 @@ function createComponent(creationMethod = mount) { } test('renders DashboardCloneModal', () => { - createComponent(render); + createComponent(shallow); expect(component).toMatchSnapshot(); // eslint-disable-line }); 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 fb573c03498b1..c79ac1078b5d6 100644 --- a/src/core_plugins/kibana/public/home/components/feature_directory.js +++ b/src/core_plugins/kibana/public/home/components/feature_directory.js @@ -52,7 +52,7 @@ 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} @@ -97,10 +97,10 @@ export class FeatureDirectory extends React.Component {
- + {this.renderTabs()} - + { this.renderDirectories() }
diff --git a/src/core_plugins/kibana/public/home/components/home.js b/src/core_plugins/kibana/public/home/components/home.js index a150ea8ec33c8..4b1e53f2ff7cc 100644 --- a/src/core_plugins/kibana/public/home/components/home.js +++ b/src/core_plugins/kibana/public/home/components/home.js @@ -3,6 +3,12 @@ import PropTypes from 'prop-types'; import { Synopsis } from './synopsis'; import { KuiLinkButton, + KuiCardGroup, + KuiCard, + KuiCardDescription, + KuiCardDescriptionTitle, + KuiCardDescriptionText, + KuiCardFooter, } from 'ui_framework/components'; import { @@ -18,6 +24,9 @@ import { import { FeatureCatalogueCategory } from 'ui/registry/feature_catalogue'; +import chrome from 'ui/chrome'; +const kbnBaseUrl = chrome.getInjected('kbnBaseUrl'); + export function Home({ addBasePath, directories }) { const renderDirectories = (category) => { @@ -39,6 +48,95 @@ export function Home({ addBasePath, directories }) { }); }; + const renderPromo = () => { + const cardStyle = { + width: '250px', + 'minWidth': '200px' + }; + return ( +
+ + + + + +

+ Logging +

+
+ + + Ingest logs from popular data sources and easily visualize in preconfigured dashboards. + +
+ + + + Add data + + +
+ + + + + +

+ Metrics +

+
+ + + Collect metrics from the operating system and services running on your servers. + +
+ + + + Add data + + +
+ + + + + +

+ Security Analytics +

+
+ + + Centralize security events for interactive investigation in ready-to-go visualizations. + +
+ + + + Add data + + +
+
+
+ ); + }; return ( @@ -49,7 +147,7 @@ export function Home({ addBasePath, directories }) { > -

Welcome to Kibana

+

Add Data to Kibana

@@ -74,6 +172,13 @@ export function Home({ addBasePath, directories }) { +

+ These turnkey solutions will help you quickly add data into Kibana and turn it into + pre-built dashboards and monitoring systems. +

+ + { renderPromo() } + diff --git a/src/core_plugins/kibana/public/home/components/home_app.js b/src/core_plugins/kibana/public/home/components/home_app.js index 2a5622fda45a2..4256f9a94f713 100644 --- a/src/core_plugins/kibana/public/home/components/home_app.js +++ b/src/core_plugins/kibana/public/home/components/home_app.js @@ -2,16 +2,51 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Home } from './home'; import { FeatureDirectory } from './feature_directory'; +import { TutorialDirectory } from './tutorial_directory'; +import { Tutorial } from './tutorial/tutorial'; import { HashRouter as Router, Switch, Route } from 'react-router-dom'; +import { getTutorial } from '../load_tutorials'; +import { replaceTemplateStrings } from './tutorial/replace_template_strings'; +import chrome from 'ui/chrome'; export function HomeApp({ addBasePath, directories }) { + + const renderTutorialDirectory = (props) => { + return ( + + ); + }; + + const renderTutorial = (props) => { + return ( + + ); + }; + return ( + + diff --git a/src/core_plugins/kibana/public/home/components/synopsis.js b/src/core_plugins/kibana/public/home/components/synopsis.js index 56a0fb7145e61..a336d777860e0 100644 --- a/src/core_plugins/kibana/public/home/components/synopsis.js +++ b/src/core_plugins/kibana/public/home/components/synopsis.js @@ -19,7 +19,11 @@ export function Synopsis({ description, iconUrl, title, url }) { } return ( - + {img} 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 new file mode 100644 index 0000000000000..95d8e79fb5eef --- /dev/null +++ b/src/core_plugins/kibana/public/home/components/tutorial/__snapshots__/content.test.js.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render content with markdown 1`] = ` +
I am some content with markdown

+", + } + } +/> +`; 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 new file mode 100644 index 0000000000000..72a0209d9d6f4 --- /dev/null +++ b/src/core_plugins/kibana/public/home/components/tutorial/__snapshots__/tutorial.test.js.snap @@ -0,0 +1,133 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render ELASTIC_CLOUD instructions when isCloudEnabled is true 1`] = ` +
+
+ + Home + + / + + Add Data + +
+ +
+
+ +
+
+
+
+`; + +exports[`should render ON_PREM instructions with instructions cloud toggle when isCloudEnabled is false 1`] = ` +
+
+ + Home + + / + + Add Data + +
+ +
+ +
+
+ +
+
+
+
+`; diff --git a/src/core_plugins/kibana/public/home/components/tutorial/content.js b/src/core_plugins/kibana/public/home/components/tutorial/content.js new file mode 100644 index 0000000000000..abba5839bb30a --- /dev/null +++ b/src/core_plugins/kibana/public/home/components/tutorial/content.js @@ -0,0 +1,36 @@ +import classNames from 'classnames'; +import React from 'react'; +import PropTypes from 'prop-types'; +import MarkdownIt from 'markdown-it'; + +const markdownIt = new MarkdownIt('zero', { html: false, linkify: true }); +// list of rules can be found at https://github.com/markdown-it/markdown-it/issues/361 +markdownIt.enable(['backticks', 'emphasis', 'link', 'list']); + +// All links should open in new browser tab. +// Define custom renderer to add 'target' attribute +// https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#renderer +const originalLinkRender = markdownIt.renderer.rules.link_open || function (tokens, idx, options, env, self) { + return self.renderToken(tokens, idx, options); +}; +markdownIt.renderer.rules.link_open = function (tokens, idx, options, env, self) { + tokens[idx].attrPush(['target', '_blank']); + // https://www.jitbit.com/alexblog/256-targetblank---the-most-underestimated-vulnerability-ever/ + tokens[idx].attrPush(['rel', 'noopener noreferrer']); + return originalLinkRender(tokens, idx, options, env, self); +}; + +export function Content({ className, text }) { + const classes = classNames('kuiText kuiSubduedText tutorialContent markdown-body', className); + return ( +
+ ); +} + +Content.propTypes = { + className: PropTypes.string, + text: PropTypes.string.isRequired +}; diff --git a/src/core_plugins/kibana/public/home/components/tutorial/content.test.js b/src/core_plugins/kibana/public/home/components/tutorial/content.test.js new file mode 100644 index 0000000000000..2dac509645c64 --- /dev/null +++ b/src/core_plugins/kibana/public/home/components/tutorial/content.test.js @@ -0,0 +1,13 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { + Content, +} from './content'; + +test('should render content with markdown', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); // eslint-disable-line +}); diff --git a/src/core_plugins/kibana/public/home/components/tutorial/copy_button.js b/src/core_plugins/kibana/public/home/components/tutorial/copy_button.js new file mode 100644 index 0000000000000..ffb45ddbff361 --- /dev/null +++ b/src/core_plugins/kibana/public/home/components/tutorial/copy_button.js @@ -0,0 +1,60 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Tooltip } from 'pivotal-ui/react/tooltip'; +import { OverlayTrigger } from 'pivotal-ui/react/overlay-trigger'; +import { KuiButton } from 'ui_framework/components'; +import { copyToClipboard } from '../../copy_to_clipboard'; + +const UNCOPIED_MSG = 'Copy to clipboard'; +const COPIED_MSG = 'Copied'; + +export class CopyButton extends React.Component { + + constructor(props) { + super(props); + + this.state = { + tooltipText: UNCOPIED_MSG + }; + } + + copySnippet = () => { + const isCopied = copyToClipboard(this.props.textToCopy); + if (isCopied) { + this.setState({ + tooltipText: COPIED_MSG, + }); + } + } + + resetTooltipText = () => { + this.setState({ + tooltipText: UNCOPIED_MSG, + }); + } + + render() { + return ( + + {this.state.tooltipText} + + } + > + + Copy snippet + + + ); + } +} + +CopyButton.propTypes = { + textToCopy: PropTypes.string.isRequired, +}; diff --git a/src/core_plugins/kibana/public/home/components/tutorial/instruction.js b/src/core_plugins/kibana/public/home/components/tutorial/instruction.js new file mode 100644 index 0000000000000..02fef8807c2ca --- /dev/null +++ b/src/core_plugins/kibana/public/home/components/tutorial/instruction.js @@ -0,0 +1,84 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Content } from './content'; +import { CopyButton } from './copy_button'; + +import { + EuiCodeBlock, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, +} from '@elastic/eui'; + +export function Instruction({ commands, paramValues, textPost, textPre, replaceTemplateStrings }) { + let pre; + if (textPre) { + pre = ( + + ); + } + + let post; + if (textPost) { + post = ( +
+ + +
+ ); + } + + let copyButton; + let commandBlock; + if (commands) { + const cmdText = commands.map(cmd => { return replaceTemplateStrings(cmd, paramValues); }).join('\n'); + copyButton = ( + + ); + commandBlock = ( +
+ + + {cmdText} + +
+ ); + } + + return ( +
+ + + + {pre} + + + + {copyButton} + + + + {commandBlock} + + {post} + +
+ ); +} + +Instruction.propTypes = { + commands: PropTypes.array, + paramValues: PropTypes.object.isRequired, + textPost: PropTypes.string, + textPre: PropTypes.string, + replaceTemplateStrings: PropTypes.func.isRequired, +}; 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 new file mode 100644 index 0000000000000..a9cf9d4ce4d89 --- /dev/null +++ b/src/core_plugins/kibana/public/home/components/tutorial/instruction_set.js @@ -0,0 +1,186 @@ +import classNames from 'classnames'; +import React from 'react'; +import PropTypes from 'prop-types'; +import { + KuiBar, + KuiBarSection, +} from 'ui_framework/components'; +import { Instruction } from './instruction'; +import { ParameterForm } from './parameter_form'; +import { getDisplayText } from '../../../../common/tutorials/instruction_variant'; +import { + EuiTabs, + EuiTab, + EuiSpacer, + EuiSteps, +} from '@elastic/eui'; + +export class InstructionSet extends React.Component { + + constructor(props) { + super(props); + + this.tabs = props.instructionVariants.map((variant) => { + return { + id: variant.id, + name: getDisplayText(variant.id) + }; + }); + + this.state = { + isParamFormVisible: false + }; + + if (this.tabs.length > 0) { + this.state.selectedTabId = this.tabs[0].id; + } + } + + handleToggleVisibility = () => { + this.setState(prevState => ( + { isParamFormVisible: !prevState.isParamFormVisible } + )); + } + + onSelectedTabChanged = id => { + this.setState({ + selectedTabId: id, + }); + }; + + renderTabs = () => { + return this.tabs.map((tab, index) => ( + this.onSelectedTabChanged(tab.id)} + isSelected={tab.id === this.state.selectedTabId} + key={index} + > + {tab.name} + + )); + } + + renderInstructions = () => { + const instructionVariant = this.props.instructionVariants.find(variant => { + return variant.id === this.state.selectedTabId; + }); + if (!instructionVariant) { + return; + } + + const steps = instructionVariant.instructions.map((instruction, index) => { + const step = ( + + ); + return { + title: instruction.title, + children: step, + key: index + }; + }); + + return ( + + ); + } + + renderHeader = () => { + let paramsVisibilityToggle; + if (this.props.params) { + const visibilityToggleClasses = classNames('kuiIcon kuiSideBarCollapsibleTitle__caret', { + 'fa-caret-right': !this.state.isParamFormVisible, + 'fa-caret-down': this.state.isParamFormVisible + }); + paramsVisibilityToggle = ( +
+
+ + + Customize your code snippets + +
+
+ ); + } + + return ( + + +
+ {this.props.title} +
+
+ + + {paramsVisibilityToggle} + +
+ ); + } + + render() { + let paramsForm; + if (this.props.params && this.state.isParamFormVisible) { + paramsForm = ( + + ); + } + + return ( +
+ + {this.renderHeader()} + + {paramsForm} + + + {this.renderTabs()} + + + + + {this.renderInstructions()} + +
+ ); + } +} + +const instructionShape = PropTypes.shape({ + title: PropTypes.string, + textPre: PropTypes.string, + commands: PropTypes.arrayOf(PropTypes.string), + textPost: PropTypes.string +}); + +const instructionVariantShape = PropTypes.shape({ + id: PropTypes.string.isRequired, + instructions: PropTypes.arrayOf(instructionShape).isRequired, +}); + +InstructionSet.propTypes = { + title: PropTypes.string.isRequired, + instructionVariants: PropTypes.arrayOf(instructionVariantShape).isRequired, + offset: PropTypes.number.isRequired, + params: PropTypes.array, + paramValues: PropTypes.object.isRequired, + setParameter: PropTypes.func, + replaceTemplateStrings: PropTypes.func.isRequired, +}; diff --git a/src/core_plugins/kibana/public/home/components/tutorial/introduction.js b/src/core_plugins/kibana/public/home/components/tutorial/introduction.js new file mode 100644 index 0000000000000..54e16e5094223 --- /dev/null +++ b/src/core_plugins/kibana/public/home/components/tutorial/introduction.js @@ -0,0 +1,48 @@ +import './introduction.less'; +import React from 'react'; +import PropTypes from 'prop-types'; +import { Content } from './content'; + +import { + EuiImage, +} from '@elastic/eui'; + +export function Introduction({ description, previewUrl, title }) { + let img; + if (previewUrl) { + img = ( + + ); + } + return ( +
+
+ +
+

+ {title} +

+ +
+ +
+ {img} +
+ +
+
+ ); +} + +Introduction.propTypes = { + description: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + previewUrl: PropTypes.string +}; diff --git a/src/core_plugins/kibana/public/home/components/tutorial/introduction.less b/src/core_plugins/kibana/public/home/components/tutorial/introduction.less new file mode 100644 index 0000000000000..cd3b8d106fef8 --- /dev/null +++ b/src/core_plugins/kibana/public/home/components/tutorial/introduction.less @@ -0,0 +1,3 @@ +.introduction { + margin: 24px; +} diff --git a/src/core_plugins/kibana/public/home/components/tutorial/number_parameter.js b/src/core_plugins/kibana/public/home/components/tutorial/number_parameter.js new file mode 100644 index 0000000000000..c3f94ed02fcdc --- /dev/null +++ b/src/core_plugins/kibana/public/home/components/tutorial/number_parameter.js @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +export function NumberParameter({ id, label, value, setParameter }) { + const handleChange = (evt) => { + setParameter(id, parseFloat(evt.target.value)); + }; + + return ( +
+ +
+ +
+
+ ); +} + +NumberParameter.propTypes = { + id: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + value: PropTypes.number.isRequired, + setParameter: PropTypes.func.isRequired, +}; diff --git a/src/core_plugins/kibana/public/home/components/tutorial/parameter_form.js b/src/core_plugins/kibana/public/home/components/tutorial/parameter_form.js new file mode 100644 index 0000000000000..686cb1742aab6 --- /dev/null +++ b/src/core_plugins/kibana/public/home/components/tutorial/parameter_form.js @@ -0,0 +1,60 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { NumberParameter } from './number_parameter'; +import { StringParameter } from './string_parameter'; + +export class ParameterForm extends React.Component { + + renderInputs = () => { + return this.props.params.map(param => { + switch (param.type) { + case 'number': + return ( + + ); + case 'string': + return ( + + ); + default: + throw new Error(`Unhandled parameter type ${param.type}`); + } + }); + } + + render() { + return ( +
+ +
+ {this.renderInputs()} +
+ +
+ ); + } +} + +const paramsShape = PropTypes.shape({ + id: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, +}); + +ParameterForm.propTypes = { + params: PropTypes.arrayOf(paramsShape).isRequired, + paramValues: PropTypes.object.isRequired, + setParameter: PropTypes.func.isRequired +}; 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 new file mode 100644 index 0000000000000..9897f00cbbb24 --- /dev/null +++ b/src/core_plugins/kibana/public/home/components/tutorial/radio_button_group.js @@ -0,0 +1,74 @@ +import './radio_button_group.less'; +import React from 'react'; +import PropTypes from 'prop-types'; +import { + KuiButtonGroup, + KuiButton +} from '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 new file mode 100644 index 0000000000000..8630f2caf2570 --- /dev/null +++ b/src/core_plugins/kibana/public/home/components/tutorial/radio_button_group.less @@ -0,0 +1,10 @@ + +// 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/replace_template_strings.js b/src/core_plugins/kibana/public/home/components/tutorial/replace_template_strings.js new file mode 100644 index 0000000000000..63c3ede5cf603 --- /dev/null +++ b/src/core_plugins/kibana/public/home/components/tutorial/replace_template_strings.js @@ -0,0 +1,35 @@ +import Mustache from 'mustache'; +import chrome from 'ui/chrome'; +import { metadata } from 'ui/metadata'; +import { + DOC_LINK_VERSION, + ELASTIC_WEBSITE_URL, + documentationLinks +} from 'ui/documentation_links/documentation_links'; + +const TEMPLATE_TAGS = ['{', '}']; + +export function replaceTemplateStrings(text, params = {}) { + const variables = { + config: { + cloud: { + id: chrome.getInjected('cloudId') + }, + docs: { + base_url: ELASTIC_WEBSITE_URL, + beats: { + filebeat: documentationLinks.filebeat.base, + metricbeat: documentationLinks.metricbeat.base + }, + logstash: documentationLinks.logstash.base, + version: DOC_LINK_VERSION + }, + kibana: { + version: metadata.version + } + }, + params: params + }; + Mustache.parse(text, TEMPLATE_TAGS); + return Mustache.render(text, variables); +} diff --git a/src/core_plugins/kibana/public/home/components/tutorial/string_parameter.js b/src/core_plugins/kibana/public/home/components/tutorial/string_parameter.js new file mode 100644 index 0000000000000..e8e425819d6c8 --- /dev/null +++ b/src/core_plugins/kibana/public/home/components/tutorial/string_parameter.js @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +export function StringParameter({ id, label, value, setParameter }) { + const handleChange = (evt) => { + setParameter(id, evt.target.value); + }; + + return ( +
+ +
+ +
+
+ ); +} + +StringParameter.propTypes = { + id: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + setParameter: PropTypes.func.isRequired, +}; diff --git a/src/core_plugins/kibana/public/home/components/tutorial/tutorial.js b/src/core_plugins/kibana/public/home/components/tutorial/tutorial.js new file mode 100644 index 0000000000000..1c5c319f6d32e --- /dev/null +++ b/src/core_plugins/kibana/public/home/components/tutorial/tutorial.js @@ -0,0 +1,202 @@ +import './tutorial.less'; +import _ from 'lodash'; +import React from 'react'; +import PropTypes from 'prop-types'; +import { Introduction } from './introduction'; +import { InstructionSet } from './instruction_set'; +import { RadioButtonGroup } from './radio_button_group'; + +const INSTRUCTIONS_TYPE = { + ELASTIC_CLOUD: 'elasticCloud', + ON_PREM: 'onPrem', + ON_PREM_ELASTIC_CLOUD: 'onPremElasticCloud' +}; + +export class Tutorial extends React.Component { + + constructor(props) { + super(props); + + this.state = { + notFound: false, + paramValues: {}, + tutorial: null + }; + + if (props.isCloudEnabled) { + this.state.visibleInstructions = INSTRUCTIONS_TYPE.ELASTIC_CLOUD; + } else { + this.state.visibleInstructions = INSTRUCTIONS_TYPE.ON_PREM; + } + } + + componentWillMount() { + this._isMounted = true; + } + + componentWillUnmount() { + this._isMounted = false; + } + + async componentDidMount() { + const tutorial = await this.props.getTutorial(this.props.tutorialId); + + if (!this._isMounted) { + return; + } + + if (tutorial) { + // eslint-disable-next-line react/no-did-mount-set-state + this.setState({ + tutorial: tutorial + }, this.setParamDefaults); + } else { + // eslint-disable-next-line react/no-did-mount-set-state + this.setState({ + notFound: true, + }); + } + } + + getInstructions = () => { + if (!this.state.tutorial) { + return { instructionSets: [] }; + } + + switch(this.state.visibleInstructions) { + case INSTRUCTIONS_TYPE.ELASTIC_CLOUD: + return this.state.tutorial.elasticCloud; + case INSTRUCTIONS_TYPE.ON_PREM: + return this.state.tutorial.onPrem; + case INSTRUCTIONS_TYPE.ON_PREM_ELASTIC_CLOUD: + return this.state.tutorial.onPremElasticCloud; + default: + throw new Error(`Unhandled instruction type ${this.state.visibleInstructions}`); + } + } + + setParamDefaults = () => { + const instructions = this.getInstructions(); + const paramValues = {}; + if (instructions.params) { + instructions.params.forEach((param => { + paramValues[param.id] = param.defaultValue; + })); + } + this.setState({ + paramValues: paramValues + }); + } + + setVisibleInstructions = (instructionsType) => { + this.setState({ + visibleInstructions: instructionsType + }, this.setParamDefaults); + } + + setParameter = (paramId, newValue) => { + this.setState(previousState => { + const paramValues = _.cloneDeep(previousState.paramValues); + paramValues[paramId] = newValue; + return { paramValues: paramValues }; + }); + } + + onPrem = () => { + this.setVisibleInstructions(INSTRUCTIONS_TYPE.ON_PREM); + } + + onPremElasticCloud = () => { + this.setVisibleInstructions(INSTRUCTIONS_TYPE.ON_PREM_ELASTIC_CLOUD); + } + + renderInstructionSetsToggle = () => { + if (!this.props.isCloudEnabled) { + const radioButtons = [ + { onClick: this.onPrem, label: 'On premise', dataTestSubj: 'onPremBtn' }, + { onClick: this.onPremElasticCloud, label: 'Elastic Cloud', dataTestSubj: 'onPremElasticCloudBtn' }, + ]; + return ( + + ); + } + } + + renderInstructionSets = (instructions) => { + let offset = 1; + return instructions.instructionSets.map((instructionSet, index) => { + const currentOffset = offset; + offset += instructionSet.instructionVariants[0].instructions.length; + return ( + + ); + }); + } + + render() { + let content; + if (this.state.notFound) { + content = ( +
+

+ Unable to find tutorial {this.props.tutorialId} +

+
+ ); + } + + if (this.state.tutorial) { + let previewUrl; + if (this.state.tutorial.previewImagePath) { + previewUrl = this.props.addBasePath(this.state.tutorial.previewImagePath); + } + + const instructions = this.getInstructions(); + content = ( +
+ + +
+ {this.renderInstructionSetsToggle()} +
+ +
+ {this.renderInstructionSets(instructions)} +
+
+ ); + } + return ( +
+
+ Home / Add Data + {content} +
+
+ ); + } +} + +Tutorial.propTypes = { + addBasePath: PropTypes.func.isRequired, + isCloudEnabled: PropTypes.bool.isRequired, + getTutorial: PropTypes.func.isRequired, + replaceTemplateStrings: PropTypes.func.isRequired, + tutorialId: PropTypes.string.isRequired +}; diff --git a/src/core_plugins/kibana/public/home/components/tutorial/tutorial.less b/src/core_plugins/kibana/public/home/components/tutorial/tutorial.less new file mode 100644 index 0000000000000..ddc39e54f04f0 --- /dev/null +++ b/src/core_plugins/kibana/public/home/components/tutorial/tutorial.less @@ -0,0 +1,17 @@ +.tutorialContent { + /* + * 1. remove bottom margin placed on p element by bootstrap + */ + p { + margin-bottom: 0; /* 1 */ + } +} + +.text-center > .kuiButtonGroup { + display: inline-block !important; +} + +.homePanel { + background: white; + padding: 24px; +} diff --git a/src/core_plugins/kibana/public/home/components/tutorial/tutorial.test.js b/src/core_plugins/kibana/public/home/components/tutorial/tutorial.test.js new file mode 100644 index 0000000000000..c20f83e272f41 --- /dev/null +++ b/src/core_plugins/kibana/public/home/components/tutorial/tutorial.test.js @@ -0,0 +1,87 @@ +import React from 'react'; +import { shallow, mount } from 'enzyme'; +import { findTestSubject } from '@elastic/eui/lib/test'; + +import { + Tutorial, +} from './tutorial'; + +function buildInstructionSet(type) { + return { + instructionSets: [ + { + title: 'Instruction title', + instructionVariants: [ + { + id: 'platform id', + instructions: [ + { + title: `${type} instructions`, + } + ] + } + ] + } + ] + }; +} +const tutorial = { + name: 'jest test tutorial', + longDescription: 'tutorial used to drive jest tests', + elasticCloud: buildInstructionSet('elasticCloud'), + onPrem: buildInstructionSet('onPrem'), + onPremElasticCloud: buildInstructionSet('onPremElasticCloud') +}; +const loadTutorialPromise = Promise.resolve(tutorial); +const getTutorial = () => { + return loadTutorialPromise; +}; +const addBasePath = (path) => { + return `BASE_PATH/${path}`; +}; +const replaceTemplateStrings = (text) => { + return text; +}; + +test('should render ON_PREM instructions with instructions cloud toggle when isCloudEnabled is false', () => { + const component = shallow(); + loadTutorialPromise.then(() => { + component.update(); + expect(component).toMatchSnapshot(); // eslint-disable-line + }); +}); + +test('should render ELASTIC_CLOUD instructions when isCloudEnabled is true', () => { + const component = shallow(); + loadTutorialPromise.then(() => { + component.update(); + expect(component).toMatchSnapshot(); // eslint-disable-line + }); +}); + +test('should display ON_PREM_ELASTIC_CLOUD instructions when cloud toggle is clicked', () => { + const component = mount(); + loadTutorialPromise.then(() => { + component.update(); + findTestSubject(component, 'onPremElasticCloudBtn').simulate('click'); + expect(component.state('visibleInstructions')).toBe('onPremElasticCloud'); + }); +}); diff --git a/src/core_plugins/kibana/public/home/components/tutorial_directory.js b/src/core_plugins/kibana/public/home/components/tutorial_directory.js new file mode 100644 index 0000000000000..c8ab44334bc7a --- /dev/null +++ b/src/core_plugins/kibana/public/home/components/tutorial_directory.js @@ -0,0 +1,120 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Synopsis } from './synopsis'; +import { + KuiTabs, + KuiTab, + KuiFlexItem, + KuiFlexGrid, +} from 'ui_framework/components'; +import { getTutorials } from '../load_tutorials'; + +const ALL = 'all'; + +export class TutorialDirectory extends React.Component { + + constructor(props) { + super(props); + + this.tabs = [{ + id: ALL, + name: 'All', + }, { + id: 'logging', + name: 'Logging', + }, { + id: 'metrics', + name: 'Metrics', + }, { + id: 'security', + name: 'Security Analytics', + }]; + + let openTab = ALL; + if (props.openTab && this.tabs.some(tab => { return tab.id === props.openTab; })) { + openTab = props.openTab; + } + this.state = { + selectedTabId: openTab, + tutorials: [] + }; + } + + async componentWillMount() { + const tutorials = await getTutorials(); + this.setState({ + tutorials: tutorials, + }); + } + + onSelectedTabChanged = id => { + this.setState({ + selectedTabId: id, + }); + }; + + renderTabs = () => { + return this.tabs.map((tab, index) => ( + this.onSelectedTabChanged(tab.id)} + isSelected={tab.id === this.state.selectedTabId} + key={index} + > + {tab.name} + + )); + } + + renderTutorials = () => { + return this.state.tutorials + .filter((tutorial) => { + if (this.state.selectedTabId === ALL) { + return true; + } + return this.state.selectedTabId === tutorial.category; + }) + .map((tutorial) => { + return ( + + + + ); + }); + }; + + render() { + return ( +
+
+ +
+ Home +

+ Add Data to Kibana +

+
+ +
+ + {this.renderTabs()} + + + { this.renderTutorials() } + +
+ +
+
+ ); + } +} + +TutorialDirectory.propTypes = { + addBasePath: PropTypes.func.isRequired, + openTab: PropTypes.string +}; diff --git a/src/core_plugins/kibana/public/home/copy_to_clipboard.js b/src/core_plugins/kibana/public/home/copy_to_clipboard.js new file mode 100644 index 0000000000000..aee6ed3472690 --- /dev/null +++ b/src/core_plugins/kibana/public/home/copy_to_clipboard.js @@ -0,0 +1,47 @@ + +function createHiddenTextElement(text) { + const textElement = document.createElement('span'); + textElement.textContent = text; + textElement.style.all = 'unset'; + // prevents scrolling to the end of the page + textElement.style.position = 'fixed'; + textElement.style.top = 0; + textElement.style.clip = 'rect(0, 0, 0, 0)'; + // used to preserve spaces and line breaks + textElement.style.whiteSpace = 'pre'; + // do not inherit user-select (it may be `none`) + textElement.style.webkitUserSelect = 'text'; + textElement.style.MozUserSelect = 'text'; + textElement.style.msUserSelect = 'text'; + textElement.style.userSelect = 'text'; + return textElement; +} + +export function copyToClipboard(text) { + let isCopied = true; + const range = document.createRange(); + const selection = document.getSelection(); + const elementToBeCopied = createHiddenTextElement(text); + + document.body.appendChild(elementToBeCopied); + range.selectNode(elementToBeCopied); + selection.empty(); + selection.addRange(range); + + if (!document.execCommand('copy')) { + isCopied = false; + console.warn('Unable to copy to clipboard.'); // eslint-disable-line no-console + } + + if (selection) { + if (typeof selection.removeRange === 'function') { + selection.removeRange(range); + } else { + selection.removeAllRanges(); + } + } + + document.body.removeChild(elementToBeCopied); + + return isCopied; +} diff --git a/src/core_plugins/kibana/public/home/home.less b/src/core_plugins/kibana/public/home/home.less index b96f1dbd95097..df4ed0ddfb0ca 100644 --- a/src/core_plugins/kibana/public/home/home.less +++ b/src/core_plugins/kibana/public/home/home.less @@ -5,7 +5,19 @@ min-height: 100vh; } -.homeFeatureDirectory { +.kuiCard__descriptionTitle img { + display: inline-block; + width: 64px; + height: 64px; + margin-bottom: 10px; +} + +.kuiCardGroup .kuiCard { + flex: 1 1 0 !important; + background-color: white; +} + +.homeDirectory { background: @white; margin: 0; border-left: 1px solid @globalColorLightGray; @@ -14,6 +26,6 @@ padding: 16px; } -.homeFeatureCategoryTab { +.homeDirectoryTab { background-color: @globalColorLightestGray; } diff --git a/src/core_plugins/kibana/public/home/index.js b/src/core_plugins/kibana/public/home/index.js index 3098ccd508be1..15e3999157fd4 100644 --- a/src/core_plugins/kibana/public/home/index.js +++ b/src/core_plugins/kibana/public/home/index.js @@ -27,3 +27,5 @@ function getRoute() { // redirect us to the default page by encountering a url it isn't marked as being able to handle. routes.when('/home', getRoute()); routes.when('/home/feature_directory', getRoute()); +routes.when('/home/tutorial_directory/:tab?', getRoute()); +routes.when('/home/tutorial/:id', getRoute()); diff --git a/src/core_plugins/kibana/public/home/load_tutorials.js b/src/core_plugins/kibana/public/home/load_tutorials.js new file mode 100644 index 0000000000000..46aeec6ff6add --- /dev/null +++ b/src/core_plugins/kibana/public/home/load_tutorials.js @@ -0,0 +1,52 @@ +import _ from 'lodash'; +import chrome from 'ui/chrome'; +import { notify } from 'ui/notify'; + +const baseUrl = chrome.addBasePath('/api/kibana/home/tutorials'); +const headers = new Headers(); +headers.append('Accept', 'application/json'); +headers.append('Content-Type', 'application/json'); +headers.append('kbn-xsrf', 'kibana'); + +let tutorials = []; +let turorialsLoaded = false; + +async function loadTutorials() { + try { + const response = await fetch(baseUrl, { + method: 'get', + credentials: 'include', + headers: headers, + }); + if (response.status >= 300) { + throw new Error(`Request failed with status code: ${response.status}`); + } + + tutorials = await response.json(); + turorialsLoaded = true; + } catch(err) { + notify.error(`Unable to load tutorials, ${err}`); + } +} + +export async function getTutorials() { + if (!turorialsLoaded) { + await loadTutorials(); + } + + return _.cloneDeep(tutorials); +} + +export async function getTutorial(id) { + if (!turorialsLoaded) { + await loadTutorials(); + } + + const tutorial = tutorials.find(tutorial => { + return tutorial.id === id; + }); + + if (tutorial) { + return _.cloneDeep(tutorial); + } +} diff --git a/src/core_plugins/kibana/public/home/tutorial_resources/apache_logs/screenshot.png b/src/core_plugins/kibana/public/home/tutorial_resources/apache_logs/screenshot.png new file mode 100644 index 0000000000000..a2039096ce041 Binary files /dev/null and b/src/core_plugins/kibana/public/home/tutorial_resources/apache_logs/screenshot.png differ diff --git a/src/core_plugins/kibana/public/home/tutorial_resources/apache_metrics/screenshot.png b/src/core_plugins/kibana/public/home/tutorial_resources/apache_metrics/screenshot.png new file mode 100644 index 0000000000000..ac40d5a637a5f Binary files /dev/null and b/src/core_plugins/kibana/public/home/tutorial_resources/apache_metrics/screenshot.png differ diff --git a/src/core_plugins/kibana/public/home/tutorial_resources/mysql_logs/screenshot.png b/src/core_plugins/kibana/public/home/tutorial_resources/mysql_logs/screenshot.png new file mode 100644 index 0000000000000..d380042ca72e8 Binary files /dev/null and b/src/core_plugins/kibana/public/home/tutorial_resources/mysql_logs/screenshot.png differ diff --git a/src/core_plugins/kibana/public/home/tutorial_resources/mysql_metrics/screenshot.png b/src/core_plugins/kibana/public/home/tutorial_resources/mysql_metrics/screenshot.png new file mode 100644 index 0000000000000..e3091f5156c5a Binary files /dev/null and b/src/core_plugins/kibana/public/home/tutorial_resources/mysql_metrics/screenshot.png differ diff --git a/src/core_plugins/kibana/public/home/tutorial_resources/nginx_logs/screenshot.png b/src/core_plugins/kibana/public/home/tutorial_resources/nginx_logs/screenshot.png new file mode 100644 index 0000000000000..10522377112cb Binary files /dev/null and b/src/core_plugins/kibana/public/home/tutorial_resources/nginx_logs/screenshot.png differ diff --git a/src/core_plugins/kibana/public/home/tutorial_resources/nginx_metrics/screenshot.png b/src/core_plugins/kibana/public/home/tutorial_resources/nginx_metrics/screenshot.png new file mode 100644 index 0000000000000..003ea39191cdf Binary files /dev/null and b/src/core_plugins/kibana/public/home/tutorial_resources/nginx_metrics/screenshot.png differ diff --git a/src/core_plugins/kibana/public/home/tutorial_resources/system_logs/screenshot.png b/src/core_plugins/kibana/public/home/tutorial_resources/system_logs/screenshot.png new file mode 100644 index 0000000000000..dfb1d832e3a86 Binary files /dev/null and b/src/core_plugins/kibana/public/home/tutorial_resources/system_logs/screenshot.png differ diff --git a/src/core_plugins/kibana/public/home/tutorial_resources/system_metrics/screenshot.png b/src/core_plugins/kibana/public/home/tutorial_resources/system_metrics/screenshot.png new file mode 100644 index 0000000000000..2ee3d14f164b6 Binary files /dev/null and b/src/core_plugins/kibana/public/home/tutorial_resources/system_metrics/screenshot.png differ 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 a10020980c4a0..5607239b00bd5 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 @@ -92,11 +92,12 @@ uiModules.get('apps/management') $scope.refreshFields = function () { const confirmModalOptions = { - confirmButtonText: 'Refresh fields', - onConfirm: () => { $scope.indexPattern.refreshFields(); } + confirmButtonText: 'Refresh', + onConfirm: () => { $scope.indexPattern.refreshFields(); }, + title: 'Refresh field list?' }; confirmModal( - 'This will reset the field popularity counters. Are you sure you want to refresh your fields?', + 'This action resets the popularity counter of each field.', confirmModalOptions ); }; @@ -119,10 +120,11 @@ uiModules.get('apps/management') } const confirmModalOptions = { - confirmButtonText: 'Remove index pattern', - onConfirm: doRemove + confirmButtonText: 'Delete', + onConfirm: doRemove, + title: 'Delete index pattern?' }; - confirmModal('Are you sure you want to remove this index pattern?', confirmModalOptions); + confirmModal('', confirmModalOptions); }; $scope.setDefaultPattern = function () { diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/scripted_fields_table.js b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/scripted_fields_table.js index aed6e4ee9b29f..4a11665b78b90 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/scripted_fields_table.js +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/scripted_fields_table.js @@ -109,10 +109,11 @@ uiModules.get('apps/management') $scope.remove = function (field) { const confirmModalOptions = { - confirmButtonText: 'Delete field', - onConfirm: () => { $scope.indexPattern.removeScriptedField(field.name); } + confirmButtonText: 'Delete', + onConfirm: () => { $scope.indexPattern.removeScriptedField(field.name); }, + title: `Delete scripted field '${field.name}'?` }; - confirmModal(`Are you sure want to delete ${field.name}? This action is irreversible!`, confirmModalOptions); + confirmModal(`You can't recover scripted fields.`, confirmModalOptions); }; function getLanguagesInUse() { diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/source_filters_table/source_filters_table.js b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/source_filters_table/source_filters_table.js index f73704999c270..8ce64350fc84c 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/source_filters_table/source_filters_table.js +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/source_filters_table/source_filters_table.js @@ -102,10 +102,11 @@ uiModules.get('kibana') }; const confirmModalOptions = { - confirmButtonText: 'Delete filter', - onConfirm: doDelete + confirmButtonText: 'Delete', + onConfirm: doDelete, + title: 'Delete source filter?' }; - confirmModal(`Are you sure want to delete this filter?`, confirmModalOptions); + confirmModal('', confirmModalOptions); } create() { diff --git a/src/core_plugins/kibana/public/management/sections/objects/_objects.js b/src/core_plugins/kibana/public/management/sections/objects/_objects.js index 2f4d9383cf800..e7c809e47ceca 100644 --- a/src/core_plugins/kibana/public/management/sections/objects/_objects.js +++ b/src/core_plugins/kibana/public/management/sections/objects/_objects.js @@ -130,11 +130,12 @@ uiModules.get('apps/management') } const confirmModalOptions = { - confirmButtonText: `Delete ${$scope.currentTab.title}`, - onConfirm: doBulkDelete + confirmButtonText: 'Delete', + onConfirm: doBulkDelete, + title: `Delete selected ${$scope.currentTab.title}?` }; confirmModal( - `Are you sure you want to delete the selected ${$scope.currentTab.title}? This action is irreversible!`, + `You can't recover deleted ${$scope.currentTab.title}.`, confirmModalOptions ); }; @@ -195,11 +196,12 @@ uiModules.get('apps/management') return new Promise((resolve) => { confirmModal( - `If any of the objects already exist, do you want to automatically overwrite them?`, { - confirmButtonText: `Yes, overwrite all`, - cancelButtonText: `No, prompt me for each one`, + '', { + confirmButtonText: `Yes, overwrite all objects`, + cancelButtonText: `No, prompt for each object`, onConfirm: () => resolve(true), onCancel: () => resolve(false), + title: 'Automatically overwrite all saved objects?' } ); }) diff --git a/src/core_plugins/kibana/public/management/sections/objects/_view.js b/src/core_plugins/kibana/public/management/sections/objects/_view.js index 77a59cb4fcde4..48ada4d40678a 100644 --- a/src/core_plugins/kibana/public/management/sections/objects/_view.js +++ b/src/core_plugins/kibana/public/management/sections/objects/_view.js @@ -177,10 +177,11 @@ uiModules.get('apps/management') } const confirmModalOptions = { onConfirm: doDelete, - confirmButtonText: 'Delete object' + confirmButtonText: 'Delete', + title: 'Delete saved Kibana object?' }; confirmModal( - 'Are you sure want to delete this object? This action is irreversible!', + `You can't recover deleted objects`, confirmModalOptions ); }; diff --git a/src/core_plugins/kibana/public/visualize/editor/editor.html b/src/core_plugins/kibana/public/visualize/editor/editor.html index 2df3e945ee520..78bbdc38a2f63 100644 --- a/src/core_plugins/kibana/public/visualize/editor/editor.html +++ b/src/core_plugins/kibana/public/visualize/editor/editor.html @@ -51,13 +51,12 @@ >
-
-
-
+
+

diff --git a/src/core_plugins/kibana/public/visualize/editor/editor.js b/src/core_plugins/kibana/public/visualize/editor/editor.js index 763791eafe45e..17921fbab66b3 100644 --- a/src/core_plugins/kibana/public/visualize/editor/editor.js +++ b/src/core_plugins/kibana/public/visualize/editor/editor.js @@ -118,6 +118,14 @@ function VisEditor($scope, $route, timefilter, AppState, $window, kbnUrl, courie dirty: !savedVis.id }; + this.getSharingTitle = () => { + return savedVis.title; + }; + + this.getSharingType = () => { + return 'visualization'; + }; + if (savedVis.id) { docTitle.change(savedVis.title); } @@ -299,7 +307,7 @@ function VisEditor($scope, $route, timefilter, AppState, $window, kbnUrl, courie $scope.getAdditionalMessage = () => { - return `This visualization is marked as experimental. ${vis.type.feedbackMessage}`; + return ` This visualization is marked as experimental. ${vis.type.feedbackMessage}`; }; init(); diff --git a/src/core_plugins/kibana/public/visualize/editor/styles/_editor.less b/src/core_plugins/kibana/public/visualize/editor/styles/_editor.less index 8d79c02c30726..25a2e9df15001 100644 --- a/src/core_plugins/kibana/public/visualize/editor/styles/_editor.less +++ b/src/core_plugins/kibana/public/visualize/editor/styles/_editor.less @@ -14,13 +14,37 @@ height: 100%; } + @media (max-width: @screen-sm-max) { + visualization { + // While we are on a small screen the visualization is below the + // editor. In this cases it needs a minimum height, since it would otherwise + // maybe end up with 0 height since it just gets the flexbox rest of the screen. + min-height: 400px; + } + } + .collapsible-sidebar { .flex-parent(0, 0, auto); margin-right: 10px; flex-direction: row; - max-width: 75%; min-width: @vis-editor-sidebar-min-width; + max-width: 100%; width: @vis-editor-sidebar-min-width; + + @media (max-width: @screen-sm-max) { + // If we are on a small screen we force the editor to take 100% width. + // We cannot use a min-width: @screen-md-min query here, since we + // would otherwise break the manual resizing on large screens. + width: 100% !important; + + vis-editor-resizer { + display: none; + } + } + + @media (min-width: @screen-md-min) { + max-width: 75%; + } } .collapsible-sidebar.closed { @@ -351,18 +375,14 @@ flex-direction: row; overflow: auto; padding-left: @collapser-width; + flex-shrink: 1; + flex-basis: 100%; &.embedded { flex-shrink: 1; flex-basis: 100%; } - // overrided for tablet and desktop - @media (min-width: @screen-md-min) { - flex-shrink: 1; - flex-basis: 100%; - } - visualize { .flex-parent(); flex: 1 1 100%; diff --git a/src/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory.js b/src/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory.js index 64e2e96d3a506..2fedcb6937f29 100644 --- a/src/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory.js +++ b/src/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory.js @@ -67,7 +67,7 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory { uiState: uiState, // Append visualization to container instead of replacing its content append: true, - cssClass: `panel-content ${savedObject.vis.type.name}`, + cssClass: `panel-content panel-content--fullWidth`, // The chrome is permanently hidden in "embed mode" in which case we don't want to show the spy pane, since // we deem that situation to be more public facing and want to hide more detailed information. showSpyPanel: !chrome.getIsChromePermanentlyHidden(), diff --git a/src/core_plugins/kibana/public/visualize/listing/visualize_listing_table.js b/src/core_plugins/kibana/public/visualize/listing/visualize_listing_table.js index 16d9da68b602d..4f6fb9d24f3a4 100644 --- a/src/core_plugins/kibana/public/visualize/listing/visualize_listing_table.js +++ b/src/core_plugins/kibana/public/visualize/listing/visualize_listing_table.js @@ -2,14 +2,11 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import _ from 'lodash'; -import { SortableProperties } from '@elastic/eui'; import { Pager } from 'ui/pager'; import { NoVisualizationsPrompt } from './no_visualizations_prompt'; import { KuiPager, - KuiModalOverlay, - KuiConfirmModal, KuiListingTableDeleteButton, KuiListingTableCreateButton, KuiListingTable, @@ -17,6 +14,12 @@ import { KuiListingTableLoadingPrompt } from 'ui_framework/components'; +import { + EuiOverlayMask, + EuiConfirmModal, + SortableProperties, +} from '@elastic/eui'; + export class VisualizeListingTable extends Component { constructor(props) { @@ -186,16 +189,17 @@ export class VisualizeListingTable extends Component { renderConfirmDeleteModal() { return ( - - + - + > +

{`You can't recover deleted visualizations.`}

+ + ); } diff --git a/src/core_plugins/kibana/server/routes/api/home/index.js b/src/core_plugins/kibana/server/routes/api/home/index.js new file mode 100644 index 0000000000000..154d9eea1a33e --- /dev/null +++ b/src/core_plugins/kibana/server/routes/api/home/index.js @@ -0,0 +1,5 @@ +import { registerTutorials } from './register_tutorials'; + +export function homeApi(server) { + registerTutorials(server); +} diff --git a/src/core_plugins/kibana/server/routes/api/home/register_tutorials.js b/src/core_plugins/kibana/server/routes/api/home/register_tutorials.js new file mode 100644 index 0000000000000..b22e135288f71 --- /dev/null +++ b/src/core_plugins/kibana/server/routes/api/home/register_tutorials.js @@ -0,0 +1,10 @@ + +export function registerTutorials(server) { + server.route({ + path: '/api/kibana/home/tutorials', + method: ['GET'], + handler: async function (req, reply) { + reply(server.getTutorials()); + } + }); +} diff --git a/src/core_plugins/kibana/server/tutorials/apache_logs/elastic_cloud.js b/src/core_plugins/kibana/server/tutorials/apache_logs/elastic_cloud.js new file mode 100644 index 0000000000000..9a517988c9a9d --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/apache_logs/elastic_cloud.js @@ -0,0 +1,50 @@ +import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; +import { FILEBEAT_INSTRUCTIONS } from '../../../common/tutorials/filebeat_instructions'; +import { FILEBEAT_CLOUD_INSTRUCTIONS } from '../../../common/tutorials/filebeat_cloud_instructions'; +import { ENABLE_INSTRUCTIONS } from './enable'; + +export const ELASTIC_CLOUD_INSTRUCTIONS = { + instructionSets: [ + { + title: 'Getting Started', + instructionVariants: [ + { + id: INSTRUCTION_VARIANT.OSX, + instructions: [ + FILEBEAT_INSTRUCTIONS.INSTALL.OSX, + FILEBEAT_CLOUD_INSTRUCTIONS.CONFIG.OSX, + ENABLE_INSTRUCTIONS.OSX, + FILEBEAT_INSTRUCTIONS.START.OSX + ] + }, + { + id: INSTRUCTION_VARIANT.DEB, + instructions: [ + FILEBEAT_INSTRUCTIONS.INSTALL.DEB, + FILEBEAT_CLOUD_INSTRUCTIONS.CONFIG.DEB, + ENABLE_INSTRUCTIONS.DEB, + FILEBEAT_INSTRUCTIONS.START.DEB + ] + }, + { + id: INSTRUCTION_VARIANT.RPM, + instructions: [ + FILEBEAT_INSTRUCTIONS.INSTALL.RPM, + FILEBEAT_CLOUD_INSTRUCTIONS.CONFIG.RPM, + ENABLE_INSTRUCTIONS.RPM, + FILEBEAT_INSTRUCTIONS.START.RPM + ] + }, + { + id: INSTRUCTION_VARIANT.WINDOWS, + instructions: [ + FILEBEAT_INSTRUCTIONS.INSTALL.WINDOWS, + FILEBEAT_CLOUD_INSTRUCTIONS.CONFIG.WINDOWS, + ENABLE_INSTRUCTIONS.WINDOWS, + FILEBEAT_INSTRUCTIONS.START.WINDOWS + ] + } + ] + } + ] +}; diff --git a/src/core_plugins/kibana/server/tutorials/apache_logs/enable.js b/src/core_plugins/kibana/server/tutorials/apache_logs/enable.js new file mode 100644 index 0000000000000..fe0ce9e416834 --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/apache_logs/enable.js @@ -0,0 +1,32 @@ +export const ENABLE_INSTRUCTIONS = { + OSX: { + title: 'Enable and configure the apache2 module', + textPre: 'From the installation directory, run:', + commands: [ + './filebeat modules enable apache2', + ], + textPost: 'Modify the settings in the `modules.d/apache2.yml` file.' + }, + DEB: { + title: 'Enable and configure the apache2 module', + commands: [ + 'sudo filebeat modules enable apache2', + ], + textPost: 'Modify the settings in the `/etc/filebeat/modules.d/apache2.yml` file.' + }, + RPM: { + title: 'Enable and configure the apache2 module', + commands: [ + 'sudo filebeat modules enable apache2', + ], + textPost: 'Modify the settings in the `/etc/filebeat/modules.d/apache2.yml` file.' + }, + WINDOWS: { + title: 'Enable and configure the apache2 module', + textPre: 'From the `C:\\Program Files\\Filebeat` folder, run:', + commands: [ + 'PS C:\\Program Files\\Filebeat> filebeat.exe modules enable apache2', + ], + textPost: 'Modify the settings in the `modules.d/apache2.yml` file.' + } +}; diff --git a/src/core_plugins/kibana/server/tutorials/apache_logs/index.js b/src/core_plugins/kibana/server/tutorials/apache_logs/index.js new file mode 100644 index 0000000000000..405ef51fce56f --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/apache_logs/index.js @@ -0,0 +1,34 @@ +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'; + +export function apacheLogsSpecProvider() { + return { + id: 'apacheLogs', + name: 'Apache logs', + category: TUTORIAL_CATEGORY.LOGGING, + shortDescription: 'Collect and parse access and error logs created by the Apache HTTP server.', + longDescription: 'The apache2 Filebeat module parses access and error logs created by the Apache 2 HTTP server.' + + ' [Learn more]({config.docs.beats.filebeat}/filebeat-module-apache2.html)' + + ' about the apache2 module.', + //iconPath: '', TODO + artifacts: { + dashboards: [ + { + title: 'Filebeat-Apache2-Dashboard', + linkLabel: 'Apache2 logs dashboard', + isOverview: true + } + ], + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-apache2.html' + } + }, + completionTimeMinutes: 10, + previewImagePath: '/plugins/kibana/home/tutorial_resources/apache_logs/screenshot.png', + onPrem: ON_PREM_INSTRUCTIONS, + elasticCloud: ELASTIC_CLOUD_INSTRUCTIONS, + onPremElasticCloud: ON_PREM_ELASTIC_CLOUD_INSTRUCTIONS + }; +} diff --git a/src/core_plugins/kibana/server/tutorials/apache_logs/on_prem.js b/src/core_plugins/kibana/server/tutorials/apache_logs/on_prem.js new file mode 100644 index 0000000000000..9ecaa557d2407 --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/apache_logs/on_prem.js @@ -0,0 +1,53 @@ +import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; +import { FILEBEAT_INSTRUCTIONS } from '../../../common/tutorials/filebeat_instructions'; +import { ENABLE_INSTRUCTIONS } from './enable'; + +export const ON_PREM_INSTRUCTIONS = { + instructionSets: [ + { + title: 'Getting Started', + instructionVariants: [ + { + id: INSTRUCTION_VARIANT.OSX, + instructions: [ + FILEBEAT_INSTRUCTIONS.PLUGINS.GEOIP_AND_UA, + FILEBEAT_INSTRUCTIONS.INSTALL.OSX, + FILEBEAT_INSTRUCTIONS.CONFIG.OSX, + ENABLE_INSTRUCTIONS.OSX, + FILEBEAT_INSTRUCTIONS.START.OSX + ] + }, + { + id: INSTRUCTION_VARIANT.DEB, + instructions: [ + FILEBEAT_INSTRUCTIONS.PLUGINS.GEOIP_AND_UA, + FILEBEAT_INSTRUCTIONS.INSTALL.DEB, + FILEBEAT_INSTRUCTIONS.CONFIG.DEB, + ENABLE_INSTRUCTIONS.DEB, + FILEBEAT_INSTRUCTIONS.START.DEB + ] + }, + { + id: INSTRUCTION_VARIANT.RPM, + instructions: [ + FILEBEAT_INSTRUCTIONS.PLUGINS.GEOIP_AND_UA, + FILEBEAT_INSTRUCTIONS.INSTALL.RPM, + FILEBEAT_INSTRUCTIONS.CONFIG.RPM, + ENABLE_INSTRUCTIONS.RPM, + FILEBEAT_INSTRUCTIONS.START.RPM + ] + }, + { + id: INSTRUCTION_VARIANT.WINDOWS, + instructions: [ + FILEBEAT_INSTRUCTIONS.PLUGINS.GEOIP_AND_UA, + FILEBEAT_INSTRUCTIONS.INSTALL.WINDOWS, + FILEBEAT_INSTRUCTIONS.CONFIG.WINDOWS, + ENABLE_INSTRUCTIONS.WINDOWS, + FILEBEAT_INSTRUCTIONS.START.WINDOWS + ] + } + ] + } + ] +}; diff --git a/src/core_plugins/kibana/server/tutorials/apache_logs/on_prem_elastic_cloud.js b/src/core_plugins/kibana/server/tutorials/apache_logs/on_prem_elastic_cloud.js new file mode 100644 index 0000000000000..2266d33eccd1f --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/apache_logs/on_prem_elastic_cloud.js @@ -0,0 +1,61 @@ +import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; +import { FILEBEAT_INSTRUCTIONS } from '../../../common/tutorials/filebeat_instructions'; +import { + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2 +} from '../../../common/tutorials/onprem_cloud_instructions'; +import { ENABLE_INSTRUCTIONS } from './enable'; + +export const ON_PREM_ELASTIC_CLOUD_INSTRUCTIONS = { + instructionSets: [ + { + title: 'Getting Started', + instructionVariants: [ + { + id: INSTRUCTION_VARIANT.OSX, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + FILEBEAT_INSTRUCTIONS.INSTALL.OSX, + FILEBEAT_INSTRUCTIONS.CONFIG.OSX, + ENABLE_INSTRUCTIONS.OSX, + FILEBEAT_INSTRUCTIONS.START.OSX + ] + }, + { + id: INSTRUCTION_VARIANT.DEB, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + FILEBEAT_INSTRUCTIONS.INSTALL.DEB, + FILEBEAT_INSTRUCTIONS.CONFIG.DEB, + ENABLE_INSTRUCTIONS.DEB, + FILEBEAT_INSTRUCTIONS.START.DEB + ] + }, + { + id: INSTRUCTION_VARIANT.RPM, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + FILEBEAT_INSTRUCTIONS.INSTALL.RPM, + FILEBEAT_INSTRUCTIONS.CONFIG.RPM, + ENABLE_INSTRUCTIONS.RPM, + FILEBEAT_INSTRUCTIONS.START.RPM + ] + }, + { + id: INSTRUCTION_VARIANT.WINDOWS, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + FILEBEAT_INSTRUCTIONS.INSTALL.WINDOWS, + FILEBEAT_INSTRUCTIONS.CONFIG.WINDOWS, + ENABLE_INSTRUCTIONS.WINDOWS, + FILEBEAT_INSTRUCTIONS.START.WINDOWS + ] + } + ] + } + ] +}; diff --git a/src/core_plugins/kibana/server/tutorials/apache_metrics/elastic_cloud.js b/src/core_plugins/kibana/server/tutorials/apache_metrics/elastic_cloud.js new file mode 100644 index 0000000000000..408ae869571e7 --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/apache_metrics/elastic_cloud.js @@ -0,0 +1,50 @@ +import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; +import { METRICBEAT_INSTRUCTIONS } from '../../../common/tutorials/metricbeat_instructions'; +import { METRICBEAT_CLOUD_INSTRUCTIONS } from '../../../common/tutorials/metricbeat_cloud_instructions'; +import { ENABLE_INSTRUCTIONS } from './enable'; + +export const ELASTIC_CLOUD_INSTRUCTIONS = { + instructionSets: [ + { + title: 'Getting Started', + instructionVariants: [ + { + id: INSTRUCTION_VARIANT.OSX, + instructions: [ + METRICBEAT_INSTRUCTIONS.INSTALL.OSX, + METRICBEAT_CLOUD_INSTRUCTIONS.CONFIG.OSX, + ENABLE_INSTRUCTIONS.OSX, + METRICBEAT_INSTRUCTIONS.START.OSX + ] + }, + { + id: INSTRUCTION_VARIANT.DEB, + instructions: [ + METRICBEAT_INSTRUCTIONS.INSTALL.DEB, + METRICBEAT_CLOUD_INSTRUCTIONS.CONFIG.DEB, + ENABLE_INSTRUCTIONS.DEB, + METRICBEAT_INSTRUCTIONS.START.DEB + ] + }, + { + id: INSTRUCTION_VARIANT.RPM, + instructions: [ + METRICBEAT_INSTRUCTIONS.INSTALL.RPM, + METRICBEAT_CLOUD_INSTRUCTIONS.CONFIG.RPM, + ENABLE_INSTRUCTIONS.RPM, + METRICBEAT_INSTRUCTIONS.START.RPM + ] + }, + { + id: INSTRUCTION_VARIANT.WINDOWS, + instructions: [ + METRICBEAT_INSTRUCTIONS.INSTALL.WINDOWS, + METRICBEAT_CLOUD_INSTRUCTIONS.CONFIG.WINDOWS, + ENABLE_INSTRUCTIONS.WINDOWS, + METRICBEAT_INSTRUCTIONS.START.WINDOWS + ] + } + ] + } + ] +}; diff --git a/src/core_plugins/kibana/server/tutorials/apache_metrics/enable.js b/src/core_plugins/kibana/server/tutorials/apache_metrics/enable.js new file mode 100644 index 0000000000000..9e18661701c7a --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/apache_metrics/enable.js @@ -0,0 +1,32 @@ +export const ENABLE_INSTRUCTIONS = { + OSX: { + title: 'Enable and configure the apache module', + textPre: 'From the installation directory, run:', + commands: [ + './metricbeat modules enable apache', + ], + textPost: 'Modify the settings in the `modules.d/apache.yml` file.' + }, + DEB: { + title: 'Enable and configure the apache module', + commands: [ + 'sudo metricbeat modules enable apache', + ], + textPost: 'Modify the settings in the `/etc/metricbeat/modules.d/apache.yml` file.' + }, + RPM: { + title: 'Enable and configure the apache module', + commands: [ + 'sudo metricbeat modules enable apache', + ], + textPost: 'Modify the settings in the `/etc/metricbeat/modules.d/apache.yml` file.' + }, + WINDOWS: { + title: 'Enable and configure the apache module', + textPre: 'From the `C:\\Program Files\\Metricbeat` folder, run:', + commands: [ + 'PS C:\\Program Files\\Metricbeat> metricbeat.exe modules enable apache', + ], + textPost: 'Modify the settings in the `modules.d/apache.yml` file.' + } +}; diff --git a/src/core_plugins/kibana/server/tutorials/apache_metrics/index.js b/src/core_plugins/kibana/server/tutorials/apache_metrics/index.js new file mode 100644 index 0000000000000..9e8b84bd06a76 --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/apache_metrics/index.js @@ -0,0 +1,34 @@ +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'; + +export function apacheMetricsSpecProvider() { + return { + id: 'apacheMetrics', + name: 'Apache metrics', + category: TUTORIAL_CATEGORY.METRICS, + shortDescription: 'Fetches internal metrics from the Apache 2 HTTP server.', + longDescription: 'The `apache` Metricbeat module fetches internal metrics from the Apache 2 HTTP server.' + + ' [Learn more]({config.docs.beats.metricbeat}/metricbeat-module-apache.html)' + + ' about the apache module.', + //iconPath: '', TODO + artifacts: { + dashboards: [ + { + title: 'Metricbeat-Apache-HTTPD-server-status', + linkLabel: 'Apache metrics dashboard', + isOverview: true + } + ], + exportedFields: { + documentationUrl: '{config.docs.beats.metricbeat}/exported-fields-apache.html' + } + }, + completionTimeMinutes: 10, + previewImagePath: '/plugins/kibana/home/tutorial_resources/apache_metrics/screenshot.png', + onPrem: ON_PREM_INSTRUCTIONS, + elasticCloud: ELASTIC_CLOUD_INSTRUCTIONS, + onPremElasticCloud: ON_PREM_ELASTIC_CLOUD_INSTRUCTIONS + }; +} diff --git a/src/core_plugins/kibana/server/tutorials/apache_metrics/on_prem.js b/src/core_plugins/kibana/server/tutorials/apache_metrics/on_prem.js new file mode 100644 index 0000000000000..8a47014420ebe --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/apache_metrics/on_prem.js @@ -0,0 +1,49 @@ +import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; +import { METRICBEAT_INSTRUCTIONS } from '../../../common/tutorials/metricbeat_instructions'; +import { ENABLE_INSTRUCTIONS } from './enable'; + +export const ON_PREM_INSTRUCTIONS = { + instructionSets: [ + { + title: 'Getting Started', + instructionVariants: [ + { + id: INSTRUCTION_VARIANT.OSX, + instructions: [ + METRICBEAT_INSTRUCTIONS.INSTALL.OSX, + METRICBEAT_INSTRUCTIONS.CONFIG.OSX, + ENABLE_INSTRUCTIONS.OSX, + METRICBEAT_INSTRUCTIONS.START.OSX + ] + }, + { + id: INSTRUCTION_VARIANT.DEB, + instructions: [ + METRICBEAT_INSTRUCTIONS.INSTALL.DEB, + METRICBEAT_INSTRUCTIONS.CONFIG.DEB, + ENABLE_INSTRUCTIONS.DEB, + METRICBEAT_INSTRUCTIONS.START.DEB + ] + }, + { + id: INSTRUCTION_VARIANT.RPM, + instructions: [ + METRICBEAT_INSTRUCTIONS.INSTALL.RPM, + METRICBEAT_INSTRUCTIONS.CONFIG.RPM, + ENABLE_INSTRUCTIONS.RPM, + METRICBEAT_INSTRUCTIONS.START.RPM + ] + }, + { + id: INSTRUCTION_VARIANT.WINDOWS, + instructions: [ + METRICBEAT_INSTRUCTIONS.INSTALL.WINDOWS, + METRICBEAT_INSTRUCTIONS.CONFIG.WINDOWS, + ENABLE_INSTRUCTIONS.WINDOWS, + METRICBEAT_INSTRUCTIONS.START.WINDOWS + ] + } + ] + } + ] +}; diff --git a/src/core_plugins/kibana/server/tutorials/apache_metrics/on_prem_elastic_cloud.js b/src/core_plugins/kibana/server/tutorials/apache_metrics/on_prem_elastic_cloud.js new file mode 100644 index 0000000000000..fe1cdbcde9391 --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/apache_metrics/on_prem_elastic_cloud.js @@ -0,0 +1,61 @@ +import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; +import { METRICBEAT_INSTRUCTIONS } from '../../../common/tutorials/metricbeat_instructions'; +import { + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2 +} from '../../../common/tutorials/onprem_cloud_instructions'; +import { ENABLE_INSTRUCTIONS } from './enable'; + +export const ON_PREM_ELASTIC_CLOUD_INSTRUCTIONS = { + instructionSets: [ + { + title: 'Getting Started', + instructionVariants: [ + { + id: INSTRUCTION_VARIANT.OSX, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + METRICBEAT_INSTRUCTIONS.INSTALL.OSX, + METRICBEAT_INSTRUCTIONS.CONFIG.OSX, + ENABLE_INSTRUCTIONS.OSX, + METRICBEAT_INSTRUCTIONS.START.OSX + ] + }, + { + id: INSTRUCTION_VARIANT.DEB, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + METRICBEAT_INSTRUCTIONS.INSTALL.DEB, + METRICBEAT_INSTRUCTIONS.CONFIG.DEB, + ENABLE_INSTRUCTIONS.DEB, + METRICBEAT_INSTRUCTIONS.START.DEB + ] + }, + { + id: INSTRUCTION_VARIANT.RPM, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + METRICBEAT_INSTRUCTIONS.INSTALL.RPM, + METRICBEAT_INSTRUCTIONS.CONFIG.RPM, + ENABLE_INSTRUCTIONS.RPM, + METRICBEAT_INSTRUCTIONS.START.RPM + ] + }, + { + id: INSTRUCTION_VARIANT.WINDOWS, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + METRICBEAT_INSTRUCTIONS.INSTALL.WINDOWS, + METRICBEAT_INSTRUCTIONS.CONFIG.WINDOWS, + ENABLE_INSTRUCTIONS.WINDOWS, + METRICBEAT_INSTRUCTIONS.START.WINDOWS + ] + } + ] + } + ] +}; diff --git a/src/core_plugins/kibana/server/tutorials/mysql_logs/elastic_cloud.js b/src/core_plugins/kibana/server/tutorials/mysql_logs/elastic_cloud.js new file mode 100644 index 0000000000000..9a517988c9a9d --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/mysql_logs/elastic_cloud.js @@ -0,0 +1,50 @@ +import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; +import { FILEBEAT_INSTRUCTIONS } from '../../../common/tutorials/filebeat_instructions'; +import { FILEBEAT_CLOUD_INSTRUCTIONS } from '../../../common/tutorials/filebeat_cloud_instructions'; +import { ENABLE_INSTRUCTIONS } from './enable'; + +export const ELASTIC_CLOUD_INSTRUCTIONS = { + instructionSets: [ + { + title: 'Getting Started', + instructionVariants: [ + { + id: INSTRUCTION_VARIANT.OSX, + instructions: [ + FILEBEAT_INSTRUCTIONS.INSTALL.OSX, + FILEBEAT_CLOUD_INSTRUCTIONS.CONFIG.OSX, + ENABLE_INSTRUCTIONS.OSX, + FILEBEAT_INSTRUCTIONS.START.OSX + ] + }, + { + id: INSTRUCTION_VARIANT.DEB, + instructions: [ + FILEBEAT_INSTRUCTIONS.INSTALL.DEB, + FILEBEAT_CLOUD_INSTRUCTIONS.CONFIG.DEB, + ENABLE_INSTRUCTIONS.DEB, + FILEBEAT_INSTRUCTIONS.START.DEB + ] + }, + { + id: INSTRUCTION_VARIANT.RPM, + instructions: [ + FILEBEAT_INSTRUCTIONS.INSTALL.RPM, + FILEBEAT_CLOUD_INSTRUCTIONS.CONFIG.RPM, + ENABLE_INSTRUCTIONS.RPM, + FILEBEAT_INSTRUCTIONS.START.RPM + ] + }, + { + id: INSTRUCTION_VARIANT.WINDOWS, + instructions: [ + FILEBEAT_INSTRUCTIONS.INSTALL.WINDOWS, + FILEBEAT_CLOUD_INSTRUCTIONS.CONFIG.WINDOWS, + ENABLE_INSTRUCTIONS.WINDOWS, + FILEBEAT_INSTRUCTIONS.START.WINDOWS + ] + } + ] + } + ] +}; diff --git a/src/core_plugins/kibana/server/tutorials/mysql_logs/enable.js b/src/core_plugins/kibana/server/tutorials/mysql_logs/enable.js new file mode 100644 index 0000000000000..a303c18717511 --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/mysql_logs/enable.js @@ -0,0 +1,32 @@ +export const ENABLE_INSTRUCTIONS = { + OSX: { + title: 'Enable and configure the mysql module', + textPre: 'From the installation directory, run:', + commands: [ + './filebeat modules enable mysql', + ], + textPost: 'Modify the settings in the `modules.d/mysql.yml` file.' + }, + DEB: { + title: 'Enable and configure the mysql module', + commands: [ + 'sudo filebeat modules enable mysql', + ], + textPost: 'Modify the settings in the `/etc/filebeat/modules.d/mysql.yml` file.' + }, + RPM: { + title: 'Enable and configure the mysql module', + commands: [ + 'sudo filebeat modules enable mysql', + ], + textPost: 'Modify the settings in the `/etc/filebeat/modules.d/mysql.yml` file.' + }, + WINDOWS: { + title: 'Enable and configure the mysql module', + textPre: 'From the `C:\\Program Files\\Filebeat` folder, run:', + commands: [ + 'PS C:\\Program Files\\Filebeat> filebeat.exe modules enable mysql', + ], + textPost: 'Modify the settings in the `modules.d/mysql.yml` file.' + } +}; diff --git a/src/core_plugins/kibana/server/tutorials/mysql_logs/index.js b/src/core_plugins/kibana/server/tutorials/mysql_logs/index.js new file mode 100644 index 0000000000000..e87ecb9415e25 --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/mysql_logs/index.js @@ -0,0 +1,34 @@ +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'; + +export function mysqlLogsSpecProvider() { + return { + id: 'mysqlLogs', + name: '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)' + + ' about the `mysql` module.', + //iconPath: '', TODO + artifacts: { + dashboards: [ + { + title: 'Filebeat-MySQL-Dashboard', + linkLabel: 'MySQL logs dashboard', + isOverview: true + } + ], + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-mysql.html' + } + }, + completionTimeMinutes: 10, + previewImagePath: '/plugins/kibana/home/tutorial_resources/mysql_logs/screenshot.png', + onPrem: ON_PREM_INSTRUCTIONS, + elasticCloud: ELASTIC_CLOUD_INSTRUCTIONS, + onPremElasticCloud: ON_PREM_ELASTIC_CLOUD_INSTRUCTIONS + }; +} diff --git a/src/core_plugins/kibana/server/tutorials/mysql_logs/on_prem.js b/src/core_plugins/kibana/server/tutorials/mysql_logs/on_prem.js new file mode 100644 index 0000000000000..e280230d7b78e --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/mysql_logs/on_prem.js @@ -0,0 +1,49 @@ +import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; +import { FILEBEAT_INSTRUCTIONS } from '../../../common/tutorials/filebeat_instructions'; +import { ENABLE_INSTRUCTIONS } from './enable'; + +export const ON_PREM_INSTRUCTIONS = { + instructionSets: [ + { + title: 'Getting Started', + instructionVariants: [ + { + id: INSTRUCTION_VARIANT.OSX, + instructions: [ + FILEBEAT_INSTRUCTIONS.INSTALL.OSX, + FILEBEAT_INSTRUCTIONS.CONFIG.OSX, + ENABLE_INSTRUCTIONS.OSX, + FILEBEAT_INSTRUCTIONS.START.OSX + ] + }, + { + id: INSTRUCTION_VARIANT.DEB, + instructions: [ + FILEBEAT_INSTRUCTIONS.INSTALL.DEB, + FILEBEAT_INSTRUCTIONS.CONFIG.DEB, + ENABLE_INSTRUCTIONS.DEB, + FILEBEAT_INSTRUCTIONS.START.DEB + ] + }, + { + id: INSTRUCTION_VARIANT.RPM, + instructions: [ + FILEBEAT_INSTRUCTIONS.INSTALL.RPM, + FILEBEAT_INSTRUCTIONS.CONFIG.RPM, + ENABLE_INSTRUCTIONS.RPM, + FILEBEAT_INSTRUCTIONS.START.RPM + ] + }, + { + id: INSTRUCTION_VARIANT.WINDOWS, + instructions: [ + FILEBEAT_INSTRUCTIONS.INSTALL.WINDOWS, + FILEBEAT_INSTRUCTIONS.CONFIG.WINDOWS, + ENABLE_INSTRUCTIONS.WINDOWS, + FILEBEAT_INSTRUCTIONS.START.WINDOWS + ] + } + ] + } + ] +}; diff --git a/src/core_plugins/kibana/server/tutorials/mysql_logs/on_prem_elastic_cloud.js b/src/core_plugins/kibana/server/tutorials/mysql_logs/on_prem_elastic_cloud.js new file mode 100644 index 0000000000000..2266d33eccd1f --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/mysql_logs/on_prem_elastic_cloud.js @@ -0,0 +1,61 @@ +import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; +import { FILEBEAT_INSTRUCTIONS } from '../../../common/tutorials/filebeat_instructions'; +import { + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2 +} from '../../../common/tutorials/onprem_cloud_instructions'; +import { ENABLE_INSTRUCTIONS } from './enable'; + +export const ON_PREM_ELASTIC_CLOUD_INSTRUCTIONS = { + instructionSets: [ + { + title: 'Getting Started', + instructionVariants: [ + { + id: INSTRUCTION_VARIANT.OSX, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + FILEBEAT_INSTRUCTIONS.INSTALL.OSX, + FILEBEAT_INSTRUCTIONS.CONFIG.OSX, + ENABLE_INSTRUCTIONS.OSX, + FILEBEAT_INSTRUCTIONS.START.OSX + ] + }, + { + id: INSTRUCTION_VARIANT.DEB, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + FILEBEAT_INSTRUCTIONS.INSTALL.DEB, + FILEBEAT_INSTRUCTIONS.CONFIG.DEB, + ENABLE_INSTRUCTIONS.DEB, + FILEBEAT_INSTRUCTIONS.START.DEB + ] + }, + { + id: INSTRUCTION_VARIANT.RPM, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + FILEBEAT_INSTRUCTIONS.INSTALL.RPM, + FILEBEAT_INSTRUCTIONS.CONFIG.RPM, + ENABLE_INSTRUCTIONS.RPM, + FILEBEAT_INSTRUCTIONS.START.RPM + ] + }, + { + id: INSTRUCTION_VARIANT.WINDOWS, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + FILEBEAT_INSTRUCTIONS.INSTALL.WINDOWS, + FILEBEAT_INSTRUCTIONS.CONFIG.WINDOWS, + ENABLE_INSTRUCTIONS.WINDOWS, + FILEBEAT_INSTRUCTIONS.START.WINDOWS + ] + } + ] + } + ] +}; diff --git a/src/core_plugins/kibana/server/tutorials/mysql_metrics/elastic_cloud.js b/src/core_plugins/kibana/server/tutorials/mysql_metrics/elastic_cloud.js new file mode 100644 index 0000000000000..408ae869571e7 --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/mysql_metrics/elastic_cloud.js @@ -0,0 +1,50 @@ +import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; +import { METRICBEAT_INSTRUCTIONS } from '../../../common/tutorials/metricbeat_instructions'; +import { METRICBEAT_CLOUD_INSTRUCTIONS } from '../../../common/tutorials/metricbeat_cloud_instructions'; +import { ENABLE_INSTRUCTIONS } from './enable'; + +export const ELASTIC_CLOUD_INSTRUCTIONS = { + instructionSets: [ + { + title: 'Getting Started', + instructionVariants: [ + { + id: INSTRUCTION_VARIANT.OSX, + instructions: [ + METRICBEAT_INSTRUCTIONS.INSTALL.OSX, + METRICBEAT_CLOUD_INSTRUCTIONS.CONFIG.OSX, + ENABLE_INSTRUCTIONS.OSX, + METRICBEAT_INSTRUCTIONS.START.OSX + ] + }, + { + id: INSTRUCTION_VARIANT.DEB, + instructions: [ + METRICBEAT_INSTRUCTIONS.INSTALL.DEB, + METRICBEAT_CLOUD_INSTRUCTIONS.CONFIG.DEB, + ENABLE_INSTRUCTIONS.DEB, + METRICBEAT_INSTRUCTIONS.START.DEB + ] + }, + { + id: INSTRUCTION_VARIANT.RPM, + instructions: [ + METRICBEAT_INSTRUCTIONS.INSTALL.RPM, + METRICBEAT_CLOUD_INSTRUCTIONS.CONFIG.RPM, + ENABLE_INSTRUCTIONS.RPM, + METRICBEAT_INSTRUCTIONS.START.RPM + ] + }, + { + id: INSTRUCTION_VARIANT.WINDOWS, + instructions: [ + METRICBEAT_INSTRUCTIONS.INSTALL.WINDOWS, + METRICBEAT_CLOUD_INSTRUCTIONS.CONFIG.WINDOWS, + ENABLE_INSTRUCTIONS.WINDOWS, + METRICBEAT_INSTRUCTIONS.START.WINDOWS + ] + } + ] + } + ] +}; diff --git a/src/core_plugins/kibana/server/tutorials/mysql_metrics/enable.js b/src/core_plugins/kibana/server/tutorials/mysql_metrics/enable.js new file mode 100644 index 0000000000000..815f68f794910 --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/mysql_metrics/enable.js @@ -0,0 +1,32 @@ +export const ENABLE_INSTRUCTIONS = { + OSX: { + title: 'Enable and configure the mysql module', + textPre: 'From the installation directory, run:', + commands: [ + './metricbeat modules enable mysql', + ], + textPost: 'Modify the settings in the `modules.d/mysql.yml` file.' + }, + DEB: { + title: 'Enable and configure the mysql module', + commands: [ + 'sudo metricbeat modules enable mysql', + ], + textPost: 'Modify the settings in the `/etc/metricbeat/modules.d/mysql.yml` file.' + }, + RPM: { + title: 'Enable and configure the mysql module', + commands: [ + 'sudo metricbeat modules enable mysql', + ], + textPost: 'Modify the settings in the `/etc/metricbeat/modules.d/mysql.yml` file.' + }, + WINDOWS: { + title: 'Enable and configure the mysql module', + textPre: 'From the `C:\\Program Files\\Metricbeat` folder, run:', + commands: [ + 'PS C:\\Program Files\\Metricbeat> metricbeat.exe modules enable mysql', + ], + textPost: 'Modify the settings in the `modules.d/mysql.yml` file.' + } +}; diff --git a/src/core_plugins/kibana/server/tutorials/mysql_metrics/index.js b/src/core_plugins/kibana/server/tutorials/mysql_metrics/index.js new file mode 100644 index 0000000000000..a8cacd7f60a77 --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/mysql_metrics/index.js @@ -0,0 +1,34 @@ +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'; + +export function mysqlMetricsSpecProvider() { + return { + id: 'mysqlMetrics', + name: 'MySQL metrics', + category: TUTORIAL_CATEGORY.METRICS, + shortDescription: 'Fetches 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)' + + ' about the mysql module.', + //iconPath: '', TODO + artifacts: { + dashboards: [ + { + title: '66881e90-0006-11e7-bf7f-c9acc3d3e306', + linkLabel: 'MySQL metrics dashboard', + isOverview: true + } + ], + exportedFields: { + documentationUrl: '{config.docs.beats.metricbeat}/exported-fields-mysql.html' + } + }, + completionTimeMinutes: 10, + previewImagePath: '/plugins/kibana/home/tutorial_resources/mysql_metrics/screenshot.png', + onPrem: ON_PREM_INSTRUCTIONS, + elasticCloud: ELASTIC_CLOUD_INSTRUCTIONS, + onPremElasticCloud: ON_PREM_ELASTIC_CLOUD_INSTRUCTIONS + }; +} diff --git a/src/core_plugins/kibana/server/tutorials/mysql_metrics/on_prem.js b/src/core_plugins/kibana/server/tutorials/mysql_metrics/on_prem.js new file mode 100644 index 0000000000000..8a47014420ebe --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/mysql_metrics/on_prem.js @@ -0,0 +1,49 @@ +import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; +import { METRICBEAT_INSTRUCTIONS } from '../../../common/tutorials/metricbeat_instructions'; +import { ENABLE_INSTRUCTIONS } from './enable'; + +export const ON_PREM_INSTRUCTIONS = { + instructionSets: [ + { + title: 'Getting Started', + instructionVariants: [ + { + id: INSTRUCTION_VARIANT.OSX, + instructions: [ + METRICBEAT_INSTRUCTIONS.INSTALL.OSX, + METRICBEAT_INSTRUCTIONS.CONFIG.OSX, + ENABLE_INSTRUCTIONS.OSX, + METRICBEAT_INSTRUCTIONS.START.OSX + ] + }, + { + id: INSTRUCTION_VARIANT.DEB, + instructions: [ + METRICBEAT_INSTRUCTIONS.INSTALL.DEB, + METRICBEAT_INSTRUCTIONS.CONFIG.DEB, + ENABLE_INSTRUCTIONS.DEB, + METRICBEAT_INSTRUCTIONS.START.DEB + ] + }, + { + id: INSTRUCTION_VARIANT.RPM, + instructions: [ + METRICBEAT_INSTRUCTIONS.INSTALL.RPM, + METRICBEAT_INSTRUCTIONS.CONFIG.RPM, + ENABLE_INSTRUCTIONS.RPM, + METRICBEAT_INSTRUCTIONS.START.RPM + ] + }, + { + id: INSTRUCTION_VARIANT.WINDOWS, + instructions: [ + METRICBEAT_INSTRUCTIONS.INSTALL.WINDOWS, + METRICBEAT_INSTRUCTIONS.CONFIG.WINDOWS, + ENABLE_INSTRUCTIONS.WINDOWS, + METRICBEAT_INSTRUCTIONS.START.WINDOWS + ] + } + ] + } + ] +}; diff --git a/src/core_plugins/kibana/server/tutorials/mysql_metrics/on_prem_elastic_cloud.js b/src/core_plugins/kibana/server/tutorials/mysql_metrics/on_prem_elastic_cloud.js new file mode 100644 index 0000000000000..fe1cdbcde9391 --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/mysql_metrics/on_prem_elastic_cloud.js @@ -0,0 +1,61 @@ +import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; +import { METRICBEAT_INSTRUCTIONS } from '../../../common/tutorials/metricbeat_instructions'; +import { + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2 +} from '../../../common/tutorials/onprem_cloud_instructions'; +import { ENABLE_INSTRUCTIONS } from './enable'; + +export const ON_PREM_ELASTIC_CLOUD_INSTRUCTIONS = { + instructionSets: [ + { + title: 'Getting Started', + instructionVariants: [ + { + id: INSTRUCTION_VARIANT.OSX, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + METRICBEAT_INSTRUCTIONS.INSTALL.OSX, + METRICBEAT_INSTRUCTIONS.CONFIG.OSX, + ENABLE_INSTRUCTIONS.OSX, + METRICBEAT_INSTRUCTIONS.START.OSX + ] + }, + { + id: INSTRUCTION_VARIANT.DEB, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + METRICBEAT_INSTRUCTIONS.INSTALL.DEB, + METRICBEAT_INSTRUCTIONS.CONFIG.DEB, + ENABLE_INSTRUCTIONS.DEB, + METRICBEAT_INSTRUCTIONS.START.DEB + ] + }, + { + id: INSTRUCTION_VARIANT.RPM, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + METRICBEAT_INSTRUCTIONS.INSTALL.RPM, + METRICBEAT_INSTRUCTIONS.CONFIG.RPM, + ENABLE_INSTRUCTIONS.RPM, + METRICBEAT_INSTRUCTIONS.START.RPM + ] + }, + { + id: INSTRUCTION_VARIANT.WINDOWS, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + METRICBEAT_INSTRUCTIONS.INSTALL.WINDOWS, + METRICBEAT_INSTRUCTIONS.CONFIG.WINDOWS, + ENABLE_INSTRUCTIONS.WINDOWS, + METRICBEAT_INSTRUCTIONS.START.WINDOWS + ] + } + ] + } + ] +}; diff --git a/src/core_plugins/kibana/server/tutorials/netflow/common_instructions.js b/src/core_plugins/kibana/server/tutorials/netflow/common_instructions.js new file mode 100644 index 0000000000000..f6fbadb9ac2ae --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/netflow/common_instructions.js @@ -0,0 +1,91 @@ +export const COMMON_NETFLOW_INSTRUCTIONS = { + CONFIG: { + ON_PREM: { + OSX: [ + { + title: 'Edit the configuration', + textPre: 'In the Logstash installation directory, modify `config/logstash.yml` to set the' + + ' configuration parameters for the Netflow module.', + commands: [ + 'modules:', + ' - name: netflow', + ' var.input.udp.port: ', + ' var.elasticsearch.hosts: [ "" ]', + ' var.kibana.host: ":"' + ] + } + ], + WINDOWS: [ + { + title: 'Edit the configuration', + textPre: 'While in the Logstash install directory, modify `config\\logstash.yml` to set the' + + ' configuration parameters for the Netflow module:', + commands: [ + 'modules:', + ' - name: netflow', + ' var.input.udp.port: ', + ' var.elasticsearch.hosts: [ "" ]', + ' var.kibana.host: ":"' + ] + } + ] + }, + ELASTIC_CLOUD: { + OSX: [ + { + title: 'Edit the configuration', + textPre: 'In the Logstash installation directory, modify `config/logstash.yml` to set the' + + ' configuration parameters for the Netflow module.', + commands: [ + 'modules:', + ' - name: netflow', + ' var.input.udp.port: ', + ' cloud.id: "{config.cloud.id}"', + ' cloud.auth: "elastic:"' + ], + textPost: 'Where `` is the password of the `elastic` user.' + } + ], + WINDOWS: [ + { + title: 'Edit the configuration', + textPre: 'While in the Logstash install directory, modify `config\\logstash.yml` to set the' + + ' configuration parameters for the Netflow module:', + commands: [ + 'modules:', + ' - name: netflow', + ' var.input.udp.port: ', + ' cloud.id: "{config.cloud.id}"', + ' cloud.auth: "elastic:"' + ] + } + ] + } + }, + SETUP: { + OSX: [ + { + title: 'Run the Netflow module', + textPre: 'In the Logstash installation directory, run the following command to set up the Netflow module.', + 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 of the module to avoid' + + ' overwriting existing Kibana dashboards.' + } + ], + WINDOWS: [ + { + title: 'Set up and run the Netflow module', + textPre: 'In the Logstash install directory, run the following command to set up the Netflow module.', + 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 of the module to avoid' + + ' overwriting existing Kibana 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 new file mode 100644 index 0000000000000..b27ec3a021713 --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/netflow/elastic_cloud.js @@ -0,0 +1,30 @@ +import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; +import { LOGSTASH_INSTRUCTIONS } from '../../../common/tutorials/logstash_instructions'; +import { COMMON_NETFLOW_INSTRUCTIONS } 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 + ] + } + ] + } + ] +}; diff --git a/src/core_plugins/kibana/server/tutorials/netflow/index.js b/src/core_plugins/kibana/server/tutorials/netflow/index.js new file mode 100644 index 0000000000000..324432c1cd4df --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/netflow/index.js @@ -0,0 +1,23 @@ +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'; + +export function netflowSpecProvider() { + return { + id: 'netflow', + name: 'Netflow', + category: TUTORIAL_CATEGORY.SECURITY, + shortDescription: 'Collect Netflow records sent by a Netflow exporter', + longDescription: 'The Logstash Netflow module simplifies the collection, normalization, and visualization of network flow data. ' + + 'With a single command, the module parses network flow data, indexes the events into Elasticsearch, and installs a suite of Kibana ' + + 'dashboards to get you exploring your data immediately. Logstash modules support Netflow Version 5 and 9. [Learn more]' + + '({config.docs.logstash}/netflow-module.html) about the Netflow module', + //iconPath: '', TODO + completionTimeMinutes: 10, + //previewImagePath: 'kibana-apache.png', TODO + onPrem: ON_PREM_INSTRUCTIONS, + elasticCloud: ELASTIC_CLOUD_INSTRUCTIONS, + onPremElasticCloud: ON_PREM_ELASTIC_CLOUD_INSTRUCTIONS + }; +} diff --git a/src/core_plugins/kibana/server/tutorials/netflow/on_prem.js b/src/core_plugins/kibana/server/tutorials/netflow/on_prem.js new file mode 100644 index 0000000000000..c398555f9efb9 --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/netflow/on_prem.js @@ -0,0 +1,30 @@ +import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; +import { LOGSTASH_INSTRUCTIONS } from '../../../common/tutorials/logstash_instructions'; +import { COMMON_NETFLOW_INSTRUCTIONS } 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 + ] + } + ] + } + ] +}; 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 new file mode 100644 index 0000000000000..7afb95671d708 --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/netflow/on_prem_elastic_cloud.js @@ -0,0 +1,38 @@ +import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; +import { LOGSTASH_INSTRUCTIONS } from '../../../common/tutorials/logstash_instructions'; +import { + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2 +} from '../../../common/tutorials/onprem_cloud_instructions'; +import { COMMON_NETFLOW_INSTRUCTIONS } 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.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.WINDOWS, + ...COMMON_NETFLOW_INSTRUCTIONS.SETUP.WINDOWS + ] + } + ] + } + ] +}; diff --git a/src/core_plugins/kibana/server/tutorials/nginx_logs/elastic_cloud.js b/src/core_plugins/kibana/server/tutorials/nginx_logs/elastic_cloud.js new file mode 100644 index 0000000000000..9a517988c9a9d --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/nginx_logs/elastic_cloud.js @@ -0,0 +1,50 @@ +import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; +import { FILEBEAT_INSTRUCTIONS } from '../../../common/tutorials/filebeat_instructions'; +import { FILEBEAT_CLOUD_INSTRUCTIONS } from '../../../common/tutorials/filebeat_cloud_instructions'; +import { ENABLE_INSTRUCTIONS } from './enable'; + +export const ELASTIC_CLOUD_INSTRUCTIONS = { + instructionSets: [ + { + title: 'Getting Started', + instructionVariants: [ + { + id: INSTRUCTION_VARIANT.OSX, + instructions: [ + FILEBEAT_INSTRUCTIONS.INSTALL.OSX, + FILEBEAT_CLOUD_INSTRUCTIONS.CONFIG.OSX, + ENABLE_INSTRUCTIONS.OSX, + FILEBEAT_INSTRUCTIONS.START.OSX + ] + }, + { + id: INSTRUCTION_VARIANT.DEB, + instructions: [ + FILEBEAT_INSTRUCTIONS.INSTALL.DEB, + FILEBEAT_CLOUD_INSTRUCTIONS.CONFIG.DEB, + ENABLE_INSTRUCTIONS.DEB, + FILEBEAT_INSTRUCTIONS.START.DEB + ] + }, + { + id: INSTRUCTION_VARIANT.RPM, + instructions: [ + FILEBEAT_INSTRUCTIONS.INSTALL.RPM, + FILEBEAT_CLOUD_INSTRUCTIONS.CONFIG.RPM, + ENABLE_INSTRUCTIONS.RPM, + FILEBEAT_INSTRUCTIONS.START.RPM + ] + }, + { + id: INSTRUCTION_VARIANT.WINDOWS, + instructions: [ + FILEBEAT_INSTRUCTIONS.INSTALL.WINDOWS, + FILEBEAT_CLOUD_INSTRUCTIONS.CONFIG.WINDOWS, + ENABLE_INSTRUCTIONS.WINDOWS, + FILEBEAT_INSTRUCTIONS.START.WINDOWS + ] + } + ] + } + ] +}; diff --git a/src/core_plugins/kibana/server/tutorials/nginx_logs/enable.js b/src/core_plugins/kibana/server/tutorials/nginx_logs/enable.js new file mode 100644 index 0000000000000..70be5bc520555 --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/nginx_logs/enable.js @@ -0,0 +1,32 @@ +export const ENABLE_INSTRUCTIONS = { + OSX: { + title: 'Enable and configure the nginx module', + textPre: 'From the installation directory, run:', + commands: [ + './filebeat modules enable nginx', + ], + textPost: 'Modify the settings in the `modules.d/nginx.yml` file.' + }, + DEB: { + title: 'Enable and configure the nginx module', + commands: [ + 'sudo filebeat modules enable nginx', + ], + textPost: 'Modify the settings in the `/etc/filebeat/modules.d/nginx.yml` file.' + }, + RPM: { + title: 'Enable and configure the nginx module', + commands: [ + 'sudo filebeat modules enable nginx', + ], + textPost: 'Modify the settings in the `/etc/filebeat/modules.d/nginx.yml` file.' + }, + WINDOWS: { + title: 'Enable and configure the nginx module', + textPre: 'From the `C:\\Program Files\\Filebeat` folder, run:', + commands: [ + 'PS C:\\Program Files\\Filebeat> filebeat.exe modules enable nginx', + ], + textPost: 'Modify the settings in the `modules.d/nginx.yml` file.' + } +}; diff --git a/src/core_plugins/kibana/server/tutorials/nginx_logs/index.js b/src/core_plugins/kibana/server/tutorials/nginx_logs/index.js new file mode 100644 index 0000000000000..f6ab47590c153 --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/nginx_logs/index.js @@ -0,0 +1,34 @@ +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'; + +export function nginxLogsSpecProvider() { + return { + id: 'nginxLogs', + name: '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)' + + ' about the nginx module.', + //iconPath: '', TODO + artifacts: { + dashboards: [ + { + title: '55a9e6e0-a29e-11e7-928f-5dbe6f6f5519', + linkLabel: 'Nginx logs dashboard', + isOverview: true + } + ], + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-nginx.html' + } + }, + completionTimeMinutes: 10, + previewImagePath: '/plugins/kibana/home/tutorial_resources/nginx_logs/screenshot.png', + onPrem: ON_PREM_INSTRUCTIONS, + elasticCloud: ELASTIC_CLOUD_INSTRUCTIONS, + onPremElasticCloud: ON_PREM_ELASTIC_CLOUD_INSTRUCTIONS + }; +} diff --git a/src/core_plugins/kibana/server/tutorials/nginx_logs/on_prem.js b/src/core_plugins/kibana/server/tutorials/nginx_logs/on_prem.js new file mode 100644 index 0000000000000..9ecaa557d2407 --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/nginx_logs/on_prem.js @@ -0,0 +1,53 @@ +import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; +import { FILEBEAT_INSTRUCTIONS } from '../../../common/tutorials/filebeat_instructions'; +import { ENABLE_INSTRUCTIONS } from './enable'; + +export const ON_PREM_INSTRUCTIONS = { + instructionSets: [ + { + title: 'Getting Started', + instructionVariants: [ + { + id: INSTRUCTION_VARIANT.OSX, + instructions: [ + FILEBEAT_INSTRUCTIONS.PLUGINS.GEOIP_AND_UA, + FILEBEAT_INSTRUCTIONS.INSTALL.OSX, + FILEBEAT_INSTRUCTIONS.CONFIG.OSX, + ENABLE_INSTRUCTIONS.OSX, + FILEBEAT_INSTRUCTIONS.START.OSX + ] + }, + { + id: INSTRUCTION_VARIANT.DEB, + instructions: [ + FILEBEAT_INSTRUCTIONS.PLUGINS.GEOIP_AND_UA, + FILEBEAT_INSTRUCTIONS.INSTALL.DEB, + FILEBEAT_INSTRUCTIONS.CONFIG.DEB, + ENABLE_INSTRUCTIONS.DEB, + FILEBEAT_INSTRUCTIONS.START.DEB + ] + }, + { + id: INSTRUCTION_VARIANT.RPM, + instructions: [ + FILEBEAT_INSTRUCTIONS.PLUGINS.GEOIP_AND_UA, + FILEBEAT_INSTRUCTIONS.INSTALL.RPM, + FILEBEAT_INSTRUCTIONS.CONFIG.RPM, + ENABLE_INSTRUCTIONS.RPM, + FILEBEAT_INSTRUCTIONS.START.RPM + ] + }, + { + id: INSTRUCTION_VARIANT.WINDOWS, + instructions: [ + FILEBEAT_INSTRUCTIONS.PLUGINS.GEOIP_AND_UA, + FILEBEAT_INSTRUCTIONS.INSTALL.WINDOWS, + FILEBEAT_INSTRUCTIONS.CONFIG.WINDOWS, + ENABLE_INSTRUCTIONS.WINDOWS, + FILEBEAT_INSTRUCTIONS.START.WINDOWS + ] + } + ] + } + ] +}; diff --git a/src/core_plugins/kibana/server/tutorials/nginx_logs/on_prem_elastic_cloud.js b/src/core_plugins/kibana/server/tutorials/nginx_logs/on_prem_elastic_cloud.js new file mode 100644 index 0000000000000..2266d33eccd1f --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/nginx_logs/on_prem_elastic_cloud.js @@ -0,0 +1,61 @@ +import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; +import { FILEBEAT_INSTRUCTIONS } from '../../../common/tutorials/filebeat_instructions'; +import { + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2 +} from '../../../common/tutorials/onprem_cloud_instructions'; +import { ENABLE_INSTRUCTIONS } from './enable'; + +export const ON_PREM_ELASTIC_CLOUD_INSTRUCTIONS = { + instructionSets: [ + { + title: 'Getting Started', + instructionVariants: [ + { + id: INSTRUCTION_VARIANT.OSX, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + FILEBEAT_INSTRUCTIONS.INSTALL.OSX, + FILEBEAT_INSTRUCTIONS.CONFIG.OSX, + ENABLE_INSTRUCTIONS.OSX, + FILEBEAT_INSTRUCTIONS.START.OSX + ] + }, + { + id: INSTRUCTION_VARIANT.DEB, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + FILEBEAT_INSTRUCTIONS.INSTALL.DEB, + FILEBEAT_INSTRUCTIONS.CONFIG.DEB, + ENABLE_INSTRUCTIONS.DEB, + FILEBEAT_INSTRUCTIONS.START.DEB + ] + }, + { + id: INSTRUCTION_VARIANT.RPM, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + FILEBEAT_INSTRUCTIONS.INSTALL.RPM, + FILEBEAT_INSTRUCTIONS.CONFIG.RPM, + ENABLE_INSTRUCTIONS.RPM, + FILEBEAT_INSTRUCTIONS.START.RPM + ] + }, + { + id: INSTRUCTION_VARIANT.WINDOWS, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + FILEBEAT_INSTRUCTIONS.INSTALL.WINDOWS, + FILEBEAT_INSTRUCTIONS.CONFIG.WINDOWS, + ENABLE_INSTRUCTIONS.WINDOWS, + FILEBEAT_INSTRUCTIONS.START.WINDOWS + ] + } + ] + } + ] +}; diff --git a/src/core_plugins/kibana/server/tutorials/nginx_metrics/elastic_cloud.js b/src/core_plugins/kibana/server/tutorials/nginx_metrics/elastic_cloud.js new file mode 100644 index 0000000000000..408ae869571e7 --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/nginx_metrics/elastic_cloud.js @@ -0,0 +1,50 @@ +import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; +import { METRICBEAT_INSTRUCTIONS } from '../../../common/tutorials/metricbeat_instructions'; +import { METRICBEAT_CLOUD_INSTRUCTIONS } from '../../../common/tutorials/metricbeat_cloud_instructions'; +import { ENABLE_INSTRUCTIONS } from './enable'; + +export const ELASTIC_CLOUD_INSTRUCTIONS = { + instructionSets: [ + { + title: 'Getting Started', + instructionVariants: [ + { + id: INSTRUCTION_VARIANT.OSX, + instructions: [ + METRICBEAT_INSTRUCTIONS.INSTALL.OSX, + METRICBEAT_CLOUD_INSTRUCTIONS.CONFIG.OSX, + ENABLE_INSTRUCTIONS.OSX, + METRICBEAT_INSTRUCTIONS.START.OSX + ] + }, + { + id: INSTRUCTION_VARIANT.DEB, + instructions: [ + METRICBEAT_INSTRUCTIONS.INSTALL.DEB, + METRICBEAT_CLOUD_INSTRUCTIONS.CONFIG.DEB, + ENABLE_INSTRUCTIONS.DEB, + METRICBEAT_INSTRUCTIONS.START.DEB + ] + }, + { + id: INSTRUCTION_VARIANT.RPM, + instructions: [ + METRICBEAT_INSTRUCTIONS.INSTALL.RPM, + METRICBEAT_CLOUD_INSTRUCTIONS.CONFIG.RPM, + ENABLE_INSTRUCTIONS.RPM, + METRICBEAT_INSTRUCTIONS.START.RPM + ] + }, + { + id: INSTRUCTION_VARIANT.WINDOWS, + instructions: [ + METRICBEAT_INSTRUCTIONS.INSTALL.WINDOWS, + METRICBEAT_CLOUD_INSTRUCTIONS.CONFIG.WINDOWS, + ENABLE_INSTRUCTIONS.WINDOWS, + METRICBEAT_INSTRUCTIONS.START.WINDOWS + ] + } + ] + } + ] +}; diff --git a/src/core_plugins/kibana/server/tutorials/nginx_metrics/enable.js b/src/core_plugins/kibana/server/tutorials/nginx_metrics/enable.js new file mode 100644 index 0000000000000..8f30486c54c90 --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/nginx_metrics/enable.js @@ -0,0 +1,32 @@ +export const ENABLE_INSTRUCTIONS = { + OSX: { + title: 'Enable and configure the nginx module', + textPre: 'From the installation directory, run:', + commands: [ + './metricbeat modules enable nginx', + ], + textPost: 'Modify the settings in the `modules.d/nginx.yml` file.' + }, + DEB: { + title: 'Enable and configure the nginx module', + commands: [ + 'sudo metricbeat modules enable nginx', + ], + textPost: 'Modify the settings in the `/etc/metricbeat/modules.d/nginx.yml` file.' + }, + RPM: { + title: 'Enable and configure the nginx module', + commands: [ + 'sudo metricbeat modules enable nginx', + ], + textPost: 'Modify the settings in the `/etc/metricbeat/modules.d/nginx.yml` file.' + }, + WINDOWS: { + title: 'Enable and configure the nginx module', + textPre: 'From the `C:\\Program Files\\Metricbeat` folder, run:', + commands: [ + 'PS C:\\Program Files\\Metricbeat> metricbeat.exe modules enable nginx', + ], + textPost: 'Modify the settings in the `modules.d/nginx.yml` file.' + } +}; diff --git a/src/core_plugins/kibana/server/tutorials/nginx_metrics/index.js b/src/core_plugins/kibana/server/tutorials/nginx_metrics/index.js new file mode 100644 index 0000000000000..860422d892a6b --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/nginx_metrics/index.js @@ -0,0 +1,37 @@ +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'; + +export function nginxMetricsSpecProvider() { + return { + id: 'nginxMetrics', + name: 'Nginx metrics', + category: TUTORIAL_CATEGORY.METRICS, + shortDescription: 'Fetches 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)' + + ' module, which needs to be enabled in you Nginx installation.' + + ' [Learn more]({config.docs.beats.metricbeat}/metricbeat-module-nginx.html)' + + ' about the nginx module.', + //iconPath: '', TODO + artifacts: { + dashboards: [ + { + title: '023d2930-f1a5-11e7-a9ef-93c69af7b129', + linkLabel: 'Nginx metrics dashboard', + isOverview: true + } + ], + exportedFields: { + documentationUrl: '{config.docs.beats.metricbeat}/exported-fields-nginx.html' + } + }, + completionTimeMinutes: 10, + previewImagePath: '/plugins/kibana/home/tutorial_resources/nginx_metrics/screenshot.png', + onPrem: ON_PREM_INSTRUCTIONS, + elasticCloud: ELASTIC_CLOUD_INSTRUCTIONS, + onPremElasticCloud: ON_PREM_ELASTIC_CLOUD_INSTRUCTIONS + }; +} diff --git a/src/core_plugins/kibana/server/tutorials/nginx_metrics/on_prem.js b/src/core_plugins/kibana/server/tutorials/nginx_metrics/on_prem.js new file mode 100644 index 0000000000000..8a47014420ebe --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/nginx_metrics/on_prem.js @@ -0,0 +1,49 @@ +import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; +import { METRICBEAT_INSTRUCTIONS } from '../../../common/tutorials/metricbeat_instructions'; +import { ENABLE_INSTRUCTIONS } from './enable'; + +export const ON_PREM_INSTRUCTIONS = { + instructionSets: [ + { + title: 'Getting Started', + instructionVariants: [ + { + id: INSTRUCTION_VARIANT.OSX, + instructions: [ + METRICBEAT_INSTRUCTIONS.INSTALL.OSX, + METRICBEAT_INSTRUCTIONS.CONFIG.OSX, + ENABLE_INSTRUCTIONS.OSX, + METRICBEAT_INSTRUCTIONS.START.OSX + ] + }, + { + id: INSTRUCTION_VARIANT.DEB, + instructions: [ + METRICBEAT_INSTRUCTIONS.INSTALL.DEB, + METRICBEAT_INSTRUCTIONS.CONFIG.DEB, + ENABLE_INSTRUCTIONS.DEB, + METRICBEAT_INSTRUCTIONS.START.DEB + ] + }, + { + id: INSTRUCTION_VARIANT.RPM, + instructions: [ + METRICBEAT_INSTRUCTIONS.INSTALL.RPM, + METRICBEAT_INSTRUCTIONS.CONFIG.RPM, + ENABLE_INSTRUCTIONS.RPM, + METRICBEAT_INSTRUCTIONS.START.RPM + ] + }, + { + id: INSTRUCTION_VARIANT.WINDOWS, + instructions: [ + METRICBEAT_INSTRUCTIONS.INSTALL.WINDOWS, + METRICBEAT_INSTRUCTIONS.CONFIG.WINDOWS, + ENABLE_INSTRUCTIONS.WINDOWS, + METRICBEAT_INSTRUCTIONS.START.WINDOWS + ] + } + ] + } + ] +}; diff --git a/src/core_plugins/kibana/server/tutorials/nginx_metrics/on_prem_elastic_cloud.js b/src/core_plugins/kibana/server/tutorials/nginx_metrics/on_prem_elastic_cloud.js new file mode 100644 index 0000000000000..fe1cdbcde9391 --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/nginx_metrics/on_prem_elastic_cloud.js @@ -0,0 +1,61 @@ +import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; +import { METRICBEAT_INSTRUCTIONS } from '../../../common/tutorials/metricbeat_instructions'; +import { + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2 +} from '../../../common/tutorials/onprem_cloud_instructions'; +import { ENABLE_INSTRUCTIONS } from './enable'; + +export const ON_PREM_ELASTIC_CLOUD_INSTRUCTIONS = { + instructionSets: [ + { + title: 'Getting Started', + instructionVariants: [ + { + id: INSTRUCTION_VARIANT.OSX, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + METRICBEAT_INSTRUCTIONS.INSTALL.OSX, + METRICBEAT_INSTRUCTIONS.CONFIG.OSX, + ENABLE_INSTRUCTIONS.OSX, + METRICBEAT_INSTRUCTIONS.START.OSX + ] + }, + { + id: INSTRUCTION_VARIANT.DEB, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + METRICBEAT_INSTRUCTIONS.INSTALL.DEB, + METRICBEAT_INSTRUCTIONS.CONFIG.DEB, + ENABLE_INSTRUCTIONS.DEB, + METRICBEAT_INSTRUCTIONS.START.DEB + ] + }, + { + id: INSTRUCTION_VARIANT.RPM, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + METRICBEAT_INSTRUCTIONS.INSTALL.RPM, + METRICBEAT_INSTRUCTIONS.CONFIG.RPM, + ENABLE_INSTRUCTIONS.RPM, + METRICBEAT_INSTRUCTIONS.START.RPM + ] + }, + { + id: INSTRUCTION_VARIANT.WINDOWS, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + METRICBEAT_INSTRUCTIONS.INSTALL.WINDOWS, + METRICBEAT_INSTRUCTIONS.CONFIG.WINDOWS, + ENABLE_INSTRUCTIONS.WINDOWS, + METRICBEAT_INSTRUCTIONS.START.WINDOWS + ] + } + ] + } + ] +}; diff --git a/src/core_plugins/kibana/server/tutorials/register.js b/src/core_plugins/kibana/server/tutorials/register.js new file mode 100644 index 0000000000000..7383f10de5cff --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/register.js @@ -0,0 +1,21 @@ +import { systemLogsSpecProvider } from './system_logs'; +import { systemMetricsSpecProvider } from './system_metrics'; +import { apacheLogsSpecProvider } from './apache_logs'; +import { apacheMetricsSpecProvider } from './apache_metrics'; +import { nginxLogsSpecProvider } from './nginx_logs'; +import { nginxMetricsSpecProvider } from './nginx_metrics'; +import { mysqlLogsSpecProvider } from './mysql_logs'; +import { mysqlMetricsSpecProvider } from './mysql_metrics'; +import { netflowSpecProvider } from './netflow'; + +export function registerTutorials(server) { + server.registerTutorial(systemLogsSpecProvider); + server.registerTutorial(systemMetricsSpecProvider); + server.registerTutorial(apacheLogsSpecProvider); + server.registerTutorial(apacheMetricsSpecProvider); + server.registerTutorial(nginxLogsSpecProvider); + server.registerTutorial(nginxMetricsSpecProvider); + server.registerTutorial(mysqlLogsSpecProvider); + server.registerTutorial(mysqlMetricsSpecProvider); + server.registerTutorial(netflowSpecProvider); +} diff --git a/src/core_plugins/kibana/server/tutorials/system_logs/elastic_cloud.js b/src/core_plugins/kibana/server/tutorials/system_logs/elastic_cloud.js new file mode 100644 index 0000000000000..9a517988c9a9d --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/system_logs/elastic_cloud.js @@ -0,0 +1,50 @@ +import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; +import { FILEBEAT_INSTRUCTIONS } from '../../../common/tutorials/filebeat_instructions'; +import { FILEBEAT_CLOUD_INSTRUCTIONS } from '../../../common/tutorials/filebeat_cloud_instructions'; +import { ENABLE_INSTRUCTIONS } from './enable'; + +export const ELASTIC_CLOUD_INSTRUCTIONS = { + instructionSets: [ + { + title: 'Getting Started', + instructionVariants: [ + { + id: INSTRUCTION_VARIANT.OSX, + instructions: [ + FILEBEAT_INSTRUCTIONS.INSTALL.OSX, + FILEBEAT_CLOUD_INSTRUCTIONS.CONFIG.OSX, + ENABLE_INSTRUCTIONS.OSX, + FILEBEAT_INSTRUCTIONS.START.OSX + ] + }, + { + id: INSTRUCTION_VARIANT.DEB, + instructions: [ + FILEBEAT_INSTRUCTIONS.INSTALL.DEB, + FILEBEAT_CLOUD_INSTRUCTIONS.CONFIG.DEB, + ENABLE_INSTRUCTIONS.DEB, + FILEBEAT_INSTRUCTIONS.START.DEB + ] + }, + { + id: INSTRUCTION_VARIANT.RPM, + instructions: [ + FILEBEAT_INSTRUCTIONS.INSTALL.RPM, + FILEBEAT_CLOUD_INSTRUCTIONS.CONFIG.RPM, + ENABLE_INSTRUCTIONS.RPM, + FILEBEAT_INSTRUCTIONS.START.RPM + ] + }, + { + id: INSTRUCTION_VARIANT.WINDOWS, + instructions: [ + FILEBEAT_INSTRUCTIONS.INSTALL.WINDOWS, + FILEBEAT_CLOUD_INSTRUCTIONS.CONFIG.WINDOWS, + ENABLE_INSTRUCTIONS.WINDOWS, + FILEBEAT_INSTRUCTIONS.START.WINDOWS + ] + } + ] + } + ] +}; diff --git a/src/core_plugins/kibana/server/tutorials/system_logs/enable.js b/src/core_plugins/kibana/server/tutorials/system_logs/enable.js new file mode 100644 index 0000000000000..32ed797e76367 --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/system_logs/enable.js @@ -0,0 +1,32 @@ +export const ENABLE_INSTRUCTIONS = { + OSX: { + title: 'Enable and configure the system module', + textPre: 'From the installation directory, run:', + commands: [ + './filebeat modules enable system', + ], + textPost: 'Modify the settings in the `modules.d/system.yml` file.' + }, + DEB: { + title: 'Enable and configure the system module', + commands: [ + 'sudo filebeat modules enable system', + ], + textPost: 'Modify the settings in the `/etc/filebeat/modules.d/system.yml` file.' + }, + RPM: { + title: 'Enable and configure the system module', + commands: [ + 'sudo filebeat modules enable system', + ], + textPost: 'Modify the settings in the `/etc/filebeat/modules.d/system.yml` file.' + }, + WINDOWS: { + title: 'Enable and configure the system module', + textPre: 'From the `C:\\Program Files\\Filebeat` folder, run:', + commands: [ + 'PS C:\\Program Files\\Filebeat> filebeat.exe modules enable system', + ], + textPost: 'Modify the settings in the `modules.d/system.yml` file.' + } +}; diff --git a/src/core_plugins/kibana/server/tutorials/system_logs/index.js b/src/core_plugins/kibana/server/tutorials/system_logs/index.js new file mode 100644 index 0000000000000..40bec08f98401 --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/system_logs/index.js @@ -0,0 +1,35 @@ +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'; + +export function systemLogsSpecProvider() { + return { + id: 'systemLogs', + name: '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)' + + ' about the `system` module.', + //iconPath: '', TODO + artifacts: { + dashboards: [ + { + title: 'Filebeat-syslog-dashboard', + linkLabel: 'System logs dashboard', + isOverview: true + } + ], + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-system.html' + } + }, + completionTimeMinutes: 10, + previewImagePath: '/plugins/kibana/home/tutorial_resources/system_logs/screenshot.png', + onPrem: ON_PREM_INSTRUCTIONS, + elasticCloud: ELASTIC_CLOUD_INSTRUCTIONS, + onPremElasticCloud: ON_PREM_ELASTIC_CLOUD_INSTRUCTIONS + }; +} diff --git a/src/core_plugins/kibana/server/tutorials/system_logs/on_prem.js b/src/core_plugins/kibana/server/tutorials/system_logs/on_prem.js new file mode 100644 index 0000000000000..47c9024326573 --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/system_logs/on_prem.js @@ -0,0 +1,53 @@ +import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; +import { FILEBEAT_INSTRUCTIONS } from '../../../common/tutorials/filebeat_instructions'; +import { ENABLE_INSTRUCTIONS } from './enable'; + +export const ON_PREM_INSTRUCTIONS = { + instructionSets: [ + { + title: 'Getting Started', + instructionVariants: [ + { + id: INSTRUCTION_VARIANT.OSX, + instructions: [ + FILEBEAT_INSTRUCTIONS.PLUGINS.GEOIP, + FILEBEAT_INSTRUCTIONS.INSTALL.OSX, + FILEBEAT_INSTRUCTIONS.CONFIG.OSX, + ENABLE_INSTRUCTIONS.OSX, + FILEBEAT_INSTRUCTIONS.START.OSX + ] + }, + { + id: INSTRUCTION_VARIANT.DEB, + instructions: [ + FILEBEAT_INSTRUCTIONS.PLUGINS.GEOIP, + FILEBEAT_INSTRUCTIONS.INSTALL.DEB, + FILEBEAT_INSTRUCTIONS.CONFIG.DEB, + ENABLE_INSTRUCTIONS.DEB, + FILEBEAT_INSTRUCTIONS.START.DEB + ] + }, + { + id: INSTRUCTION_VARIANT.RPM, + instructions: [ + FILEBEAT_INSTRUCTIONS.PLUGINS.GEOIP, + FILEBEAT_INSTRUCTIONS.INSTALL.RPM, + FILEBEAT_INSTRUCTIONS.CONFIG.RPM, + ENABLE_INSTRUCTIONS.RPM, + FILEBEAT_INSTRUCTIONS.START.RPM + ] + }, + { + id: INSTRUCTION_VARIANT.WINDOWS, + instructions: [ + FILEBEAT_INSTRUCTIONS.PLUGINS.GEOIP, + FILEBEAT_INSTRUCTIONS.INSTALL.WINDOWS, + FILEBEAT_INSTRUCTIONS.CONFIG.WINDOWS, + ENABLE_INSTRUCTIONS.WINDOWS, + FILEBEAT_INSTRUCTIONS.START.WINDOWS + ] + } + ] + } + ] +}; diff --git a/src/core_plugins/kibana/server/tutorials/system_logs/on_prem_elastic_cloud.js b/src/core_plugins/kibana/server/tutorials/system_logs/on_prem_elastic_cloud.js new file mode 100644 index 0000000000000..2266d33eccd1f --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/system_logs/on_prem_elastic_cloud.js @@ -0,0 +1,61 @@ +import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; +import { FILEBEAT_INSTRUCTIONS } from '../../../common/tutorials/filebeat_instructions'; +import { + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2 +} from '../../../common/tutorials/onprem_cloud_instructions'; +import { ENABLE_INSTRUCTIONS } from './enable'; + +export const ON_PREM_ELASTIC_CLOUD_INSTRUCTIONS = { + instructionSets: [ + { + title: 'Getting Started', + instructionVariants: [ + { + id: INSTRUCTION_VARIANT.OSX, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + FILEBEAT_INSTRUCTIONS.INSTALL.OSX, + FILEBEAT_INSTRUCTIONS.CONFIG.OSX, + ENABLE_INSTRUCTIONS.OSX, + FILEBEAT_INSTRUCTIONS.START.OSX + ] + }, + { + id: INSTRUCTION_VARIANT.DEB, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + FILEBEAT_INSTRUCTIONS.INSTALL.DEB, + FILEBEAT_INSTRUCTIONS.CONFIG.DEB, + ENABLE_INSTRUCTIONS.DEB, + FILEBEAT_INSTRUCTIONS.START.DEB + ] + }, + { + id: INSTRUCTION_VARIANT.RPM, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + FILEBEAT_INSTRUCTIONS.INSTALL.RPM, + FILEBEAT_INSTRUCTIONS.CONFIG.RPM, + ENABLE_INSTRUCTIONS.RPM, + FILEBEAT_INSTRUCTIONS.START.RPM + ] + }, + { + id: INSTRUCTION_VARIANT.WINDOWS, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + FILEBEAT_INSTRUCTIONS.INSTALL.WINDOWS, + FILEBEAT_INSTRUCTIONS.CONFIG.WINDOWS, + ENABLE_INSTRUCTIONS.WINDOWS, + FILEBEAT_INSTRUCTIONS.START.WINDOWS + ] + } + ] + } + ] +}; diff --git a/src/core_plugins/kibana/server/tutorials/system_metrics/elastic_cloud.js b/src/core_plugins/kibana/server/tutorials/system_metrics/elastic_cloud.js new file mode 100644 index 0000000000000..408ae869571e7 --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/system_metrics/elastic_cloud.js @@ -0,0 +1,50 @@ +import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; +import { METRICBEAT_INSTRUCTIONS } from '../../../common/tutorials/metricbeat_instructions'; +import { METRICBEAT_CLOUD_INSTRUCTIONS } from '../../../common/tutorials/metricbeat_cloud_instructions'; +import { ENABLE_INSTRUCTIONS } from './enable'; + +export const ELASTIC_CLOUD_INSTRUCTIONS = { + instructionSets: [ + { + title: 'Getting Started', + instructionVariants: [ + { + id: INSTRUCTION_VARIANT.OSX, + instructions: [ + METRICBEAT_INSTRUCTIONS.INSTALL.OSX, + METRICBEAT_CLOUD_INSTRUCTIONS.CONFIG.OSX, + ENABLE_INSTRUCTIONS.OSX, + METRICBEAT_INSTRUCTIONS.START.OSX + ] + }, + { + id: INSTRUCTION_VARIANT.DEB, + instructions: [ + METRICBEAT_INSTRUCTIONS.INSTALL.DEB, + METRICBEAT_CLOUD_INSTRUCTIONS.CONFIG.DEB, + ENABLE_INSTRUCTIONS.DEB, + METRICBEAT_INSTRUCTIONS.START.DEB + ] + }, + { + id: INSTRUCTION_VARIANT.RPM, + instructions: [ + METRICBEAT_INSTRUCTIONS.INSTALL.RPM, + METRICBEAT_CLOUD_INSTRUCTIONS.CONFIG.RPM, + ENABLE_INSTRUCTIONS.RPM, + METRICBEAT_INSTRUCTIONS.START.RPM + ] + }, + { + id: INSTRUCTION_VARIANT.WINDOWS, + instructions: [ + METRICBEAT_INSTRUCTIONS.INSTALL.WINDOWS, + METRICBEAT_CLOUD_INSTRUCTIONS.CONFIG.WINDOWS, + ENABLE_INSTRUCTIONS.WINDOWS, + METRICBEAT_INSTRUCTIONS.START.WINDOWS + ] + } + ] + } + ] +}; diff --git a/src/core_plugins/kibana/server/tutorials/system_metrics/enable.js b/src/core_plugins/kibana/server/tutorials/system_metrics/enable.js new file mode 100644 index 0000000000000..f81af30a80ef7 --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/system_metrics/enable.js @@ -0,0 +1,32 @@ +export const ENABLE_INSTRUCTIONS = { + OSX: { + title: 'Enable and configure the system module', + textPre: 'From the installation directory, run:', + commands: [ + './metricbeat modules enable system', + ], + textPost: 'Modify the settings in the `modules.d/system.yml` file.' + }, + DEB: { + title: 'Enable and configure the system module', + commands: [ + 'sudo metricbeat modules enable system', + ], + textPost: 'Modify the settings in the `/etc/metricbeat/modules.d/system.yml` file.' + }, + RPM: { + title: 'Enable and configure the system module', + commands: [ + 'sudo metricbeat modules enable system', + ], + textPost: 'Modify the settings in the `/etc/metricbeat/modules.d/system.yml` file.' + }, + WINDOWS: { + title: 'Enable and configure the system module', + textPre: 'From the `C:\\Program Files\\Metricbeat` folder, run:', + commands: [ + 'PS C:\\Program Files\\Metricbeat> metricbeat.exe modules enable system', + ], + textPost: 'Modify the settings in the `modules.d/system.yml` file.' + } +}; diff --git a/src/core_plugins/kibana/server/tutorials/system_metrics/index.js b/src/core_plugins/kibana/server/tutorials/system_metrics/index.js new file mode 100644 index 0000000000000..0ffec8b8b2e66 --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/system_metrics/index.js @@ -0,0 +1,35 @@ +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'; + +export function systemMetricsSpecProvider() { + return { + id: 'systemMetrics', + name: 'System metrics', + category: TUTORIAL_CATEGORY.METRICS, + shortDescription: 'Collects 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 as well as per process and per filesystem statistics.' + + ' [Learn more]({config.docs.beats.metricbeat}/metricbeat-module-system.html)' + + ' about the system module.', + //iconPath: '', TODO + artifacts: { + dashboards: [ + { + title: 'Metricbeat-system-overview', + linkLabel: 'System metrics dashboard', + isOverview: true + } + ], + exportedFields: { + documentationUrl: '{config.docs.beats.metricbeat}/exported-fields-system.html' + } + }, + completionTimeMinutes: 10, + previewImagePath: '/plugins/kibana/home/tutorial_resources/system_metrics/screenshot.png', + onPrem: ON_PREM_INSTRUCTIONS, + elasticCloud: ELASTIC_CLOUD_INSTRUCTIONS, + onPremElasticCloud: ON_PREM_ELASTIC_CLOUD_INSTRUCTIONS + }; +} diff --git a/src/core_plugins/kibana/server/tutorials/system_metrics/on_prem.js b/src/core_plugins/kibana/server/tutorials/system_metrics/on_prem.js new file mode 100644 index 0000000000000..8a47014420ebe --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/system_metrics/on_prem.js @@ -0,0 +1,49 @@ +import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; +import { METRICBEAT_INSTRUCTIONS } from '../../../common/tutorials/metricbeat_instructions'; +import { ENABLE_INSTRUCTIONS } from './enable'; + +export const ON_PREM_INSTRUCTIONS = { + instructionSets: [ + { + title: 'Getting Started', + instructionVariants: [ + { + id: INSTRUCTION_VARIANT.OSX, + instructions: [ + METRICBEAT_INSTRUCTIONS.INSTALL.OSX, + METRICBEAT_INSTRUCTIONS.CONFIG.OSX, + ENABLE_INSTRUCTIONS.OSX, + METRICBEAT_INSTRUCTIONS.START.OSX + ] + }, + { + id: INSTRUCTION_VARIANT.DEB, + instructions: [ + METRICBEAT_INSTRUCTIONS.INSTALL.DEB, + METRICBEAT_INSTRUCTIONS.CONFIG.DEB, + ENABLE_INSTRUCTIONS.DEB, + METRICBEAT_INSTRUCTIONS.START.DEB + ] + }, + { + id: INSTRUCTION_VARIANT.RPM, + instructions: [ + METRICBEAT_INSTRUCTIONS.INSTALL.RPM, + METRICBEAT_INSTRUCTIONS.CONFIG.RPM, + ENABLE_INSTRUCTIONS.RPM, + METRICBEAT_INSTRUCTIONS.START.RPM + ] + }, + { + id: INSTRUCTION_VARIANT.WINDOWS, + instructions: [ + METRICBEAT_INSTRUCTIONS.INSTALL.WINDOWS, + METRICBEAT_INSTRUCTIONS.CONFIG.WINDOWS, + ENABLE_INSTRUCTIONS.WINDOWS, + METRICBEAT_INSTRUCTIONS.START.WINDOWS + ] + } + ] + } + ] +}; diff --git a/src/core_plugins/kibana/server/tutorials/system_metrics/on_prem_elastic_cloud.js b/src/core_plugins/kibana/server/tutorials/system_metrics/on_prem_elastic_cloud.js new file mode 100644 index 0000000000000..fe1cdbcde9391 --- /dev/null +++ b/src/core_plugins/kibana/server/tutorials/system_metrics/on_prem_elastic_cloud.js @@ -0,0 +1,61 @@ +import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; +import { METRICBEAT_INSTRUCTIONS } from '../../../common/tutorials/metricbeat_instructions'; +import { + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2 +} from '../../../common/tutorials/onprem_cloud_instructions'; +import { ENABLE_INSTRUCTIONS } from './enable'; + +export const ON_PREM_ELASTIC_CLOUD_INSTRUCTIONS = { + instructionSets: [ + { + title: 'Getting Started', + instructionVariants: [ + { + id: INSTRUCTION_VARIANT.OSX, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + METRICBEAT_INSTRUCTIONS.INSTALL.OSX, + METRICBEAT_INSTRUCTIONS.CONFIG.OSX, + ENABLE_INSTRUCTIONS.OSX, + METRICBEAT_INSTRUCTIONS.START.OSX + ] + }, + { + id: INSTRUCTION_VARIANT.DEB, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + METRICBEAT_INSTRUCTIONS.INSTALL.DEB, + METRICBEAT_INSTRUCTIONS.CONFIG.DEB, + ENABLE_INSTRUCTIONS.DEB, + METRICBEAT_INSTRUCTIONS.START.DEB + ] + }, + { + id: INSTRUCTION_VARIANT.RPM, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + METRICBEAT_INSTRUCTIONS.INSTALL.RPM, + METRICBEAT_INSTRUCTIONS.CONFIG.RPM, + ENABLE_INSTRUCTIONS.RPM, + METRICBEAT_INSTRUCTIONS.START.RPM + ] + }, + { + id: INSTRUCTION_VARIANT.WINDOWS, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + METRICBEAT_INSTRUCTIONS.INSTALL.WINDOWS, + METRICBEAT_INSTRUCTIONS.CONFIG.WINDOWS, + ENABLE_INSTRUCTIONS.WINDOWS, + METRICBEAT_INSTRUCTIONS.START.WINDOWS + ] + } + ] + } + ] +}; diff --git a/src/core_plugins/metrics/common/__tests__/agg_lookup.js b/src/core_plugins/metrics/common/__tests__/agg_lookup.js index 16cceaef16269..e79e77e03bcbf 100644 --- a/src/core_plugins/metrics/common/__tests__/agg_lookup.js +++ b/src/core_plugins/metrics/common/__tests__/agg_lookup.js @@ -16,7 +16,7 @@ describe('aggLookup', () => { it('returns options for all aggs', () => { const options = createOptions(); - expect(options).to.have.length(29); + expect(options).to.have.length(28); options.forEach((option) => { expect(option).to.have.property('label'); expect(option).to.have.property('value'); @@ -32,13 +32,13 @@ describe('aggLookup', () => { it('returns options for pipeline', () => { const options = createOptions('pipeline'); - expect(options).to.have.length(15); + expect(options).to.have.length(14); expect(options.every(opt => !isBasicAgg({ type: opt.value }))).to.equal(true); }); it('returns options for all if given unknown key', () => { const options = createOptions('foo'); - expect(options).to.have.length(29); + expect(options).to.have.length(28); }); }); diff --git a/src/core_plugins/metrics/common/__tests__/calculate_label.js b/src/core_plugins/metrics/common/__tests__/calculate_label.js index 8496241dcd99d..bb1ce3ebd38d0 100644 --- a/src/core_plugins/metrics/common/__tests__/calculate_label.js +++ b/src/core_plugins/metrics/common/__tests__/calculate_label.js @@ -15,7 +15,7 @@ describe('calculateLabel(metric, metrics)', () => { }); it('returns "Calcuation" for a bucket script metric', () => { - expect(calculateLabel({ type: 'calculation' })).to.equal('Bucket Script'); + expect(calculateLabel({ type: 'calculation' })).to.equal('Calculation'); }); it('returns formated label for series_agg', () => { diff --git a/src/core_plugins/metrics/common/agg_lookup.js b/src/core_plugins/metrics/common/agg_lookup.js index 670ce0fe33cd0..fc0ddf69369f1 100644 --- a/src/core_plugins/metrics/common/agg_lookup.js +++ b/src/core_plugins/metrics/common/agg_lookup.js @@ -1,7 +1,7 @@ import _ from 'lodash'; const lookup = { 'count': 'Count', - 'calculation': 'Bucket Script', + 'calculation': 'Calculation', 'std_deviation': 'Std. Deviation', 'variance': 'Variance', 'sum_of_squares': 'Sum of Sq.', @@ -24,7 +24,6 @@ const lookup = { 'sum_of_squares_bucket': 'Overall Sum of Sq.', 'std_deviation_bucket': 'Overall Std. Deviation', 'series_agg': 'Series Agg', - 'math': 'Math', 'serial_diff': 'Serial Difference', 'filter_ratio': 'Filter Ratio', 'positive_only': 'Positive Only', @@ -44,7 +43,6 @@ const pipeline = [ 'sum_of_squares_bucket', 'std_deviation_bucket', 'series_agg', - 'math', 'serial_diff', 'positive_only' ]; diff --git a/src/core_plugins/metrics/common/calculate_label.js b/src/core_plugins/metrics/common/calculate_label.js index 485c83e133e47..c3f044bbfec27 100644 --- a/src/core_plugins/metrics/common/calculate_label.js +++ b/src/core_plugins/metrics/common/calculate_label.js @@ -19,8 +19,7 @@ export default function calculateLabel(metric, metrics) { if (metric.alias) return metric.alias; if (metric.type === 'count') return 'Count'; - if (metric.type === 'calculation') return 'Bucket Script'; - if (metric.type === 'math') return 'Math'; + if (metric.type === 'calculation') return 'Calculation'; if (metric.type === 'series_agg') return `Series Agg (${metric.function})`; if (metric.type === 'filter_ratio') return 'Filter Ratio'; if (metric.type === 'static') return `Static Value of ${metric.value}`; diff --git a/src/core_plugins/metrics/public/components/aggs/agg.js b/src/core_plugins/metrics/public/components/aggs/agg.js index 5b027f480d08a..f082e53d55d62 100644 --- a/src/core_plugins/metrics/public/components/aggs/agg.js +++ b/src/core_plugins/metrics/public/components/aggs/agg.js @@ -1,14 +1,14 @@ import PropTypes from 'prop-types'; import React from 'react'; -import StdAgg from './std_agg'; import aggToComponent from '../lib/agg_to_component'; import { sortable } from 'react-anything-sortable'; +import { UnsupportedAgg } from './unsupported_agg'; function Agg(props) { const { model } = props; let Component = aggToComponent[model.type]; if (!Component) { - Component = StdAgg; + Component = UnsupportedAgg; } const style = { cursor: 'default', diff --git a/src/core_plugins/metrics/public/components/aggs/agg_select.js b/src/core_plugins/metrics/public/components/aggs/agg_select.js index f67863cc62287..f7cd3c66c26da 100644 --- a/src/core_plugins/metrics/public/components/aggs/agg_select.js +++ b/src/core_plugins/metrics/public/components/aggs/agg_select.js @@ -20,12 +20,13 @@ const metricAggs = [ ]; const pipelineAggs = [ - { label: 'Bucket Script', value: 'calculation' }, + { label: 'Calculation', value: 'calculation' }, { label: 'Cumulative Sum', value: 'cumulative_sum' }, { label: 'Derivative', value: 'derivative' }, { label: 'Moving Average', value: 'moving_average' }, { label: 'Positive Only', value: 'positive_only' }, { label: 'Serial Difference', value: 'serial_diff' }, + { label: 'Series Agg', value: 'series_agg' } ]; const siblingAggs = [ @@ -38,11 +39,6 @@ const siblingAggs = [ { label: 'Overall Variance', value: 'variance_bucket' } ]; -const specialAggs = [ - { label: 'Series Agg', value: 'series_agg' }, - { label: 'Math', value: 'math' } -]; - class AggSelectOption extends Component { constructor(props) { @@ -148,9 +144,7 @@ function AggSelect(props) { { label: 'Parent Pipeline Aggregations', value: null, pipeline: true, heading: true, disabled: true }, ...pipelineAggs.filter(filterByPanelType(panelType)).map(agg => ({ ...agg, disabled: !enablePipelines })), { label: 'Sibling Pipeline Aggregations', value: null, pipeline: true, heading: true, disabled: true }, - ...siblingAggs.map(agg => ({ ...agg, disabled: !enablePipelines })), - { label: 'Special Aggregations', value: null, pipeline: true, heading: true, disabled: true }, - ...specialAggs.map(agg => ({ ...agg, disabled: !enablePipelines })) + ...siblingAggs.map(agg => ({ ...agg, disabled: !enablePipelines })) ]; } diff --git a/src/core_plugins/metrics/public/components/aggs/math.js b/src/core_plugins/metrics/public/components/aggs/math.js deleted file mode 100644 index ffa339da228d2..0000000000000 --- a/src/core_plugins/metrics/public/components/aggs/math.js +++ /dev/null @@ -1,98 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import _ from 'lodash'; -import uuid from 'uuid'; -import AggRow from './agg_row'; -import AggSelect from './agg_select'; - -import createChangeHandler from '../lib/create_change_handler'; -import createSelectHandler from '../lib/create_select_handler'; -import createTextHandler from '../lib/create_text_handler'; -import Vars from './vars'; - -class MathAgg extends Component { - - componentWillMount() { - if (!this.props.model.variables) { - this.props.onChange(_.assign({}, this.props.model, { - variables: [{ id: uuid.v1() }] - })); - } - } - - render() { - const { siblings } = this.props; - - const defaults = { script: '' }; - const model = { ...defaults, ...this.props.model }; - - const handleChange = createChangeHandler(this.props.onChange, model); - const handleSelectChange = createSelectHandler(handleChange); - const handleTextChange = createTextHandler(handleChange); - - return ( - -
-
-
Aggregation
- -
-
Variables
- -
-
- - -
- This field uses basic math expresions (see MathJS) - Variables - are keys on the params object, i.e. params.<name> To access all the data use - params._all.<name>.values for an array of the values and params._all.<name>.timestamps - for an array of the timestamps. params._timestamp is available for the current bucket's timestamp, - params._index is available for the current bucket's index, and params._intervals available - for the interval in milliseconds. -
-
-
-
-
- ); - } - -} - -MathAgg.propTypes = { - disableDelete: PropTypes.bool, - fields: PropTypes.object, - model: PropTypes.object, - onAdd: PropTypes.func, - onChange: PropTypes.func, - onDelete: PropTypes.func, - panel: PropTypes.object, - series: PropTypes.object, - siblings: PropTypes.array, -}; - -export default MathAgg; diff --git a/src/core_plugins/metrics/public/components/aggs/metric_select.js b/src/core_plugins/metrics/public/components/aggs/metric_select.js index 0a62ae469c99e..15c34e6fe6c3e 100644 --- a/src/core_plugins/metrics/public/components/aggs/metric_select.js +++ b/src/core_plugins/metrics/public/components/aggs/metric_select.js @@ -20,14 +20,10 @@ function createTypeFilter(restrict, exclude) { // This filters out sibling aggs, percentiles, and special aggs (like Series Agg) -export function filterRows(includeSiblings) { - return row => { - if (includeSiblings) return !/^series/.test(row.type) && !/^percentile/.test(row.type) && row.type !== 'math'; - return !/_bucket$/.test(row.type) - && !/^series/.test(row.type) - && !/^percentile/.test(row.type) - && row.type !== 'math'; - }; +export function filterRows(row) { + return !/_bucket$/.test(row.type) + && !/^series/.test(row.type) + && !/^percentile/.test(row.type); } function MetricSelect(props) { @@ -36,8 +32,7 @@ function MetricSelect(props) { metric, onChange, value, - exclude, - includeSiblings + exclude } = props; const metrics = props.metrics @@ -63,7 +58,7 @@ function MetricSelect(props) { const options = siblings - .filter(filterRows(includeSiblings)) + .filter(filterRows) .map(row => { const label = calculateLabel(row, metrics); return { value: row.id, label }; @@ -85,7 +80,6 @@ MetricSelect.defaultProps = { exclude: [], metric: {}, restrict: 'none', - includeSiblings: false }; MetricSelect.propTypes = { @@ -94,8 +88,7 @@ MetricSelect.propTypes = { metric: PropTypes.object, onChange: PropTypes.func, restrict: PropTypes.string, - value: PropTypes.string, - includeSiblings: PropTypes.bool + value: PropTypes.string }; export default MetricSelect; diff --git a/src/core_plugins/metrics/public/components/aggs/unsupported_agg.js b/src/core_plugins/metrics/public/components/aggs/unsupported_agg.js new file mode 100644 index 0000000000000..f07efe9e8dc8d --- /dev/null +++ b/src/core_plugins/metrics/public/components/aggs/unsupported_agg.js @@ -0,0 +1,18 @@ +import AggRow from './agg_row'; +import React from 'react'; +export function UnsupportedAgg(props) { + return ( + +
+

The {props.model.type} aggregation is no longer supported.

+
+
+ ); + +} diff --git a/src/core_plugins/metrics/public/components/aggs/vars.js b/src/core_plugins/metrics/public/components/aggs/vars.js index 61c5816cd538f..d2b4b8f49222d 100644 --- a/src/core_plugins/metrics/public/components/aggs/vars.js +++ b/src/core_plugins/metrics/public/components/aggs/vars.js @@ -42,7 +42,6 @@ class CalculationVars extends Component { metrics={this.props.metrics} metric={this.props.model} value={row.field} - includeSiblings={this.props.includeSiblings} />
@@ -70,16 +69,14 @@ class CalculationVars extends Component { } CalculationVars.defaultProps = { - name: 'variables', - includeSiblings: false + name: 'variables' }; CalculationVars.propTypes = { metrics: PropTypes.array, model: PropTypes.object, name: PropTypes.string, - onChange: PropTypes.func, - includeSiblings: PropTypes.bool + onChange: PropTypes.func }; export default CalculationVars; diff --git a/src/core_plugins/metrics/public/components/lib/agg_to_component.js b/src/core_plugins/metrics/public/components/lib/agg_to_component.js index 20c57b082c07a..9a6b0369b26f3 100644 --- a/src/core_plugins/metrics/public/components/lib/agg_to_component.js +++ b/src/core_plugins/metrics/public/components/lib/agg_to_component.js @@ -12,7 +12,6 @@ import { PositiveOnlyAgg } from '../aggs/positive_only'; import { FilterRatioAgg } from '../aggs/filter_ratio'; import { PercentileRankAgg } from '../aggs/percentile_rank'; import { Static } from '../aggs/static'; -import MathAgg from '../aggs/math'; export default { count: StdAgg, avg: StdAgg, @@ -41,8 +40,7 @@ export default { serial_diff: SerialDiffAgg, filter_ratio: FilterRatioAgg, positive_only: PositiveOnlyAgg, - static: Static, - math: MathAgg + static: Static }; diff --git a/src/core_plugins/metrics/public/components/tooltip.js b/src/core_plugins/metrics/public/components/tooltip.js index 7d05b6f94621f..84521e2840372 100644 --- a/src/core_plugins/metrics/public/components/tooltip.js +++ b/src/core_plugins/metrics/public/components/tooltip.js @@ -1,7 +1,7 @@ import React from 'react'; -import { Tooltip } from 'pui-react-tooltip'; import PropTypes from 'prop-types'; -import { OverlayTrigger } from 'pui-react-overlay-trigger'; +import { Tooltip } from 'pivotal-ui/react/tooltip'; +import { OverlayTrigger } from 'pivotal-ui/react/overlay-trigger'; function TooltipComponent(props) { const tooltip = ( diff --git a/src/core_plugins/metrics/public/components/vis_types/timeseries/vis.js b/src/core_plugins/metrics/public/components/vis_types/timeseries/vis.js index cbf68ff53fe0a..57b22188acc92 100644 --- a/src/core_plugins/metrics/public/components/vis_types/timeseries/vis.js +++ b/src/core_plugins/metrics/public/components/vis_types/timeseries/vis.js @@ -31,8 +31,9 @@ class TimeseriesVisualization extends Component { } xaxisFormatter = (val) => { - const { visData } = this.props; - const formatter = createXaxisFormatter(this.getInterval(), visData.scaledDataFormat, visData.dateFormat); + const { scaledDataFormat, dateFormat } = this.props.visData; + if (!scaledDataFormat || !dateFormat) return val; + const formatter = createXaxisFormatter(this.getInterval(), scaledDataFormat, dateFormat); return formatter(val); } diff --git a/src/core_plugins/metrics/public/less/editor.less b/src/core_plugins/metrics/public/less/editor.less index be3e6fbba0a9d..95176d51dc162 100644 --- a/src/core_plugins/metrics/public/less/editor.less +++ b/src/core_plugins/metrics/public/less/editor.less @@ -58,15 +58,11 @@ margin: 0 10px 0 0; } } - -.vis_editor__note { - .vis_editor__label; - font-style: italic; -} .vis_editor__input { padding: 8px 10px; border-radius: @borderRadius; border: 1px solid @grayLight; + font-size: 1.1em; } .vis_editor__input-grows { .vis_editor__input; @@ -336,7 +332,6 @@ margin-bottom: 2px; padding: 10px; align-items: center; - .vis_editor__note, .vis_editor__label { margin-bottom: 5px; font-size: 12px; diff --git a/src/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_splits.js b/src/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_splits.js index 139f9f4b3e520..501f230e7b2fa 100644 --- a/src/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_splits.js +++ b/src/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_splits.js @@ -8,8 +8,7 @@ describe('getSplits(resp, panel, series)', () => { aggregations: { SERIES: { timeseries: { buckets: [] }, - SIBAGG: { value: 1 }, - meta: { bucketSize: 10 } + SIBAGG: { value: 1 } } } }; @@ -27,7 +26,6 @@ describe('getSplits(resp, panel, series)', () => { { id: 'SERIES', label: 'Overall Average of Average of cpu', - meta: { bucketSize: 10 }, color: '#FF0000', timeseries: { buckets: [] }, SIBAGG: { value: 1 } @@ -50,8 +48,7 @@ describe('getSplits(resp, panel, series)', () => { timeseries: { buckets: [] }, SIBAGG: { value: 2 } } - ], - meta: { bucketSize: 10 } + ] } } }; @@ -72,7 +69,6 @@ describe('getSplits(resp, panel, series)', () => { id: 'SERIES:example-01', key: 'example-01', label: 'example-01', - meta: { bucketSize: 10 }, color: '#FF0000', timeseries: { buckets: [] }, SIBAGG: { value: 1 } @@ -81,7 +77,6 @@ describe('getSplits(resp, panel, series)', () => { id: 'SERIES:example-02', key: 'example-02', label: 'example-02', - meta: { bucketSize: 10 }, color: '#FF0000', timeseries: { buckets: [] }, SIBAGG: { value: 2 } @@ -104,8 +99,7 @@ describe('getSplits(resp, panel, series)', () => { timeseries: { buckets: [] }, SIBAGG: { value: 2 } } - ], - meta: { bucketSize: 10 } + ] } } }; @@ -126,7 +120,6 @@ describe('getSplits(resp, panel, series)', () => { id: 'SERIES:example-01', key: 'example-01', label: 'example-01', - meta: { bucketSize: 10 }, color: '#FF0000', timeseries: { buckets: [] }, SIBAGG: { value: 1 } @@ -135,7 +128,6 @@ describe('getSplits(resp, panel, series)', () => { id: 'SERIES:example-02', key: 'example-02', label: 'example-02', - meta: { bucketSize: 10 }, color: '#930000', timeseries: { buckets: [] }, SIBAGG: { value: 2 } @@ -154,8 +146,7 @@ describe('getSplits(resp, panel, series)', () => { 'filter-2': { timeseries: { buckets: [] }, } - }, - meta: { bucketSize: 10 } + } } } }; @@ -177,7 +168,6 @@ describe('getSplits(resp, panel, series)', () => { id: 'SERIES:filter-1', key: 'filter-1', label: '200s', - meta: { bucketSize: 10 }, color: '#F00', timeseries: { buckets: [] }, }, @@ -185,7 +175,6 @@ describe('getSplits(resp, panel, series)', () => { id: 'SERIES:filter-2', key: 'filter-2', label: '300s', - meta: { bucketSize: 10 }, color: '#0F0', timeseries: { buckets: [] }, } diff --git a/src/core_plugins/metrics/server/lib/vis_data/helpers/get_splits.js b/src/core_plugins/metrics/server/lib/vis_data/helpers/get_splits.js index 139f540fbd62a..911e6d42fcb10 100644 --- a/src/core_plugins/metrics/server/lib/vis_data/helpers/get_splits.js +++ b/src/core_plugins/metrics/server/lib/vis_data/helpers/get_splits.js @@ -5,7 +5,6 @@ import getLastMetric from './get_last_metric'; import getSplitColors from './get_split_colors'; import { formatKey } from './format_key'; export default function getSplits(resp, panel, series) { - const meta = _.get(resp, `aggregations.${series.id}.meta`); const color = new Color(series.color); const metric = getLastMetric(series); if (_.has(resp, `aggregations.${series.id}.buckets`)) { @@ -17,7 +16,6 @@ export default function getSplits(resp, panel, series) { bucket.id = `${series.id}:${bucket.key}`; bucket.label = formatKey(bucket.key, series); bucket.color = panel.type === 'top_n' ? color.hex() : colors.shift(); - bucket.meta = meta; return bucket; }); } @@ -29,7 +27,6 @@ export default function getSplits(resp, panel, series) { bucket.key = filter.id; bucket.color = filter.color; bucket.label = filter.label || filter.filter || '*'; - bucket.meta = meta; return bucket; }); } @@ -49,8 +46,7 @@ export default function getSplits(resp, panel, series) { id: series.id, label: series.label || calculateLabel(metric, series.metrics), color: color.hex(), - ...mergeObj, - meta + ...mergeObj } ]; } diff --git a/src/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/date_histogram.js b/src/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/date_histogram.js index e8f5bdd94088a..cb0046f6e7ed2 100644 --- a/src/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/date_histogram.js +++ b/src/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/date_histogram.js @@ -50,11 +50,6 @@ describe('dateHistogram(req, panel, series)', () => { } } } - }, - meta: { - bucketSize: 10, - intervalString: '10s', - timeField: '@timestamp' } } } @@ -81,11 +76,6 @@ describe('dateHistogram(req, panel, series)', () => { } } } - }, - meta: { - bucketSize: 10, - intervalString: '10s', - timeField: '@timestamp' } } } @@ -115,11 +105,6 @@ describe('dateHistogram(req, panel, series)', () => { } } } - }, - meta: { - bucketSize: 20, - intervalString: '20s', - timeField: 'timestamp' } } } diff --git a/src/core_plugins/metrics/server/lib/vis_data/request_processors/series/date_histogram.js b/src/core_plugins/metrics/server/lib/vis_data/request_processors/series/date_histogram.js index 6e801f019ba8c..b4008ae0b2801 100644 --- a/src/core_plugins/metrics/server/lib/vis_data/request_processors/series/date_histogram.js +++ b/src/core_plugins/metrics/server/lib/vis_data/request_processors/series/date_histogram.js @@ -5,7 +5,7 @@ import { set } from 'lodash'; export default function dateHistogram(req, panel, series) { return next => doc => { const { timeField, interval } = getIntervalAndTimefield(panel, series); - const { bucketSize, intervalString } = getBucketSize(req, interval); + const { intervalString } = getBucketSize(req, interval); const { from, to } = offsetTime(req, series.offset_time); const { timezone } = req.payload.timerange; @@ -19,11 +19,6 @@ export default function dateHistogram(req, panel, series) { max: to.valueOf() } }); - set(doc, `aggs.${series.id}.meta`, { - timeField, - intervalString, - bucketSize - }); return next(doc); }; } diff --git a/src/core_plugins/metrics/server/lib/vis_data/request_processors/table/date_histogram.js b/src/core_plugins/metrics/server/lib/vis_data/request_processors/table/date_histogram.js index fdeae19826015..5d80b11dfff5c 100644 --- a/src/core_plugins/metrics/server/lib/vis_data/request_processors/table/date_histogram.js +++ b/src/core_plugins/metrics/server/lib/vis_data/request_processors/table/date_histogram.js @@ -1,4 +1,4 @@ -import { set } from 'lodash'; +import _ from 'lodash'; import getBucketSize from '../../helpers/get_bucket_size'; import getIntervalAndTimefield from '../../get_interval_and_timefield'; import getTimerange from '../../helpers/get_timerange'; @@ -7,11 +7,11 @@ import { calculateAggRoot } from './calculate_agg_root'; export default function dateHistogram(req, panel) { return next => doc => { const { timeField, interval } = getIntervalAndTimefield(panel); - const { bucketSize, intervalString } = getBucketSize(req, interval); + const { intervalString } = getBucketSize(req, interval); const { from, to } = getTimerange(req); panel.series.forEach(column => { const aggRoot = calculateAggRoot(doc, column); - set(doc, `${aggRoot}.timeseries.date_histogram`, { + _.set(doc, `${aggRoot}.timeseries.date_histogram`, { field: timeField, interval: intervalString, min_doc_count: 0, @@ -20,11 +20,6 @@ export default function dateHistogram(req, panel) { max: to.valueOf() } }); - set(doc, aggRoot.replace(/\.aggs$/, '.meta'), { - timeField, - intervalString, - bucketSize - }); }); return next(doc); }; diff --git a/src/core_plugins/metrics/server/lib/vis_data/response_processors/series/index.js b/src/core_plugins/metrics/server/lib/vis_data/response_processors/series/index.js index 22ac278ed812d..c57aee7f222e0 100644 --- a/src/core_plugins/metrics/server/lib/vis_data/response_processors/series/index.js +++ b/src/core_plugins/metrics/server/lib/vis_data/response_processors/series/index.js @@ -6,7 +6,6 @@ import stdMetric from './std_metric'; import stdSibling from './std_sibling'; import timeShift from './time_shift'; import { dropLastBucket } from './drop_last_bucket'; -import { mathAgg } from './math'; export default [ percentile, @@ -14,7 +13,6 @@ export default [ stdDeviationSibling, stdMetric, stdSibling, - mathAgg, seriesAgg, timeShift, dropLastBucket diff --git a/src/core_plugins/metrics/server/lib/vis_data/response_processors/series/math.js b/src/core_plugins/metrics/server/lib/vis_data/response_processors/series/math.js deleted file mode 100644 index c5bb587acc18f..0000000000000 --- a/src/core_plugins/metrics/server/lib/vis_data/response_processors/series/math.js +++ /dev/null @@ -1,110 +0,0 @@ -const percentileValueMatch = /\[([0-9\.]+)\]$/; -import { startsWith, flatten, values, first, last } from 'lodash'; -import getDefaultDecoration from '../../helpers/get_default_decoration'; -import getSiblingAggValue from '../../helpers/get_sibling_agg_value'; -import getSplits from '../../helpers/get_splits'; -import mapBucket from '../../helpers/map_bucket'; -import mathjs from 'mathjs'; - -const limitedEval = mathjs.eval; -mathjs.import({ - 'import': function () { throw new Error('Function import is not allowed in your expression.'); }, - 'createUnit': function () { throw new Error('Function createUnit is not allowed in your expression.'); }, - 'eval': function () { throw new Error('Function eval is not allowed in your expression.'); }, - 'parse': function () { throw new Error('Function parse is not allowed in your expression.'); }, - 'simplify': function () { throw new Error('Function simplify is not allowed in your expression.'); }, - 'derivative': function () { throw new Error('Function derivative is not allowed in your expression.'); } -}, { override: true }); - -export function mathAgg(resp, panel, series) { - return next => results => { - const mathMetric = last(series.metrics); - if (mathMetric.type !== 'math') return next(results); - // Filter the results down to only the ones that match the series.id. Sometimes - // there will be data from other series mixed in. - results = results.filter(s => { - if (s.id.split(/:/)[0] === series.id) { - return false; - } - return true; - }); - const decoration = getDefaultDecoration(series); - const splits = getSplits(resp, panel, series); - const mathSeries = splits.map((split) => { - if (mathMetric.variables.length) { - // Gather the data for the splits. The data will either be a sibling agg or - // a standard metric/pipeline agg - const splitData = mathMetric.variables.reduce((acc, v) => { - const metric = series.metrics.find(m => startsWith(v.field, m.id)); - if (!metric) return acc; - if (/_bucket$/.test(metric.type)) { - acc[v.name] = split.timeseries.buckets.map(bucket => { - return [bucket.key, getSiblingAggValue(split, metric)]; - }); - } else { - const percentileMatch = v.field.match(percentileValueMatch); - const m = percentileMatch ? { ...metric, percent: percentileMatch[1] } : { ...metric }; - acc[v.name] = split.timeseries.buckets.map(mapBucket(m)); - } - return acc; - }, {}); - // Create an params._all so the users can access the entire series of data - // in the Math.js equation - const all = Object.keys(splitData).reduce((acc, key) => { - acc[key] = { - values: splitData[key].map(x => x[1]), - timestamps: splitData[key].map(x => x[0]) - }; - return acc; - }, {}); - // Get the first var and check that it shows up in the split data otherwise - // we need to return an empty array for the data since we can't opperate - // without the first varaible - const firstVar = first(mathMetric.variables); - if (!splitData[firstVar.name]) { - return { - id: split.id, - label: split.label, - color: split.color, - data: [], - ...decoration - }; - } - // Use the first var to collect all the timestamps - const timestamps = splitData[firstVar.name].map(r => first(r)); - // Map the timestamps to actual data - const data = timestamps.map((ts, index) => { - const params = mathMetric.variables.reduce((acc, v) => { - acc[v.name] = last(splitData[v.name].find(row => row[0] === ts)); - return acc; - }, {}); - // If some of the values are null, return the timestamp and null, this is - // a safety check for the user - const someNull = values(params).some(v => v == null); - if (someNull) return [ts, null]; - // calculate the result based on the user's script and return the value - const result = limitedEval(mathMetric.script, { - params: { - ...params, - _index: index, - _timestamp: ts, - _all: all, - _interval: split.meta.bucketSize * 1000 - } - }); - // if the result is an object (usually when the user is working with maps and functions) flatten the results and return the last value. - if (typeof result === 'object') return [ts, last(flatten(result.valueOf()))]; - return [ts, result]; - }); - return { - id: split.id, - label: split.label, - color: split.color, - data, - ...decoration - }; - } - }); - return next(results.concat(mathSeries)); - }; -} diff --git a/src/core_plugins/metrics/server/lib/vis_data/response_processors/table/index.js b/src/core_plugins/metrics/server/lib/vis_data/response_processors/table/index.js index 00f2621499934..822bc02cce7f4 100644 --- a/src/core_plugins/metrics/server/lib/vis_data/response_processors/table/index.js +++ b/src/core_plugins/metrics/server/lib/vis_data/response_processors/table/index.js @@ -2,14 +2,12 @@ import stdMetric from './std_metric'; import stdSibling from './std_sibling'; import seriesAgg from './series_agg'; -import { math } from './math'; import { dropLastBucketFn } from './drop_last_bucket'; export default [ // percentile, stdMetric, stdSibling, - math, seriesAgg, dropLastBucketFn ]; diff --git a/src/core_plugins/metrics/server/lib/vis_data/response_processors/table/math.js b/src/core_plugins/metrics/server/lib/vis_data/response_processors/table/math.js deleted file mode 100644 index 27da5c58c9a86..0000000000000 --- a/src/core_plugins/metrics/server/lib/vis_data/response_processors/table/math.js +++ /dev/null @@ -1,8 +0,0 @@ -import { mathAgg } from '../series/math'; - -export function math(bucket, panel, series) { - return next => results => { - const mathFn = mathAgg({ aggregations: bucket }, panel, series); - return mathFn(next)(results); - }; -} diff --git a/src/core_plugins/metrics/server/lib/vis_data/series/__tests__/build_request_body.js b/src/core_plugins/metrics/server/lib/vis_data/series/__tests__/build_request_body.js index 315d944e4b0a7..93cf4c4b65606 100644 --- a/src/core_plugins/metrics/server/lib/vis_data/series/__tests__/build_request_body.js +++ b/src/core_plugins/metrics/server/lib/vis_data/series/__tests__/build_request_body.js @@ -99,21 +99,16 @@ describe('buildRequestBody(req)', () => { filter: { match_all: {} }, - meta: { - timeField: '@timestamp', - bucketSize: 10, - intervalString: '10s' - }, - aggs: { - timeseries: { - date_histogram: { - field: '@timestamp', - interval: '10s', - min_doc_count: 0, - time_zone: 'UTC', - extended_bounds: { - min: 1485463055881, - max: 1485463955881 + 'aggs': { + 'timeseries': { + 'date_histogram': { + 'field': '@timestamp', + 'interval': '10s', + 'min_doc_count': 0, + 'time_zone': 'UTC', + 'extended_bounds': { + 'min': 1485463055881, + 'max': 1485463955881 } }, aggs: { diff --git a/src/core_plugins/region_map/public/region_map_visualization.js b/src/core_plugins/region_map/public/region_map_visualization.js index 3597e9a0ed723..8cf98df0047d2 100644 --- a/src/core_plugins/region_map/public/region_map_visualization.js +++ b/src/core_plugins/region_map/public/region_map_visualization.js @@ -1,6 +1,5 @@ import 'plugins/kbn_vislib_vis_types/controls/vislib_basic_options'; import _ from 'lodash'; -import AggConfigResult from 'ui/vis/agg_config_result'; import { BaseMapsVisualizationProvider } from '../../tile_map/public/base_maps_visualization'; import ChoroplethLayer from './choropleth_layer'; import { truncatedColorMaps } from 'ui/vislib/components/color/truncated_colormaps'; @@ -93,9 +92,9 @@ export function RegionMapsVisualizationProvider(Private, Notifier, config) { this._choroplethLayer.setMetrics(previousMetrics, previousMetricsAgg); } this._choroplethLayer.on('select', (event) => { - const aggs = this._vis.getAggConfig().getResponseAggs(); - const aggConfigResult = new AggConfigResult(aggs[0], false, event, event); - this._vis.API.events.filter({ point: { aggConfigResult: aggConfigResult } }); + const agg = this._vis.aggs.bySchemaName.segment[0]; + const filter = agg.createFilter(event); + this._vis.API.queryFilter.addFilters(filter); }); this._choroplethLayer.on('styleChanged', (event) => { const shouldShowWarning = this._vis.params.isDisplayWarning && config.get('visualization:regionmap:showWarnings'); diff --git a/src/core_plugins/tagcloud/public/tag_cloud_controller.js b/src/core_plugins/tagcloud/public/tag_cloud_controller.js index 28ea70da770f1..f3bdbf3934ce6 100644 --- a/src/core_plugins/tagcloud/public/tag_cloud_controller.js +++ b/src/core_plugins/tagcloud/public/tag_cloud_controller.js @@ -1,6 +1,5 @@ import { uiModules } from 'ui/modules'; import TagCloud from 'plugins/tagcloud/tag_cloud'; -import AggConfigResult from 'ui/vis/agg_config_result'; const module = uiModules.get('kibana/tagcloud', ['kibana']); module.controller('KbnTagCloudController', function ($scope, $element) { @@ -13,9 +12,8 @@ module.controller('KbnTagCloudController', function ($scope, $element) { const tagCloud = new TagCloud(containerNode); tagCloud.on('select', (event) => { if (!bucketAgg) return; - const aggConfigResult = new AggConfigResult(bucketAgg, false, event, event); - $scope.vis.API.events.filter({ point: { aggConfigResult: aggConfigResult } }); - $scope.$apply(); + const filter = bucketAgg.createFilter(event); + $scope.vis.API.queryFilter.addFilters(filter); }); tagCloud.on('renderComplete', () => { diff --git a/src/core_plugins/timelion/public/app.js b/src/core_plugins/timelion/public/app.js index 105f3f530afab..9d4d571fc24a6 100644 --- a/src/core_plugins/timelion/public/app.js +++ b/src/core_plugins/timelion/public/app.js @@ -117,9 +117,10 @@ app.controller('timelion', function ( const confirmModalOptions = { onConfirm: doDelete, - confirmButtonText: 'Delete sheet' + confirmButtonText: 'Delete', + title: `Delete Timelion sheet '${title}'?` }; - confirmModal(`Are you sure you want to delete the sheet ${title}?`, confirmModalOptions); + confirmModal(`You can't recover deleted sheets.`, confirmModalOptions); }, testId: 'timelionDeleteButton', }, { diff --git a/src/core_plugins/timelion/public/panels/timechart/schema.js b/src/core_plugins/timelion/public/panels/timechart/schema.js index 7025a34eb8614..e05d24968be2f 100644 --- a/src/core_plugins/timelion/public/panels/timechart/schema.js +++ b/src/core_plugins/timelion/public/panels/timechart/schema.js @@ -319,7 +319,7 @@ export default function timechartFn(Private, config, $rootScope, timefilter, $co $scope.plot = $.plot(canvasElem, _.compact(series), options); if ($scope.plot) { - $scope.$emit('renderComplete'); + $scope.$emit('timelionChartRendered'); } legendScope.$destroy(); diff --git a/src/core_plugins/timelion/public/vis/timelion_vis_controller.js b/src/core_plugins/timelion/public/vis/timelion_vis_controller.js index 3dfae847fcd07..9ae6b0850e120 100644 --- a/src/core_plugins/timelion/public/vis/timelion_vis_controller.js +++ b/src/core_plugins/timelion/public/vis/timelion_vis_controller.js @@ -8,7 +8,7 @@ import { uiModules } from 'ui/modules'; uiModules .get('kibana/timelion_vis', ['kibana']) .controller('TimelionVisController', function ($scope) { - $scope.$on('renderComplete', event => { + $scope.$on('timelionChartRendered', event => { event.stopPropagation(); $scope.renderComplete(); }); diff --git a/src/dev/ci_setup/setup.sh b/src/dev/ci_setup/setup.sh index 06abe7bde61b7..10278ad7a8fba 100755 --- a/src/dev/ci_setup/setup.sh +++ b/src/dev/ci_setup/setup.sh @@ -10,7 +10,7 @@ cacheDir="${CACHE_DIR:-"/tmp/kibana"}" ### check that we seem to be in a kibana project ### if [ -f "$dir/package.json" ] && [ -f "$dir/.node-version" ]; then - echo "Setting up node.js and npm in $dir" + echo "Setting up node.js and yarn in $dir" else echo "src/dev/ci_setup/setup.sh must be run within a kibana repo" exit 1 @@ -41,13 +41,47 @@ fi ### -### "install" node by extending the path with it's bin directory +### "install" node into this shell ### export PATH="$nodeDir/bin:$PATH" +hash -r + +### +### downloading yarn +### +yarnVersion="$(node -e "console.log(String(require('./package.json').engines.yarn || '').replace(/^[^\d]+/,''))")" +yarnUrl="https://github.com/yarnpkg/yarn/releases/download/v$yarnVersion/yarn-$yarnVersion.js" +yarnDir="$cacheDir/yarn/$yarnVersion" +if [ -z "$yarnVersion" ]; then + echo " !! missing engines.yarn in package.json"; + exit 1 +elif [ -x "$yarnDir/bin/yarn" ] && [ "$($yarnDir/bin/yarn --version)" == "$yarnVersion" ]; then + echo " -- reusing yarn install" +else + if [ -d "$yarnDir" ]; then + echo " -- clearing previous yarn install" + rm -rf "$yarnDir" + fi + + echo " -- downloading yarn from $yarnUrl" + mkdir -p "$yarnDir/bin" + curl -L --silent "$yarnUrl" > "$yarnDir/bin/yarn" + chmod +x "$yarnDir/bin/yarn" +fi + + +### +### "install" yarn into this shell +### +export PATH="$yarnDir/bin:$PATH" +yarnGlobalDir="$(yarn global bin)" +export PATH="$PATH:$yarnGlobalDir" +hash -r ### ### install dependencies ### echo " -- installing node.js dependencies" -npm install --cache "$cacheDir/npm" +yarn config set cache-folder "$cacheDir/yarn" +yarn --frozen-lockfile diff --git a/src/functional_test_runner/__tests__/fixtures/with_es_archiver/archives/test_archive/mappings.json b/src/functional_test_runner/__tests__/fixtures/with_es_archiver/archives/test_archive/mappings.json index c62aea1be4895..bb2cdefc9e2dc 100644 --- a/src/functional_test_runner/__tests__/fixtures/with_es_archiver/archives/test_archive/mappings.json +++ b/src/functional_test_runner/__tests__/fixtures/with_es_archiver/archives/test_archive/mappings.json @@ -5,9 +5,6 @@ "settings": { "index": { "number_of_shards": "1", - "mapper": { - "dynamic": "false" - }, "number_of_replicas": "1" } }, diff --git a/src/functional_test_runner/__tests__/integration/with_es_archiver.js b/src/functional_test_runner/__tests__/integration/with_es_archiver.js index f25fcd0a145ef..cd0b4574dedde 100644 --- a/src/functional_test_runner/__tests__/integration/with_es_archiver.js +++ b/src/functional_test_runner/__tests__/integration/with_es_archiver.js @@ -2,6 +2,8 @@ import { spawn } from 'child_process'; import { resolve } from 'path'; import { format as formatUrl } from 'url'; +import expect from 'expect.js'; + import { readConfigFile } from '../../lib'; import { createToolingLog } from '../../../dev'; import { createReduceStream } from '../../../utils'; @@ -48,7 +50,8 @@ describe('single test that uses esArchiver', () => { cleanupWork.unshift(() => kibana.close()); }); - it('test', async () => { + it('test', async function () { + this.timeout(10000); const proc = spawn(process.execPath, [SCRIPT, '--config', CONFIG], { stdio: ['ignore', 'pipe', 'ignore'] }); @@ -56,7 +59,7 @@ describe('single test that uses esArchiver', () => { const concatChunks = (acc, chunk) => `${acc}${chunk}`; const concatStdout = proc.stdout.pipe(createReduceStream(concatChunks)); - const [stdout] = await Promise.all([ + const [stdout, exitCode] = await Promise.all([ new Promise((resolve, reject) => { concatStdout.on('error', reject); concatStdout.on('data', resolve); // reduce streams produce a single value, no need to wait for anything else @@ -69,6 +72,7 @@ describe('single test that uses esArchiver', () => { ]); log.debug(stdout.toString('utf8')); + expect(exitCode).to.be(0); }); after(async () => { diff --git a/src/optimize/base_optimizer.js b/src/optimize/base_optimizer.js index 27d2a17ec79eb..5b4d149372511 100644 --- a/src/optimize/base_optimizer.js +++ b/src/optimize/base_optimizer.js @@ -3,6 +3,10 @@ import { writeFile } from 'fs'; import Boom from 'boom'; import ExtractTextPlugin from 'extract-text-webpack-plugin'; import webpack from 'webpack'; +import CommonsChunkPlugin from 'webpack/lib/optimize/CommonsChunkPlugin'; +import DefinePlugin from 'webpack/lib/DefinePlugin'; +import UglifyJsPlugin from 'webpack/lib/optimize/UglifyJsPlugin'; +import NoEmitOnErrorsPlugin from 'webpack/lib/NoEmitOnErrorsPlugin'; import Stats from 'webpack/lib/Stats'; import webpackMerge from 'webpack-merge'; @@ -94,8 +98,6 @@ export default class BaseOptimizer { }); } - const nodeModulesPath = fromRoot('node_modules'); - /** * Adds a cache loader if we're running in dev mode. The reason we're not adding * the cache-loader when running in production mode is that it creates cache @@ -139,20 +141,12 @@ export default class BaseOptimizer { allChunks: true }), - new webpack.optimize.CommonsChunkPlugin({ + new CommonsChunkPlugin({ name: 'commons', - filename: 'commons.bundle.js', - minChunks: 2, - }), - - new webpack.optimize.CommonsChunkPlugin({ - name: 'vendors', - filename: 'vendors.bundle.js', - // only combine node_modules from Kibana - minChunks: module => module.context && module.context.indexOf(nodeModulesPath) !== -1 + filename: 'commons.bundle.js' }), - new webpack.NoEmitOnErrorsPlugin(), + new NoEmitOnErrorsPlugin(), ], module: { @@ -239,12 +233,12 @@ export default class BaseOptimizer { return webpackMerge(commonConfig, { plugins: [ - new webpack.DefinePlugin({ + new DefinePlugin({ 'process.env': { 'NODE_ENV': '"production"' } }), - new webpack.optimize.UglifyJsPlugin({ + new UglifyJsPlugin({ compress: { warnings: false }, diff --git a/src/optimize/bundles_route/dynamic_asset_response.js b/src/optimize/bundles_route/dynamic_asset_response.js index 53fd9a77307b6..6212f340c15c7 100644 --- a/src/optimize/bundles_route/dynamic_asset_response.js +++ b/src/optimize/bundles_route/dynamic_asset_response.js @@ -67,7 +67,7 @@ export async function createDynamicAssetResponse(options) { const response = request.generateResponse(replacePlaceholder(read, publicPath)); response.code(200); response.etag(`${hash}-${publicPath}`); - response.header('last-modified', stat.mtime.toUTCString()); + response.header('cache-control', 'must-revalidate'); response.type(request.server.mime.path(path).type); return response; diff --git a/src/optimize/index.js b/src/optimize/index.js index dde11b0ef3060..b8153cf157553 100644 --- a/src/optimize/index.js +++ b/src/optimize/index.js @@ -4,7 +4,7 @@ import { createBundlesRoute } from './bundles_route'; export default async (kbnServer, server, config) => { if (!config.get('optimize.enabled')) return; - // the lazy optimizer sets up two threads, one is the server listening + // the watch optimizer sets up two threads, one is the server listening // on 5601 and the other is a server listening on 5602 that builds the // bundles in a "middleware" style. // @@ -12,9 +12,9 @@ export default async (kbnServer, server, config) => { // on the watch setup managed by the cli. It proxies all bundles/* requests to // the other server. The server on 5602 is long running, in order to prevent // complete rebuilds of the optimize content. - const lazy = config.get('optimize.lazy'); - if (lazy) { - return await kbnServer.mixin(require('./lazy/lazy')); + const watch = config.get('optimize.watch'); + if (watch) { + return await kbnServer.mixin(require('./watch/watch')); } const { uiBundles } = kbnServer; diff --git a/src/optimize/lazy/lazy_optimizer.js b/src/optimize/lazy/lazy_optimizer.js deleted file mode 100644 index 82d4b88fb1436..0000000000000 --- a/src/optimize/lazy/lazy_optimizer.js +++ /dev/null @@ -1,119 +0,0 @@ -import BaseOptimizer from '../base_optimizer'; -import WeirdControlFlow from './weird_control_flow'; -import { once } from 'lodash'; -import { join } from 'path'; - -import { createBundlesRoute } from '../bundles_route'; - -export default class LazyOptimizer extends BaseOptimizer { - constructor(opts) { - super(opts); - this.log = opts.log || (() => null); - this.prebuild = opts.prebuild || false; - - this.timer = { - ms: null, - start: () => this.timer.ms = Date.now(), - end: () => this.timer.ms = ((Date.now() - this.timer.ms) / 1000).toFixed(2) - }; - - this.build = new WeirdControlFlow(); - } - - async init() { - this.initializing = true; - - await this.uiBundles.writeEntryFiles(); - await this.initCompiler(); - - this.compiler.plugin('watch-run', (w, webpackCb) => { - this.build.work(once(() => { - this.timer.start(); - this.logRunStart(); - webpackCb(); - })); - }); - - this.compiler.plugin('done', stats => { - if (!stats.hasErrors() && !stats.hasWarnings()) { - this.logRunSuccess(); - this.build.success(); - return; - } - - const err = this.failedStatsToError(stats); - this.logRunFailure(err); - this.build.failure(err); - this.watching.invalidate(); - }); - - this.watching = this.compiler.watch({ aggregateTimeout: 200 }, err => { - if (err) { - this.log('fatal', err); - process.exit(1); - } - }); - - const buildPromise = this.build.get(); - if (this.prebuild) await buildPromise; - - this.initializing = false; - this.log(['info', 'optimize'], { - tmpl: `Lazy optimization of ${this.uiBundles.getDescription()} ready`, - bundles: this.uiBundles.getIds() - }); - } - - async getPath(relativePath) { - await this.build.get(); - return join(this.compiler.outputPath, relativePath); - } - - bindToServer(server, basePath) { - - // calling `build.get()` resolves when the build is - // "stable" (the compiler is not running) so this pauses - // all requests received while the compiler is running - // and lets the continue once it is done. - server.ext('onRequest', (request, reply) => { - this.build.get() - .then(() => reply.continue()) - .catch(reply); - }); - - server.route(createBundlesRoute({ - bundlesPath: this.compiler.outputPath, - basePublicPath: basePath - })); - } - - logRunStart() { - this.log(['info', 'optimize'], { - tmpl: `Lazy optimization started`, - bundles: this.uiBundles.getIds() - }); - } - - logRunSuccess() { - this.log(['info', 'optimize'], { - tmpl: 'Lazy optimization <%= status %> in <%= seconds %> seconds', - bundles: this.uiBundles.getIds(), - status: 'success', - seconds: this.timer.end() - }); - } - - logRunFailure(err) { - // errors during initialization to the server, unlike the rest of the - // errors produced here. Lets not muddy the console with extra errors - if (this.initializing) return; - - this.log(['fatal', 'optimize'], { - tmpl: 'Lazy optimization <%= status %> in <%= seconds %> seconds<%= err %>', - bundles: this.uiBundles.getIds(), - status: 'failed', - seconds: this.timer.end(), - err: err - }); - } -} diff --git a/src/optimize/lazy/weird_control_flow.js b/src/optimize/lazy/weird_control_flow.js deleted file mode 100644 index f5d3d31bde2e8..0000000000000 --- a/src/optimize/lazy/weird_control_flow.js +++ /dev/null @@ -1,58 +0,0 @@ -import { fromNode } from 'bluebird'; - - -export default class WeirdControlFlow { - constructor() { - this.handlers = []; - } - - get() { - return fromNode(cb => { - if (this.ready) return cb(); - this.handlers.push(cb); - this.start(); - }); - } - - work(work) { - this._work = work; - this.stop(); - - if (this.handlers.length) { - this.start(); - } - } - - start() { - if (this.running) return; - this.stop(); - if (this._work) { - this.running = true; - this._work(); - } - } - - stop() { - this.ready = false; - this.error = false; - this.running = false; - } - - success(...args) { - this.stop(); - this.ready = true; - this._flush(args); - } - - failure(err) { - this.stop(); - this.error = err; - this._flush([err]); - } - - _flush(args) { - for (const fn of this.handlers.splice(0)) { - fn.apply(null, args); - } - } -} diff --git a/src/optimize/lazy/optmzr_role.js b/src/optimize/watch/optmzr_role.js similarity index 71% rename from src/optimize/lazy/optmzr_role.js rename to src/optimize/watch/optmzr_role.js index ba50258078e76..9c10fc0304cab 100644 --- a/src/optimize/lazy/optmzr_role.js +++ b/src/optimize/watch/optmzr_role.js @@ -1,17 +1,17 @@ -import LazyServer from './lazy_server'; -import LazyOptimizer from './lazy_optimizer'; +import WatchServer from './watch_server'; +import WatchOptimizer from './watch_optimizer'; export default async (kbnServer, kibanaHapiServer, config) => { - const server = new LazyServer( - config.get('optimize.lazyHost'), - config.get('optimize.lazyPort'), + const server = new WatchServer( + config.get('optimize.watchHost'), + config.get('optimize.watchPort'), config.get('server.basePath'), - new LazyOptimizer({ + new WatchOptimizer({ log: (tags, data) => kibanaHapiServer.log(tags, data), uiBundles: kbnServer.uiBundles, profile: config.get('optimize.profile'), sourceMaps: config.get('optimize.sourceMaps'), - prebuild: config.get('optimize.lazyPrebuild'), + prebuild: config.get('optimize.watchPrebuild'), unsafeCache: config.get('optimize.unsafeCache'), }) ); diff --git a/src/optimize/lazy/proxy_role.js b/src/optimize/watch/proxy_role.js similarity index 72% rename from src/optimize/lazy/proxy_role.js rename to src/optimize/watch/proxy_role.js index 32d0a7eaeaef1..873f67e683c36 100644 --- a/src/optimize/lazy/proxy_role.js +++ b/src/optimize/watch/proxy_role.js @@ -8,8 +8,8 @@ export default (kbnServer, server, config) => { method: 'GET', handler: { proxy: { - host: config.get('optimize.lazyHost'), - port: config.get('optimize.lazyPort'), + host: config.get('optimize.watchHost'), + port: config.get('optimize.watchPort'), passThrough: true, xforward: true } @@ -19,11 +19,11 @@ export default (kbnServer, server, config) => { return fromNode(cb => { const timeout = setTimeout(() => { - cb(new Error('Server timedout waiting for the optimizer to become ready')); - }, config.get('optimize.lazyProxyTimeout')); + cb(new Error('Timeout waiting for the optimizer to become ready')); + }, config.get('optimize.watchProxyTimeout')); const waiting = once(() => { - server.log(['info', 'optimize'], 'Waiting for optimizer completion'); + server.log(['info', 'optimize'], 'Waiting for optimizer to be ready'); }); if (!process.connected) return; diff --git a/src/optimize/lazy/lazy.js b/src/optimize/watch/watch.js similarity index 54% rename from src/optimize/lazy/lazy.js rename to src/optimize/watch/watch.js index 46d27084f20b4..2be31bc64ad5e 100644 --- a/src/optimize/lazy/lazy.js +++ b/src/optimize/watch/watch.js @@ -2,21 +2,19 @@ import { isWorker } from 'cluster'; export default async kbnServer => { - if (!isWorker) { - throw new Error(`lazy optimization is only available in "watch" mode`); + throw new Error(`watch optimization is only available when using the "--dev" cli flag`); } /** - * When running in lazy mode two workers/threads run in one - * of the modes: 'optmzr' or 'server' + * When running in watch mode two processes run in one of the following modes: * - * optmzr: this thread runs the LiveOptimizer and the LazyServer - * which serves the LiveOptimizer's output and blocks requests + * optmzr: this process runs the WatchOptimizer and the WatchServer + * which serves the WatchOptimizer's output and blocks requests * while the optimizer is running * - * server: this thread runs the entire kibana server and proxies - * all requests for /bundles/* to the optmzr + * server: this process runs the entire kibana server and proxies + * all requests for /bundles/* to the optmzr process * * @param {string} process.env.kbnWorkerType */ diff --git a/src/optimize/watch/watch_optimizer.js b/src/optimize/watch/watch_optimizer.js new file mode 100644 index 0000000000000..9d4801a02f3c4 --- /dev/null +++ b/src/optimize/watch/watch_optimizer.js @@ -0,0 +1,155 @@ +import { Observable, ReplaySubject } from 'rxjs'; + +import BaseOptimizer from '../base_optimizer'; + +import { createBundlesRoute } from '../bundles_route'; + +const STATUS = { + RUNNING: 'optimizer running', + SUCCESS: 'optimizer completed successfully', + FAILURE: 'optimizer failed with stats', + FATAL: 'optimizer failed without stats', +}; + +export default class WatchOptimizer extends BaseOptimizer { + constructor(opts) { + super(opts); + this.log = opts.log || (() => null); + this.prebuild = opts.prebuild || false; + this.status$ = new ReplaySubject(1); + } + + async init() { + this.initializing = true; + this.initialBuildComplete = false; + + // log status changes + this.status$.subscribe(this.onStatusChangeHandler); + + await this.uiBundles.writeEntryFiles(); + await this.initCompiler(); + + this.compiler.plugin('watch-run', this.compilerRunStartHandler); + this.compiler.plugin('done', this.compilerDoneHandler); + this.compiler.watch({ aggregateTimeout: 200 }, this.compilerWatchErrorHandler); + + if (this.prebuild) { + await this.onceBuildOutcome(); + } + + this.initializing = false; + } + + bindToServer(server, basePath) { + // pause all requests received while the compiler is running + // and continue once an outcome is reached (aborting the request + // with an error if it was a failure). + server.ext('onRequest', (request, reply) => { + this.onceBuildOutcome() + .then(() => reply.continue()) + .catch(reply); + }); + + server.route(createBundlesRoute({ + bundlesPath: this.compiler.outputPath, + basePublicPath: basePath + })); + } + + async onceBuildOutcome() { + return await this.status$ + .mergeMap(this.mapStatusToOutcomes) + .take(1) + .toPromise(); + } + + mapStatusToOutcomes({ type, error }) { + switch (type) { + case STATUS.RUNNING: + return []; + + case STATUS.SUCCESS: + return [true]; + + case STATUS.FAILURE: + case STATUS.FATAL: + return Observable.throw(error); + } + } + + compilerRunStartHandler = (watchingCompiler, cb) => { + this.status$.next({ + type: STATUS.RUNNING + }); + + cb(); + } + + compilerWatchErrorHandler = (error) => { + if (error) { + this.status$.next({ + type: STATUS.FATAL, + error + }); + } + } + + compilerDoneHandler = (stats) => { + this.initialBuildComplete = true; + const seconds = parseFloat((stats.endTime - stats.startTime) / 1000).toFixed(2); + + if (stats.hasErrors() || stats.hasWarnings()) { + this.status$.next({ + type: STATUS.FAILURE, + seconds, + error: this.failedStatsToError(stats) + }); + } else { + this.status$.next({ + type: STATUS.SUCCESS, + seconds, + }); + } + } + + onStatusChangeHandler = ({ type, seconds, error }) => { + switch (type) { + case STATUS.RUNNING: + if (!this.initialBuildComplete) { + this.log(['info', 'optimize'], { + tmpl: 'Optimization started', + bundles: this.uiBundles.getIds() + }); + } + break; + + case STATUS.SUCCESS: + this.log(['info', 'optimize'], { + tmpl: 'Optimization <%= status %> in <%= seconds %> seconds', + bundles: this.uiBundles.getIds(), + status: 'success', + seconds + }); + break; + + case STATUS.FAILURE: + // errors during initialization to the server, unlike the rest of the + // errors produced here. Lets not muddy the console with extra errors + if (!this.initializing) { + this.log(['fatal', 'optimize'], { + tmpl: 'Optimization <%= status %> in <%= seconds %> seconds<%= err %>', + bundles: this.uiBundles.getIds(), + status: 'failed', + seconds, + err: error + }); + } + break; + + case STATUS.FATAL: + this.log('fatal', error); + process.exit(1); + break; + } + } +} diff --git a/src/optimize/lazy/lazy_server.js b/src/optimize/watch/watch_server.js similarity index 94% rename from src/optimize/lazy/lazy_server.js rename to src/optimize/watch/watch_server.js index 752cd3a36aa5f..d963fcf7b4dc8 100644 --- a/src/optimize/lazy/lazy_server.js +++ b/src/optimize/watch/watch_server.js @@ -3,7 +3,7 @@ import { Server } from 'hapi'; import { fromNode } from 'bluebird'; import registerHapiPlugins from '../../server/http/register_hapi_plugins'; -export default class LazyServer { +export default class WatchServer { constructor(host, port, basePath, optimizer) { this.basePath = basePath; this.optimizer = optimizer; diff --git a/src/server/config/schema.js b/src/server/config/schema.js index 0c3d020e964f8..a94d157e1ac9c 100644 --- a/src/server/config/schema.js +++ b/src/server/config/schema.js @@ -133,11 +133,11 @@ export default () => Joi.object({ bundleFilter: Joi.string().default('!tests'), bundleDir: Joi.string().default(fromRoot('optimize/bundles')), viewCaching: Joi.boolean().default(Joi.ref('$prod')), - lazy: Joi.boolean().default(false), - lazyPort: Joi.number().default(5602), - lazyHost: Joi.string().hostname().default('localhost'), - lazyPrebuild: Joi.boolean().default(false), - lazyProxyTimeout: Joi.number().default(5 * 60000), + watch: Joi.boolean().default(false), + watchPort: Joi.number().default(5602), + watchHost: Joi.string().hostname().default('localhost'), + watchPrebuild: Joi.boolean().default(false), + watchProxyTimeout: Joi.number().default(5 * 60000), useBundleCache: Joi.boolean().default(Joi.ref('$prod')), unsafeCache: Joi.when('$prod', { is: true, diff --git a/src/server/config/transform_deprecations.js b/src/server/config/transform_deprecations.js index 7548db30e7c97..fb74522597f93 100644 --- a/src/server/config/transform_deprecations.js +++ b/src/server/config/transform_deprecations.js @@ -30,6 +30,11 @@ const deprecations = [ rename('server.ssl.cert', 'server.ssl.certificate'), unused('server.xsrf.token'), unused('uiSettings.enabled'), + rename('optimize.lazy', 'optimize.watch'), + rename('optimize.lazyPort', 'optimize.watchPort'), + rename('optimize.lazyHost', 'optimize.watchHost'), + rename('optimize.lazyPrebuild', 'optimize.watchPrebuild'), + rename('optimize.lazyProxyTimeout', 'optimize.watchProxyTimeout'), serverSslEnabled, savedObjectsIndexCheckTimeout, ]; diff --git a/src/server/kbn_server.js b/src/server/kbn_server.js index faa6aa35c0e89..e93666035f554 100644 --- a/src/server/kbn_server.js +++ b/src/server/kbn_server.js @@ -62,7 +62,7 @@ export default class KbnServer { savedObjectsMixin, // ensure that all bundles are built, or that the - // lazy bundle server is running + // watch bundle server is running optimizeMixin, // initialize the plugins diff --git a/src/ui/public/agg_response/hierarchical/_transform_aggregation.js b/src/ui/public/agg_response/hierarchical/_transform_aggregation.js index 3e6001360e43e..9450385558a27 100644 --- a/src/ui/public/agg_response/hierarchical/_transform_aggregation.js +++ b/src/ui/public/agg_response/hierarchical/_transform_aggregation.js @@ -9,7 +9,8 @@ export function HierarchicalTransformAggregationProvider() { agg, parent && parent.aggConfigResult, metric.getValue(bucket), - agg.getKey(bucket) + agg.getKey(bucket), + bucket.filters ); const branch = { 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 c72d7b19dd58f..a05e6125a419e 100644 --- a/src/ui/public/agg_response/hierarchical/build_hierarchical_data.js +++ b/src/ui/public/agg_response/hierarchical/build_hierarchical_data.js @@ -77,7 +77,7 @@ export function BuildHierarchicalDataProvider(Private, Notifier) { if (!_.isEmpty(displayName)) split.label += ': ' + displayName; split.tooltipFormatter = tooltipFormatter(raw.columns); - const aggConfigResult = new AggConfigResult(firstAgg, null, null, firstAgg.getKey(bucket)); + const aggConfigResult = new AggConfigResult(firstAgg, null, null, firstAgg.getKey(bucket), bucket.filters); split.split = { aggConfig: firstAgg, aggConfigResult: aggConfigResult, key: bucket.key }; _.each(split.slices.children, function (child) { child.aggConfigResult.$parent = aggConfigResult; diff --git a/src/ui/public/agg_response/tabify/_response_writer.js b/src/ui/public/agg_response/tabify/_response_writer.js index d068f2e112ec7..211c9a6d40090 100644 --- a/src/ui/public/agg_response/tabify/_response_writer.js +++ b/src/ui/public/agg_response/tabify/_response_writer.js @@ -194,7 +194,7 @@ export function TabbedAggResponseWriterProvider(Private) { newList.unshift(injected); } - const newAcr = new AggConfigResult(acr.aggConfig, newList[0], acr.value, acr.aggConfig.getKey(acr)); + const newAcr = new AggConfigResult(acr.aggConfig, newList[0], acr.value, acr.aggConfig.getKey(acr), acr.filters); newList.unshift(newAcr); // and replace the acr in the row buffer if its there @@ -215,9 +215,9 @@ export function TabbedAggResponseWriterProvider(Private) { * @param {function} block - the function to run while this value is in the row * @return {any} - the value that was added */ - TabbedAggResponseWriter.prototype.cell = function (agg, value, block) { + TabbedAggResponseWriter.prototype.cell = function (agg, value, block, filters) { if (this.asAggConfigResults) { - value = new AggConfigResult(agg, this.acrStack[0], value, value); + value = new AggConfigResult(agg, this.acrStack[0], value, value, filters); } const staskResult = this.asAggConfigResults && value.type === 'bucket'; diff --git a/src/ui/public/agg_response/tabify/tabify.js b/src/ui/public/agg_response/tabify/tabify.js index f5a6e8b562b3d..a2e2ed4d82b22 100644 --- a/src/ui/public/agg_response/tabify/tabify.js +++ b/src/ui/public/agg_response/tabify/tabify.js @@ -46,7 +46,7 @@ export function AggResponseTabifyProvider(Private, Notifier) { buckets.forEach(function (subBucket, key) { write.cell(agg, agg.getKey(subBucket, key), function () { collectBucket(write, subBucket, agg.getKey(subBucket, key), aggScale); - }); + }, subBucket.filters); }); } } else if (write.partialRows && write.metricsForAllBuckets && write.minimalColumns) { diff --git a/src/ui/public/agg_types/__tests__/buckets/_terms_other_bucket_helper.js b/src/ui/public/agg_types/__tests__/buckets/_terms_other_bucket_helper.js new file mode 100644 index 0000000000000..fc0fd344027af --- /dev/null +++ b/src/ui/public/agg_types/__tests__/buckets/_terms_other_bucket_helper.js @@ -0,0 +1,260 @@ +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import { OtherBucketHelperProvider } from 'ui/agg_types/buckets/_terms_other_bucket_helper'; +import { VisProvider } from 'ui/vis'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; + +const visConfigSingleTerm = { + type: 'pie', + aggs: [ + { + type: 'terms', + schema: 'segment', + params: { field: 'machine.os.raw', otherBucket: true, missingBucket: true } + } + ] +}; + +const visConfigNestedTerm = { + type: 'pie', + aggs: [ + { + type: 'terms', + schema: 'segment', + params: { field: 'geo.src', size: 2, otherBucket: false, missingBucket: false } + }, { + type: 'terms', + schema: 'segment', + params: { field: 'machine.os.raw', size: 2, otherBucket: true, missingBucket: true } + } + ] +}; + +const singleTermResponse = { + 'took': 10, + 'timed_out': false, + '_shards': { + 'total': 1, 'successful': 1, 'skipped': 0, 'failed': 0 + }, 'hits': { + 'total': 14005, 'max_score': 0, 'hits': [] + }, 'aggregations': { + '1': { + 'doc_count_error_upper_bound': 0, + 'sum_other_doc_count': 8325, + 'buckets': [ + { 'key': 'ios', 'doc_count': 2850 }, + { 'key': 'win xp', 'doc_count': 2830 }, + { 'key': '__missing__', 'doc_count': 1430 } + ] + } + }, 'status': 200 +}; + +const nestedTermResponse = { + 'took': 10, + 'timed_out': false, + '_shards': { + 'total': 1, 'successful': 1, 'skipped': 0, 'failed': 0 + }, 'hits': { + 'total': 14005, 'max_score': 0, 'hits': [] + }, 'aggregations': { + '1': { + 'doc_count_error_upper_bound': 0, + 'sum_other_doc_count': 8325, + 'buckets': [ + { + '2': { + 'doc_count_error_upper_bound': 0, + 'sum_other_doc_count': 8325, + 'buckets': [ + { 'key': 'ios', 'doc_count': 2850 }, + { 'key': 'win xp', 'doc_count': 2830 }, + { 'key': '__missing__', 'doc_count': 1430 } + ] + }, + key: 'US', + doc_count: 2850 + }, { + '2': { + 'doc_count_error_upper_bound': 0, + 'sum_other_doc_count': 8325, + 'buckets': [ + { 'key': 'ios', 'doc_count': 1850 }, + { 'key': 'win xp', 'doc_count': 1830 }, + { 'key': '__missing__', 'doc_count': 130 } + ] + }, + key: 'IN', + doc_count: 2830 + } + ] + } + }, 'status': 200 +}; + +const singleOtherResponse = { + 'took': 3, + 'timed_out': false, + '_shards': { 'total': 1, 'successful': 1, 'skipped': 0, 'failed': 0 }, + 'hits': { 'total': 14005, 'max_score': 0, 'hits': [] }, + 'aggregations': { + 'other-filter': { + 'buckets': { '': { 'doc_count': 2805 } } + } + }, 'status': 200 +}; + +const nestedOtherResponse = { + 'took': 3, + 'timed_out': false, + '_shards': { 'total': 1, 'successful': 1, 'skipped': 0, 'failed': 0 }, + 'hits': { 'total': 14005, 'max_score': 0, 'hits': [] }, + 'aggregations': { + 'other-filter': { + 'buckets': { '-US': { 'doc_count': 2805 }, '-IN': { 'doc_count': 2804 } } + } + }, 'status': 200 +}; + +describe('Terms Agg Other bucket helper', () => { + + let otherBucketHelper; + let vis; + + function init(aggConfig) { + ngMock.module('kibana'); + ngMock.inject((Private) => { + const Vis = Private(VisProvider); + const indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); + otherBucketHelper = Private(OtherBucketHelperProvider); + + vis = new Vis(indexPattern, aggConfig); + }); + } + + describe('buildOtherBucketAgg', () => { + + it('returns a function', () => { + init(visConfigSingleTerm); + const agg = otherBucketHelper.buildOtherBucketAgg(vis.aggs, vis.aggs[0], singleTermResponse); + expect(agg).to.be.a('function'); + }); + + it('correctly builds query with single terms agg', () => { + init(visConfigSingleTerm); + const agg = otherBucketHelper.buildOtherBucketAgg(vis.aggs, vis.aggs[0], singleTermResponse)(); + const expectedResponse = { + aggs: undefined, + filters: { + filters: { + '': { + 'bool': { + 'must': [{ + 'exists': { + 'field': 'machine.os.raw', + } + }], + 'filter': [], + 'should': [], + 'must_not': [ + { 'match_phrase': { 'machine.os.raw': { 'query': 'ios' } } }, + { 'match_phrase': { 'machine.os.raw': { 'query': 'win xp' } } } + ] + } + } + } + } + }; + + expect(agg['other-filter']).to.eql(expectedResponse); + }); + + it('correctly builds query for nested terms agg', () => { + init(visConfigNestedTerm); + const agg = otherBucketHelper.buildOtherBucketAgg(vis.aggs, vis.aggs[1], nestedTermResponse)(); + const expectedResponse = { + 'other-filter': { + aggs: undefined, + 'filters': { + 'filters': { + '-IN': { + 'bool': { + 'must': [ + { match_phrase: { 'geo.src': { 'query': 'IN' } } }, + { + 'exists': { + 'field': 'machine.os.raw', + } + } + ], 'filter': [], + 'should': [], + 'must_not': [ + { 'match_phrase': { 'machine.os.raw': { 'query': 'ios' } } }, + { 'match_phrase': { 'machine.os.raw': { 'query': 'win xp' } } } + ] + } + }, '-US': { + 'bool': { + 'must': [ + { 'match_phrase': { 'geo.src': { 'query': 'US' } } }, + { + 'exists': { + 'field': 'machine.os.raw', + } + } + ], 'filter': [], 'should': [], 'must_not': [ + { 'match_phrase': { 'machine.os.raw': { 'query': 'ios' } } }, + { 'match_phrase': { 'machine.os.raw': { 'query': 'win xp' } } } + ] + } + } + } + } + } + }; + + expect(agg).to.eql(expectedResponse); + }); + }); + + describe('mergeOtherBucketAggResponse', () => { + it('correctly merges other bucket with single terms agg', () => { + init(visConfigSingleTerm); + const otherAggConfig = otherBucketHelper.buildOtherBucketAgg(vis.aggs, vis.aggs[0], singleTermResponse)(); + const mergedResponse = otherBucketHelper + .mergeOtherBucketAggResponse(vis.aggs, singleTermResponse, singleOtherResponse, vis.aggs[0], otherAggConfig); + + expect(mergedResponse.aggregations['1'].buckets[3].key).to.equal('Other'); + expect(mergedResponse.aggregations['1'].buckets[3].filters.length).to.equal(2); + }); + + it('correctly merges other bucket with nested terms agg', () => { + init(visConfigNestedTerm); + const otherAggConfig = otherBucketHelper.buildOtherBucketAgg(vis.aggs, vis.aggs[1], nestedTermResponse)(); + const mergedResponse = otherBucketHelper + .mergeOtherBucketAggResponse(vis.aggs, nestedTermResponse, nestedOtherResponse, vis.aggs[1], otherAggConfig); + + expect(mergedResponse.aggregations['1'].buckets[1]['2'].buckets[3].key).to.equal('Other'); + expect(mergedResponse.aggregations['1'].buckets[1]['2'].buckets[3].filters.length).to.equal(2); + }); + + }); + + describe('updateMissingBucket', () => { + it('correctly updates missing bucket key', () => { + init(visConfigNestedTerm); + const updatedResponse = otherBucketHelper.updateMissingBucket(singleTermResponse, vis.aggs, vis.aggs[0]); + expect(updatedResponse.aggregations['1'].buckets.find(bucket => bucket.key === 'Missing')).to.not.be('undefined'); + }); + + it('correctly sets the bucket filter', () => { + const updatedResponse = otherBucketHelper.updateMissingBucket(singleTermResponse, vis.aggs, vis.aggs[0]); + const missingBucket = updatedResponse.aggregations['1'].buckets.find(bucket => bucket.key === 'Missing'); + expect(missingBucket.filters).to.not.be('undefined'); + expect(missingBucket.filters[0]).to.eql({ + meta: { index: 'logstash-*', negate: true }, + exists: { field: 'geo.src' } + }); + }); + }); +}); diff --git a/src/ui/public/agg_types/__tests__/buckets/create_filter/terms.js b/src/ui/public/agg_types/__tests__/buckets/create_filter/terms.js index ffc7b0e26ae89..1998dc0e16f90 100644 --- a/src/ui/public/agg_types/__tests__/buckets/create_filter/terms.js +++ b/src/ui/public/agg_types/__tests__/buckets/create_filter/terms.js @@ -34,5 +34,24 @@ describe('AggConfig Filters', function () { expect(filter.meta).to.have.property('index', indexPattern.id); }); + + it('should set query to true or false for boolean filter', () => { + const vis = new Vis(indexPattern, { + type: 'histogram', + aggs: [ { type: 'terms', schema: 'segment', params: { field: 'ssl' } } ] + }); + const aggConfig = vis.aggs.byTypeName.terms[0]; + const filterFalse = createFilter(aggConfig, 0); + expect(filterFalse).to.have.property('query'); + expect(filterFalse.query).to.have.property('match'); + expect(filterFalse.query.match).to.have.property('ssl'); + expect(filterFalse.query.match.ssl).to.have.property('query', false); + + const filterTrue = createFilter(aggConfig, 1); + expect(filterTrue).to.have.property('query'); + expect(filterTrue.query).to.have.property('match'); + expect(filterTrue.query.match).to.have.property('ssl'); + expect(filterTrue.query.match.ssl).to.have.property('query', true); + }); }); }); diff --git a/src/ui/public/agg_types/__tests__/index.js b/src/ui/public/agg_types/__tests__/index.js index c281867d9a3dd..87fd5985b382e 100644 --- a/src/ui/public/agg_types/__tests__/index.js +++ b/src/ui/public/agg_types/__tests__/index.js @@ -5,6 +5,7 @@ import './agg_params'; import './buckets/_histogram'; import './buckets/_geo_hash'; import './buckets/_range'; +import './buckets/_terms_other_bucket_helper'; import './buckets/date_histogram/_editor'; import './buckets/date_histogram/_params'; import { AggTypesIndexProvider } from 'ui/agg_types/index'; diff --git a/src/ui/public/agg_types/agg_type.js b/src/ui/public/agg_types/agg_type.js index b337a03db57d5..0c78e102535dc 100644 --- a/src/ui/public/agg_types/agg_type.js +++ b/src/ui/public/agg_types/agg_type.js @@ -140,6 +140,18 @@ export function AggTypesAggTypeProvider(Private) { */ this.decorateAggConfig = config.decorateAggConfig || null; + /** + * A function that needs to be called after the main request has been made + * and should return an updated response + * @param aggConfigs - agg config array used to produce main request + * @param aggConfig - AggConfig that requested the post flight request + * @param searchSourceAggs - SearchSource aggregation configuration + * @param resp - Response to the main request + * @param nestedSearchSource - the new SearchSource that will be used to make post flight request + * @return {Promise} + */ + this.postFlightRequest = config.postFlightRequest || _.identity; + if (config.getFormat) { this.getFormat = config.getFormat; } diff --git a/src/ui/public/agg_types/buckets/_terms_other_bucket_helper.js b/src/ui/public/agg_types/buckets/_terms_other_bucket_helper.js new file mode 100644 index 0000000000000..eabd46d5674b0 --- /dev/null +++ b/src/ui/public/agg_types/buckets/_terms_other_bucket_helper.js @@ -0,0 +1,187 @@ +import _ from 'lodash'; +import { VisAggConfigProvider } from 'ui/vis/agg_config'; +import { buildPhrasesFilter } from 'ui/filter_manager/lib/phrases'; +import { buildExistsFilter } from 'ui/filter_manager/lib/exists'; +import { buildQueryFromFilters } from 'ui/courier/data_source/build_query/from_filters'; + +/** + * walks the aggregation DSL and returns DSL starting at aggregation with id of startFromAggId + * @param aggNestedDsl: aggregation config DSL (top level) + * @param startFromId: id of an aggregation from where we want to get the nested DSL + */ +const getNestedAggDSL = (aggNestedDsl, startFromAggId) => { + if (aggNestedDsl[startFromAggId]) return aggNestedDsl[startFromAggId]; + return getNestedAggDSL(_.values(aggNestedDsl)[0].aggs, startFromAggId); +}; + +/** + * returns buckets from response for a specific other bucket + * @param aggConfigs: configuration for the aggregations + * @param response: response from elasticsearch + * @param aggWithOtherBucket: AggConfig of the aggregation with other bucket enabled + * @param key: key from the other bucket request for a specific other bucket + */ +const getAggResultBuckets = (aggConfigs, response, aggWithOtherBucket, key) => { + const keyParts = key.split('-'); + let responseAgg = response; + for (const i in keyParts) { + if (keyParts[i]) { + const agg = _.values(responseAgg)[0]; + const aggKey = _.keys(responseAgg)[0]; + const aggConfig = _.find(aggConfigs, agg => agg.id === aggKey); + const bucket = _.find(agg.buckets, (bucket, bucketObjKey) => { + const bucketKey = aggConfig.getKey(bucket, Number.isInteger(bucketObjKey) ? null : bucketObjKey).toString(); + return bucketKey === keyParts[i]; + }); + if (bucket) { + responseAgg = bucket; + } + } + } + if (responseAgg[aggWithOtherBucket.id]) return responseAgg[aggWithOtherBucket.id].buckets; + return []; +}; + +/** + * gets all the missing buckets in our response for a specific aggregation id + * @param responseAggs: array of aggregations from response + * @param aggId: id of the aggregation with missing bucket + */ +const getAggConfigResultMissingBuckets = (responseAggs, aggId) => { + const missingKey = '__missing__'; + let resultBuckets = []; + if (responseAggs[aggId]) { + const matchingBucket = responseAggs[aggId].buckets.find(bucket => bucket.key === missingKey); + if (matchingBucket) resultBuckets.push(matchingBucket); + return resultBuckets; + } + _.each(responseAggs, agg => { + if (agg.buckets) { + _.each(agg.buckets, bucket => { + resultBuckets = [ + ...resultBuckets, + ...getAggConfigResultMissingBuckets(bucket, aggId, missingKey) + ]; + }); + } + }); + + return resultBuckets; +}; + +/** + * gets all the terms that are NOT in the other bucket + * @param requestAgg: an aggregation we are looking at + * @param key: the key for this specific other bucket + * @param otherAgg: AggConfig of the aggregation with other bucket + */ +const getOtherAggTerms = (requestAgg, key, otherAgg) => { + return requestAgg['other-filter'].filters.filters[key].bool.must_not.filter(filter => + filter.match_phrase && filter.match_phrase[otherAgg.params.field.name] + ).map(filter => + filter.match_phrase[otherAgg.params.field.name].query + ); +}; + + +export const OtherBucketHelperProvider = (Private) => { + const AggConfig = Private(VisAggConfigProvider); + + const buildOtherBucketAgg = (aggConfigs, aggWithOtherBucket, response) => { + const bucketAggs = aggConfigs.filter(agg => agg.type.type === 'buckets'); + const index = bucketAggs.findIndex(agg => agg.id === aggWithOtherBucket.id); + const aggs = aggConfigs.toDsl(); + const indexPattern = aggWithOtherBucket.params.field.indexPattern; + + // create filters aggregation + const filterAgg = new AggConfig(aggConfigs[index].vis, { + type: 'filters', + id: 'other', + schema: { + group: 'buckets' + } + }); + + // nest all the child aggregations of aggWithOtherBucket + const resultAgg = { + aggs: getNestedAggDSL(aggs, aggWithOtherBucket.id).aggs, + filters: filterAgg.toDsl(), + }; + + // create filters for all parent aggregation buckets + const walkBucketTree = (aggIndex, aggs, aggId, filters, key) => { + const agg = aggs[aggId]; + const newAggIndex = aggIndex + 1; + const newAgg = bucketAggs[newAggIndex]; + const currentAgg = bucketAggs[aggIndex]; + if (aggIndex < index) { + _.each(agg.buckets, (bucket, bucketObjKey) => { + const bucketKey = currentAgg.getKey(bucket, Number.isInteger(bucketObjKey) ? null : bucketObjKey); + const filter = _.cloneDeep(bucket.filter) || currentAgg.createFilter(bucketKey); + const newFilters = [...filters, filter]; + walkBucketTree(newAggIndex, bucket, newAgg.id, newFilters, `${key}-${bucketKey.toString()}`); + }); + return; + } + + if (!aggWithOtherBucket.params.missingBucket || agg.buckets.some(bucket => bucket.key === '__missing__')) { + filters.push(buildExistsFilter(aggWithOtherBucket.params.field, aggWithOtherBucket.params.field.indexPattern)); + } + + // create not filters for all the buckets + _.each(agg.buckets, bucket => { + if (bucket.key === '__missing__') return; + const filter = currentAgg.createFilter(bucket.key); + filter.meta.negate = true; + filters.push(filter); + }); + + resultAgg.filters.filters[key] = { + bool: buildQueryFromFilters(filters, _.noop, indexPattern) + }; + }; + walkBucketTree(0, response.aggregations, bucketAggs[0].id, [], ''); + + return () => { + return { + 'other-filter': resultAgg + }; + }; + }; + + const mergeOtherBucketAggResponse = (aggsConfig, response, otherResponse, otherAgg, requestAgg) => { + const updatedResponse = _.cloneDeep(response); + _.each(otherResponse.aggregations['other-filter'].buckets, (bucket, key) => { + if (!bucket.doc_count) return; + const bucketKey = key.replace(/^-/, ''); + const aggResultBuckets = getAggResultBuckets(aggsConfig, updatedResponse.aggregations, otherAgg, bucketKey); + const requestFilterTerms = getOtherAggTerms(requestAgg, key, otherAgg); + + const phraseFilter = buildPhrasesFilter(otherAgg.params.field, requestFilterTerms, otherAgg.params.field.indexPattern); + phraseFilter.meta.negate = true; + bucket.filters = [ phraseFilter ]; + bucket.key = otherAgg.params.otherBucketLabel; + + if (aggResultBuckets.some(bucket => bucket.key === '__missing__')) { + bucket.filters.push(buildExistsFilter(otherAgg.params.field, otherAgg.params.field.indexPattern)); + } + + aggResultBuckets.push(bucket); + }); + return updatedResponse; + }; + + const updateMissingBucket = (response, aggConfigs, agg) => { + const updatedResponse = _.cloneDeep(response); + const aggResultBuckets = getAggConfigResultMissingBuckets(updatedResponse.aggregations, agg.id); + aggResultBuckets.forEach(bucket => { + bucket.key = agg.params.missingBucketLabel; + const existsFilter = buildExistsFilter(agg.params.field, agg.params.field.indexPattern); + existsFilter.meta.negate = true; + bucket.filters = [ existsFilter ]; + }); + return updatedResponse; + }; + + return { buildOtherBucketAgg, mergeOtherBucketAggResponse, updateMissingBucket }; +}; diff --git a/src/ui/public/agg_types/buckets/terms.js b/src/ui/public/agg_types/buckets/terms.js index fe997c65b29cd..5287794911899 100644 --- a/src/ui/public/agg_types/buckets/terms.js +++ b/src/ui/public/agg_types/buckets/terms.js @@ -6,6 +6,7 @@ import { AggTypesBucketsCreateFilterTermsProvider } from 'ui/agg_types/buckets/c import orderAggTemplate from 'ui/agg_types/controls/order_agg.html'; import orderAndSizeTemplate from 'ui/agg_types/controls/order_and_size.html'; import { RouteBasedNotifierProvider } from 'ui/route_based_notifier'; +import { OtherBucketHelperProvider } from './_terms_other_bucket_helper'; export function AggTypesBucketsTermsProvider(Private) { const BucketAggType = Private(AggTypesBucketsBucketAggTypeProvider); @@ -13,6 +14,7 @@ export function AggTypesBucketsTermsProvider(Private) { const Schemas = Private(VisSchemasProvider); const createFilter = Private(AggTypesBucketsCreateFilterTermsProvider); const routeBasedNotifier = Private(RouteBasedNotifierProvider); + const { buildOtherBucketAgg, mergeOtherBucketAggResponse, updateMissingBucket } = Private(OtherBucketHelperProvider); const aggFilter = [ '!top_hits', '!percentiles', '!median', '!std_dev', @@ -60,11 +62,40 @@ export function AggTypesBucketsTermsProvider(Private) { return agg.getFieldDisplayName() + ': ' + params.order.display; }, createFilter: createFilter, + postFlightRequest: async (resp, aggConfigs, aggConfig, nestedSearchSource) => { + if (aggConfig.params.otherBucket) { + const filterAgg = buildOtherBucketAgg(aggConfigs, aggConfig, resp); + nestedSearchSource.set('aggs', filterAgg); + const response = await nestedSearchSource.fetchAsRejectablePromise(); + resp = mergeOtherBucketAggResponse(aggConfigs, resp, response, aggConfig, filterAgg()); + } + if (aggConfig.params.missingBucket) { + resp = updateMissingBucket(resp, aggConfigs, aggConfig); + } + return resp; + }, params: [ { name: 'field', filterFieldTypes: ['number', 'boolean', 'date', 'ip', 'string'] }, + { + name: 'otherBucket', + default: false, + write: _.noop + }, { + name: 'otherBucketLabel', + default: 'Other', + write: _.noop + }, { + name: 'missingBucket', + default: false, + write: _.noop + }, { + name: 'missingBucketLabel', + default: 'Missing', + write: _.noop + }, { name: 'exclude', type: 'string', @@ -170,6 +201,10 @@ export function AggTypesBucketsTermsProvider(Private) { output.params.valueType = agg.getField().type === 'number' ? 'float' : agg.getField().type; } + if (agg.params.missingBucket) { + output.params.missing = '__missing__'; + } + if (!orderAgg) { order[agg.params.orderBy || '_count'] = dir; return; diff --git a/src/ui/public/agg_types/controls/order_and_size.html b/src/ui/public/agg_types/controls/order_and_size.html index 9e49d9b2e3fe4..26e0a9bfd1e55 100644 --- a/src/ui/public/agg_types/controls/order_and_size.html +++ b/src/ui/public/agg_types/controls/order_and_size.html @@ -1,25 +1,79 @@ -
-
- - +
+
+
+ + +
+
+ + +
-
- - +
+
+ +
+
+
+
+ +
+ +
+
+
+
+
+ +
+
+
+
+ +
+ +
+
diff --git a/src/ui/public/agg_types/controls/raw_json.html b/src/ui/public/agg_types/controls/raw_json.html index 66c4bf249a311..b49a5c4e4520e 100644 --- a/src/ui/public/agg_types/controls/raw_json.html +++ b/src/ui/public/agg_types/controls/raw_json.html @@ -1,17 +1,17 @@
- - - + + -
-

- Any JSON formatted properties you add here will be merged with the elasticsearch aggregation definition for this section. For example shard_size on a terms aggregation -

-