diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 19dffe7114a1..25d4433bd8f9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,7 +55,7 @@ Please make sure you have signed the [Contributor License Agreement](http://www. npm run elasticsearch ``` -- Start the development server. +- Start the development server. _On Windows, you'll need you use Git Bash, Cygwin, or a similar shell that exposes the `sh` command._ ```sh npm start @@ -128,7 +128,7 @@ Runs both server and browser tests, but skips linting Run only the server tests `npm run test:browser` -Run only the browser tests +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. `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. @@ -146,7 +146,8 @@ Run the tests for just your particular plugin. Assuming you plugin lives outside #### Running browser automation tests: -*The Selenium server that is started currently only runs the tests in Firefox* +*The Selenium server that is started currently only runs the tests in a recent version of Firefox.* +*You can use the `PATH` environment variable to specify which version of Firefox to use.* The following will start Kibana, Elasticsearch and Selenium for you. To run the functional UI tests use the following commands @@ -177,7 +178,7 @@ npm run test:ui:runner - These tests have been developed and tested with Chrome and Firefox browser. In theory, they should work on all browsers (that's the benefit of Intern using Leadfoot). - These tests should also work with an external testing service like https://saucelabs.com/ or https://www.browserstack.com/ but that has not been tested. - https://theintern.github.io/ -- https://theintern.github.io/leadfoot/Element.html +- https://theintern.github.io/leadfoot/module-leadfoot_Element.html #### Building OS packages diff --git a/docs/advanced-settings.asciidoc b/docs/advanced-settings.asciidoc index e649d7dc5d8a..eb599be309ad 100644 --- a/docs/advanced-settings.asciidoc +++ b/docs/advanced-settings.asciidoc @@ -1,6 +1,8 @@ [[kibana-settings-reference]] -WARNING: Modifying the following settings can signficantly affect Kibana's performance and cause problems that are difficult to diagnose. Setting a property's value to a blank field will revert to the default behavior, which may not be compatible with other configuration settings. Deleting a custom setting removes it from Kibana permanently. +WARNING: Modifying the following settings can signficantly affect Kibana's performance and cause problems that are +difficult to diagnose. Setting a property's value to a blank field will revert to the default behavior, which may not be +compatible with other configuration settings. Deleting a custom setting removes it from Kibana permanently. .Kibana Settings Reference [horizontal] @@ -8,30 +10,43 @@ WARNING: Modifying the following settings can signficantly affect Kibana's perfo `sort:options`:: Options for the Elasticsearch https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-sort.html[sort] parameter. `dateFormat`:: The format to use for displaying pretty-formatted dates. `dateFormat:tz`:: The timezone that Kibana uses. The default value of `Browser` uses the timezone detected by the browser. -`dateFormat:scaled`:: These values define the format used to render ordered time-based data. Formatted timestamps must adapt to the interval between measurements. Keys are http://en.wikipedia.org/wiki/ISO_8601#Time_intervals[ISO8601 intervals]. +`dateFormat:scaled`:: These values define the format used to render ordered time-based data. Formatted timestamps must +adapt to the interval between measurements. Keys are http://en.wikipedia.org/wiki/ISO_8601#Time_intervals[ISO8601 intervals]. `defaultIndex`:: Default is `null`. This property specifies the default index. -`metaFields`:: An array of fields outside of `_source`. Kibana merges these fields into the document when displaying the document. +`metaFields`:: An array of fields outside of `_source`. Kibana merges these fields into the document when displaying the +document. `discover:sampleSize`:: The number of rows to show in the Discover table. -`doc_table:highlight`:: Highlight results in Discover and Saved Searches Dashboard. Highlighing makes request slow when working on big documents. Set this property to `false` to disable highlighting. -`courier:maxSegmentCount`:: Kibana splits requests in the Discover app into segments to limit the size of requests sent to the Elasticsearch cluster. This setting constrains the length of the segment list. Long segment lists can significantly increase request processing time. +`doc_table:highlight`:: Highlight results in Discover and Saved Searches Dashboard. Highlighing makes request slow when +working on big documents. Set this property to `false` to disable highlighting. +`courier:maxSegmentCount`:: Kibana splits requests in the Discover app into segments to limit the size of requests sent to +the Elasticsearch cluster. This setting constrains the length of the segment list. Long segment lists can significantly +increase request processing time. `fields:popularLimit`:: This setting governs how many of the top most popular fields are shown. `histogram:barTarget`:: When date histograms use the `auto` interval, Kibana attempts to generate this number of bars. -`histogram:maxBars`:: Date histograms are not generated with more bars than the value of this property, scaling values when necessary. -`visualization:tileMap:maxPrecision`:: The maximum geoHash precision displayed on tile maps: 7 is high, 10 is very high, 12 is the maximum. http://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator[Explanation of cell dimensions]. +`histogram:maxBars`:: Date histograms are not generated with more bars than the value of this property, scaling values +when necessary. +`visualization:tileMap:maxPrecision`:: The maximum geoHash precision displayed on tile maps: 7 is high, 10 is very high, +12 is the maximum. http://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator[Explanation of cell dimensions]. `visualization:tileMap:WMSdefaults`:: Default properties for the WMS map server support in the tile map. `visualization:colorMapping`:: Maps values to specified colors within visualizations. `visualization:loadingDelay`:: Time to wait before dimming visualizations during query. `csv:separator`:: A string that serves as the separator for exported values. `csv:quoteValues`:: Set this property to `true` to quote exported values. -`history:limit`:: In fields that have history, such as query inputs, the value of this property limits how many recent values are shown. -`shortDots:enable`:: Set this property to `true` to shorten long field names in visualizations. For example, instead of `foo.bar.baz`, show `f.b.baz`. -`truncate:maxHeight`:: This property specifies the maximum height that a cell occupies in a table. A value of 0 disables truncation. -`indexPattern:fieldMapping:lookBack`:: The value of this property sets the number of recent matching patterns to query the field mapping for index patterns with names that contain timestamps. -`format:defaultTypeMap`:: A map of the default format name for each field type. Field types that are not explicitly mentioned use "_default_". +`history:limit`:: In fields that have history, such as query inputs, the value of this property limits how many recent +values are shown. +`shortDots:enable`:: Set this property to `true` to shorten long field names in visualizations. For example, instead of +`foo.bar.baz`, show `f.b.baz`. +`truncate:maxHeight`:: This property specifies the maximum height that a cell occupies in a table. A value of 0 disables +truncation. +`indexPattern:fieldMapping:lookBack`:: The value of this property sets the number of recent matching patterns to query the +field mapping for index patterns with names that contain timestamps. +`format:defaultTypeMap`:: A map of the default format name for each field type. Field types that are not explicitly +mentioned use "_default_". `format:number:defaultPattern`:: Default numeral format for the "number" format. `format:bytes:defaultPattern`:: Default numeral format for the "bytes" format. `format:percent:defaultPattern`:: Default numeral format for the "percent" format. `format:currency:defaultPattern`:: Default numeral format for the "currency" format. +`savedObjects:perPage`:: The number of objects shown on each page of the list of saved objects. The default value is 5. `timepicker:timeDefaults`:: The default time filter selection. `timepicker:refreshIntervalDefaults`:: The time filter's default refresh interval. -`dashboard:defaultDarkTheme`:: Set this property to `true` to make new dashboards use the dark theme by default. \ No newline at end of file +`dashboard:defaultDarkTheme`:: Set this property to `true` to make new dashboards use the dark theme by default. diff --git a/docs/datatable.asciidoc b/docs/datatable.asciidoc index b80cb8ccd812..6246b19f0f1d 100644 --- a/docs/datatable.asciidoc +++ b/docs/datatable.asciidoc @@ -45,6 +45,8 @@ sub-aggregation from the list of types. You can use the up or down arrows to the right of the aggregation's type to change the aggregation's priority. +Enter a string in the *Custom Label* field to change the display label. + You can click the *Advanced* link to display more customization options for your metrics or bucket aggregation: *Exclude Pattern*:: Specify a pattern in this field to exclude from the results. diff --git a/docs/metric.asciidoc b/docs/metric.asciidoc index 8a813f7dba86..e4ce743a8210 100644 --- a/docs/metric.asciidoc +++ b/docs/metric.asciidoc @@ -4,6 +4,7 @@ A metric visualization displays a single number for each aggregation you select: include::y-axis-aggs.asciidoc[] + You can click the *Advanced* link to display more customization options: *JSON Input*:: A text field where you can add specific JSON-formatted properties to merge with the aggregation @@ -17,7 +18,7 @@ NOTE: In Elasticsearch releases 1.4.3 and later, this functionality requires you The availability of these options varies depending on the aggregation you choose. -Click the *Options* tab to change the font used to display the metrics. +Click the *Options* tab to display the font size slider. [float] [[metric-viewing-detailed-information]] diff --git a/docs/pie.asciidoc b/docs/pie.asciidoc index 6c2b5a32be97..233b263b1e84 100644 --- a/docs/pie.asciidoc +++ b/docs/pie.asciidoc @@ -11,6 +11,8 @@ field. Select a field from the drop-down. *Unique Count*:: The {ref}search-aggregations-metrics-cardinality-aggregation.html[_cardinality_] aggregation returns the number of unique values in a field. Select a field from the drop-down. +Enter a string in the *Custom Label* field to change the display label. + The _buckets_ aggregations determine what information is being retrieved from your data set. Before you choose a buckets aggregation, specify if you are splitting slices within a single chart or splitting into @@ -57,6 +59,8 @@ aggregation's type to change the aggregation's priority. include::color-picker.asciidoc[] +Enter a string in the *Custom Label* field to change the display label. + You can click the *Advanced* link to display more customization options for your metrics or bucket aggregation: *Exclude Pattern*:: Specify a pattern in this field to exclude from the results. diff --git a/docs/plugins.asciidoc b/docs/plugins.asciidoc index b4ff08e6d1d5..d505ef84e4db 100644 --- a/docs/plugins.asciidoc +++ b/docs/plugins.asciidoc @@ -65,6 +65,11 @@ bin/kibana plugin --remove marvel You can also remove a plugin manually by deleting the plugin's subdirectory under the `installedPlugins` directory. +[float] +=== Listing Installed Plugins + +Use `--list` or `-l` option to list the currently installed plugins. + [float] === Updating Plugins diff --git a/docs/tilemap.asciidoc b/docs/tilemap.asciidoc index 804abea1219b..4d959de0ed25 100644 --- a/docs/tilemap.asciidoc +++ b/docs/tilemap.asciidoc @@ -19,6 +19,8 @@ numeric field. Select a field from the drop-down. *Unique Count*:: The {ref}search-aggregations-metrics-cardinality-aggregation.html[_cardinality_] aggregation returns the number of unique values in a field. Select a field from the drop-down. +Enter a string in the *Custom Label* field to change the display label. + The _buckets_ aggregations determine what information is being retrieved from your data set. Before you choose a buckets aggregation, specify if you are splitting the chart or displaying the buckets as *Geo @@ -71,6 +73,8 @@ based on the geohash coordinates. NOTE: By default, the *Change precision on map zoom* box is checked. Uncheck the box to disable this behavior. +Enter a string in the *Custom Label* field to change the display label. + You can click the *Advanced* link to display more customization options for your metrics or bucket aggregation: *Exclude Pattern*:: Specify a pattern in this field to exclude from the results. diff --git a/docs/vertbar.asciidoc b/docs/vertbar.asciidoc index 201b87843452..b329897a3ad8 100644 --- a/docs/vertbar.asciidoc +++ b/docs/vertbar.asciidoc @@ -26,6 +26,8 @@ values field. Click *+Add* to add a values field. You can add an aggregation by clicking the *+ Add Aggregation* button. +Enter a string in the *Custom Label* field to change the display label. + The _buckets_ aggregations determine what information is being retrieved from your data set. Before you choose a buckets aggregation, specify if you are splitting slices within a single chart or splitting into @@ -36,6 +38,8 @@ include::x-axis-aggs.asciidoc[] include::color-picker.asciidoc[] +Enter a string in the *Custom Label* field to change the display label. + You can click the *Advanced* link to display more customization options for your metrics or bucket aggregation: *Exclude Pattern*:: Specify a pattern in this field to exclude from the results. diff --git a/docs/x-axis-aggs.asciidoc b/docs/x-axis-aggs.asciidoc index 802625b0afac..3b9fd3e94862 100644 --- a/docs/x-axis-aggs.asciidoc +++ b/docs/x-axis-aggs.asciidoc @@ -28,8 +28,8 @@ remove a range. or bottom _n_ elements of a given field to display, ordered by count or a custom metric. *Filters*:: You can specify a set of {ref}/search-aggregations-bucket-filters-aggregation.html[_filters_] for the data. You can specify a filter as a query string or in JSON format, just as in the Discover search bar. Click *Add Filter* to -add another filter. Click the image:images/labelbutton.png[] *label* button to open the label field, where you can type in a -name to display on the visualization. +add another filter. Click the image:images/labelbutton.png[Label button icon] *label* button to open the label field, where +you can type in a name to display on the visualization. *Significant Terms*:: Displays the results of the experimental {ref}/search-aggregations-bucket-significantterms-aggregation.html[_significant terms_] aggregation. @@ -39,3 +39,5 @@ from the list of types. When multiple aggregations are defined on a chart's axis, you can use the up or down arrows to the right of the aggregation's type to change the aggregation's priority. + +Enter a string in the *Custom Label* field to change the display label. diff --git a/docs/y-axis-aggs.asciidoc b/docs/y-axis-aggs.asciidoc index 18029bb0904f..1153a1cb478f 100644 --- a/docs/y-axis-aggs.asciidoc +++ b/docs/y-axis-aggs.asciidoc @@ -22,3 +22,5 @@ from the drop-down, then specify one or more percentile rank values in the *Valu values field. Click *+Add* to add a values field. You can add an aggregation by clicking the *+ Add Aggregation* button. + +Enter a string in the *Custom Label* field to change the display label. diff --git a/package.json b/package.json index 59d8c8f72be6..c49c26137d68 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "test:coverage": "grunt test:coverage", "build": "grunt build", "build:ospackages": "grunt build --os-packages", - "start": "./bin/kibana --dev", + "start": "sh ./bin/kibana --dev", "precommit": "grunt precommit", "karma": "karma start", "elasticsearch": "grunt esvm:dev:keepalive", @@ -156,7 +156,7 @@ "grunt-cli": "0.1.13", "grunt-contrib-clean": "0.6.0", "grunt-contrib-copy": "0.8.1", - "grunt-esvm": "3.0.3", + "grunt-esvm": "3.0.4", "grunt-karma": "0.12.0", "grunt-run": "0.5.0", "grunt-s3": "0.2.0-alpha.3", @@ -179,6 +179,7 @@ "makelogs": "3.0.0-beta3", "marked-text-renderer": "0.1.0", "mocha": "2.3.0", + "ncp": "2.0.0", "nock": "2.10.0", "npm": "2.11.0", "portscanner": "1.0.0", diff --git a/src/cli/serve/read_yaml_config.js b/src/cli/serve/read_yaml_config.js index 8ef3acff942f..eda24281d245 100644 --- a/src/cli/serve/read_yaml_config.js +++ b/src/cli/serve/read_yaml_config.js @@ -48,7 +48,14 @@ module.exports = function (path) { _.forOwn(val, function (subVal, subKey) { apply(config, subVal, key + '.' + subKey); }); - } else { + } + else if (_.isArray(val)) { + config[key] = []; + val.forEach((subVal, i) => { + apply(config, subVal, key + '.' + i); + }); + } + else { _.set(config, key, val); } } diff --git a/src/plugins/elasticsearch/index.js b/src/plugins/elasticsearch/index.js index 76a44285d9ef..c60f56f88b6d 100644 --- a/src/plugins/elasticsearch/index.js +++ b/src/plugins/elasticsearch/index.js @@ -33,6 +33,16 @@ module.exports = function ({ Plugin }) { }).default(); }, + uiExports: { + injectDefaultVars(server, options) { + return { + esRequestTimeout: options.requestTimeout, + esShardTimeout: options.shardTimeout, + esApiVersion: options.apiVersion, + }; + } + }, + init(server, options) { const kibanaIndex = server.config().get('kibana.index'); diff --git a/src/plugins/elasticsearch/lib/__tests__/routes.js b/src/plugins/elasticsearch/lib/__tests__/routes.js index c4bacca30d55..34a75f6a2981 100644 --- a/src/plugins/elasticsearch/lib/__tests__/routes.js +++ b/src/plugins/elasticsearch/lib/__tests__/routes.js @@ -1,9 +1,8 @@ import expect from 'expect.js'; -import util from 'util'; -import * as kbnTestServer from '../../../../../test/utils/kbn_server'; - -const format = util.format; +import { format } from 'util'; +import * as kbnTestServer from '../../../../../test/utils/kbn_server'; +import fromRoot from '../../../../utils/from_root'; describe('plugins/elasticsearch', function () { describe('routes', function () { @@ -13,7 +12,13 @@ describe('plugins/elasticsearch', function () { before(function () { this.timeout(60000); // sometimes waiting for server takes longer than 10 - kbnServer = kbnTestServer.createServer(); + kbnServer = kbnTestServer.createServer({ + plugins: { + scanDirs: [ + fromRoot('src/plugins') + ] + } + }); return kbnServer.ready() .then(() => kbnServer.server.plugins.elasticsearch.waitUntilReady()); }); diff --git a/src/plugins/kibana/index.js b/src/plugins/kibana/index.js index 1d44dafab2a5..f736d9e78c18 100644 --- a/src/plugins/kibana/index.js +++ b/src/plugins/kibana/index.js @@ -67,7 +67,12 @@ module.exports = function (kibana) { description: 'define index patterns, change config, and more', icon: 'plugins/kibana/assets/settings.svg', } - ] + ], + injectDefaultVars(server, options) { + return { + kbnIndex: options.index + }; + }, }, init: function (server, options) { diff --git a/src/plugins/kibana/public/dashboard/styles/main.less b/src/plugins/kibana/public/dashboard/styles/main.less index e660cb50d4d6..a0cccdd1e6dd 100644 --- a/src/plugins/kibana/public/dashboard/styles/main.less +++ b/src/plugins/kibana/public/dashboard/styles/main.less @@ -156,21 +156,3 @@ dashboard-grid { .dashboard-panel-picker > .list-group-item { border-top: 0; } - -.dashboard-load { - margin: 10px; -} - -.dashboard { - &-info { - line-height: 30px; - padding: 0px 10px; - border-bottom-left-radius: @border-radius-base; - - - &-title { - font-weight: bold; - margin-right: 10px; - } - } -} diff --git a/src/plugins/kibana/server/lib/__tests__/create_mappings_from_pattern_fields.js b/src/plugins/kibana/server/lib/__tests__/create_mappings_from_pattern_fields.js index cf4563458ece..7444d3838463 100644 --- a/src/plugins/kibana/server/lib/__tests__/create_mappings_from_pattern_fields.js +++ b/src/plugins/kibana/server/lib/__tests__/create_mappings_from_pattern_fields.js @@ -39,21 +39,21 @@ describe('createMappingsFromPatternFields', function () { let mappings = createMappingsFromPatternFields(testFields); _.forEach(mappings, function (mapping) { - if (mapping.type !== 'string') { + if (mapping.type !== 'text') { expect(_.isEqual(mapping, { type: mapping.type, - index: 'not_analyzed', + index: true, doc_values: true })).to.be.ok(); } }); }); - it('should give strings a multi-field mapping', function () { + it('should give strings a multi-field mapping with a "text" base type', function () { let mappings = createMappingsFromPatternFields(testFields); _.forEach(mappings, function (mapping) { - if (mapping.type === 'string') { + if (mapping.type === 'text') { expect(mapping).to.have.property('fields'); } }); @@ -68,7 +68,7 @@ describe('createMappingsFromPatternFields', function () { expect(mappings.geo.properties).to.have.property('coordinates'); expect(_.isEqual(mappings.geo.properties.coordinates, { type: 'geo_point', - index: 'not_analyzed', + index: true, doc_values: true })).to.be.ok(); }); diff --git a/src/plugins/kibana/server/lib/create_mappings_from_pattern_fields.js b/src/plugins/kibana/server/lib/create_mappings_from_pattern_fields.js index d05e65099161..b9ee189e35c4 100644 --- a/src/plugins/kibana/server/lib/create_mappings_from_pattern_fields.js +++ b/src/plugins/kibana/server/lib/create_mappings_from_pattern_fields.js @@ -13,10 +13,9 @@ module.exports = function createMappingsFromPatternFields(fields) { if (field.type === 'string') { mapping = { - type: 'string', - index: 'analyzed', + type: 'text', fields: { - raw: {type: 'string', index: 'not_analyzed', doc_values: true, ignore_above: 256} + raw: {type: 'keyword', ignore_above: 256} } }; } @@ -24,7 +23,7 @@ module.exports = function createMappingsFromPatternFields(fields) { const fieldType = field.type === 'number' ? 'double' : field.type; mapping = { type: fieldType, - index: 'not_analyzed', + index: true, doc_values: true }; } diff --git a/src/plugins/kibana/server/routes/api/ingest/register_post.js b/src/plugins/kibana/server/routes/api/ingest/register_post.js index fe944f530705..a4e259d6efae 100644 --- a/src/plugins/kibana/server/routes/api/ingest/register_post.js +++ b/src/plugins/kibana/server/routes/api/ingest/register_post.js @@ -58,10 +58,9 @@ module.exports = function registerPost(server) { match: '*', match_mapping_type: 'string', mapping: { - type: 'string', - index: 'analyzed', + type: 'text', fields: { - raw: {type: 'string', index: 'not_analyzed', doc_values: true, ignore_above: 256} + raw: {type: 'keyword', ignore_above: 256} } } } diff --git a/src/server/plugins/plugin.js b/src/server/plugins/plugin.js index 2b504142a86c..18c7786c1572 100644 --- a/src/server/plugins/plugin.js +++ b/src/server/plugins/plugin.js @@ -1,9 +1,11 @@ import _ from 'lodash'; import Joi from 'joi'; -import { attempt, fromNode } from 'bluebird'; +import Bluebird, { attempt, fromNode } from 'bluebird'; import { basename, resolve } from 'path'; import { inherits } from 'util'; +const extendInitFns = Symbol('extend plugin initialization'); + const defaultConfigSchema = Joi.object({ enabled: Joi.boolean().default(true) }).default(); @@ -57,6 +59,7 @@ module.exports = class Plugin { this.externalInit = opts.init || _.noop; this.getConfigSchema = opts.config || _.noop; this.init = _.once(this.init); + this[extendInitFns] = []; if (opts.publicDir === false) { this.publicDir = null; @@ -98,14 +101,12 @@ module.exports = class Plugin { let { config } = kbnServer; // setup the hapi register function and get on with it - let register = (server, options, next) => { + const asyncRegister = async (server, options) => { this.server = server; - // bind the server and options to all - // apps created by this plugin - for (let app of this.apps) { - app.getInjectedVars = _.partial(app.getInjectedVars, server, options); - } + await Promise.all(this[extendInitFns].map(async fn => { + await fn.call(this, server, options); + })); server.log(['plugins', 'debug'], { tmpl: 'Initializing plugin <%= plugin.id %>', @@ -119,7 +120,11 @@ module.exports = class Plugin { this.status = kbnServer.status.create(`plugin:${this.id}`); server.expose('status', this.status); - attempt(this.externalInit, [server, options], this).nodeify(next); + return await attempt(this.externalInit, [server, options], this); + }; + + const register = (server, options, next) => { + Bluebird.resolve(asyncRegister(server, options)).nodeify(next); }; register.attributes = { name: id, version: version }; @@ -138,6 +143,10 @@ module.exports = class Plugin { } } + extendInit(fn) { + this[extendInitFns].push(fn); + } + toJSON() { return this.pkg; } diff --git a/src/ui/__tests__/fixtures/plugin_async_foo/index.js b/src/ui/__tests__/fixtures/plugin_async_foo/index.js new file mode 100644 index 000000000000..a0cd8887bbed --- /dev/null +++ b/src/ui/__tests__/fixtures/plugin_async_foo/index.js @@ -0,0 +1,18 @@ +import Bluebird from 'bluebird'; + +export default kibana => new kibana.Plugin({ + config(Joi) { + return Joi.object().keys({ + enabled: Joi.boolean().default(true), + delay: Joi.number().required(), + shared: Joi.string(), + }).default(); + }, + + uiExports: { + async injectDefaultVars(server, options) { + await Bluebird.delay(options.delay); + return { shared: options.shared }; + } + } +}); diff --git a/src/ui/__tests__/fixtures/plugin_async_foo/package.json b/src/ui/__tests__/fixtures/plugin_async_foo/package.json new file mode 100644 index 000000000000..4ad7dda995ca --- /dev/null +++ b/src/ui/__tests__/fixtures/plugin_async_foo/package.json @@ -0,0 +1,4 @@ +{ + "name": "plugin_async_foo", + "version": "0.0.0" +} diff --git a/src/ui/__tests__/fixtures/plugin_bar/index.js b/src/ui/__tests__/fixtures/plugin_bar/index.js new file mode 100644 index 000000000000..59c555644449 --- /dev/null +++ b/src/ui/__tests__/fixtures/plugin_bar/index.js @@ -0,0 +1,14 @@ +export default kibana => new kibana.Plugin({ + config(Joi) { + return Joi.object().keys({ + enabled: Joi.boolean().default(true), + shared: Joi.string() + }).default(); + }, + + uiExports: { + injectDefaultVars(server, options) { + return { shared: options.shared }; + } + } +}); diff --git a/src/ui/__tests__/fixtures/plugin_bar/package.json b/src/ui/__tests__/fixtures/plugin_bar/package.json new file mode 100644 index 000000000000..bfc5da018226 --- /dev/null +++ b/src/ui/__tests__/fixtures/plugin_bar/package.json @@ -0,0 +1,4 @@ +{ + "name": "plugin_bar", + "version": "0.0.0" +} diff --git a/src/ui/__tests__/fixtures/plugin_foo/index.js b/src/ui/__tests__/fixtures/plugin_foo/index.js new file mode 100644 index 000000000000..59c555644449 --- /dev/null +++ b/src/ui/__tests__/fixtures/plugin_foo/index.js @@ -0,0 +1,14 @@ +export default kibana => new kibana.Plugin({ + config(Joi) { + return Joi.object().keys({ + enabled: Joi.boolean().default(true), + shared: Joi.string() + }).default(); + }, + + uiExports: { + injectDefaultVars(server, options) { + return { shared: options.shared }; + } + } +}); diff --git a/src/ui/__tests__/fixtures/plugin_foo/package.json b/src/ui/__tests__/fixtures/plugin_foo/package.json new file mode 100644 index 000000000000..6a73b89110ae --- /dev/null +++ b/src/ui/__tests__/fixtures/plugin_foo/package.json @@ -0,0 +1,4 @@ +{ + "name": "plugin_foo", + "version": "0.0.0" +} diff --git a/src/ui/__tests__/ui_exports.js b/src/ui/__tests__/ui_exports.js index 678d88d83983..adf561822e94 100644 --- a/src/ui/__tests__/ui_exports.js +++ b/src/ui/__tests__/ui_exports.js @@ -1,6 +1,8 @@ import expect from 'expect.js'; +import { resolve } from 'path'; import UiExports from '../ui_exports'; +import * as kbnTestServer from '../../../test/utils/kbn_server'; describe('UiExports', function () { describe('#find()', function () { @@ -23,4 +25,83 @@ describe('UiExports', function () { expect(uiExports.find(['foo', 'bar'])).to.eql(['a', 'b', 'c']); }); }); +// + describe('#defaultInjectedVars', function () { + context('two plugins, two sync', function () { + this.slow(10000); + this.timeout(60000); + + let kbnServer; + before(async function () { + kbnServer = kbnTestServer.createServer({ + plugins: { + paths: [ + resolve(__dirname, 'fixtures/plugin_bar'), + resolve(__dirname, 'fixtures/plugin_foo') + ] + }, + + plugin_foo: { + shared: 'foo' + }, + + plugin_bar: { + shared: 'bar' + } + }); + + await kbnServer.ready(); + }); + + after(async function () { + await kbnServer.close(); + }); + + it('merges the two plugins in the order they are loaded', function () { + expect(kbnServer.uiExports.defaultInjectedVars).to.eql({ + shared: 'foo' + }); + }); + }); + + context('two plugins, one async', function () { + this.slow(10000); + this.timeout(60000); + + let kbnServer; + before(async function () { + kbnServer = kbnTestServer.createServer({ + plugins: { + scanDirs: [], + paths: [ + resolve(__dirname, 'fixtures/plugin_async_foo'), + resolve(__dirname, 'fixtures/plugin_foo') + ] + }, + + plugin_async_foo: { + delay: 500, + shared: 'foo' + }, + + plugin_bar: { + shared: 'bar' + } + }); + + await kbnServer.ready(); + }); + + after(async function () { + await kbnServer.close(); + }); + + it('merges the two plugins in the order they are loaded', function () { + // even though plugin_async_foo loads 500ms later, it is still "first" to merge + expect(kbnServer.uiExports.defaultInjectedVars).to.eql({ + shared: 'foo' + }); + }); + }); + }); }); diff --git a/src/ui/index.js b/src/ui/index.js index 22fb688f9a73..1231704ac531 100644 --- a/src/ui/index.js +++ b/src/ui/index.js @@ -59,16 +59,6 @@ module.exports = async (kbnServer, server, config) => { } }); - const defaultInjectedVars = {}; - if (config.has('kibana')) { - defaultInjectedVars.kbnIndex = config.get('kibana.index'); - } - if (config.has('elasticsearch')) { - defaultInjectedVars.esRequestTimeout = config.get('elasticsearch.requestTimeout'); - defaultInjectedVars.esShardTimeout = config.get('elasticsearch.shardTimeout'); - defaultInjectedVars.esApiVersion = config.get('elasticsearch.apiVersion'); - } - server.decorate('reply', 'renderApp', function (app) { const payload = { app: app, @@ -77,7 +67,7 @@ module.exports = async (kbnServer, server, config) => { buildNum: config.get('pkg.buildNum'), buildSha: config.get('pkg.buildSha'), basePath: config.get('server.basePath'), - vars: defaults(app.getInjectedVars() || {}, defaultInjectedVars), + vars: defaults(app.getInjectedVars() || {}, uiExports.defaultInjectedVars), }; return this.view(app.templateName, { diff --git a/src/ui/public/agg_types/metrics/percentile_ranks.js b/src/ui/public/agg_types/metrics/percentile_ranks.js index f782f2a13f29..f0e6147a6c70 100644 --- a/src/ui/public/agg_types/metrics/percentile_ranks.js +++ b/src/ui/public/agg_types/metrics/percentile_ranks.js @@ -37,6 +37,11 @@ export default function AggTypeMetricPercentileRanksProvider(Private) { name: 'values', editor: valuesEditor, default: [] + }, + { + write(agg, output) { + output.params.keyed = false; + } } ], getResponseAggs: function (agg) { diff --git a/src/ui/public/agg_types/metrics/percentiles.js b/src/ui/public/agg_types/metrics/percentiles.js index 95fd76dfbf29..08edbfc193a8 100644 --- a/src/ui/public/agg_types/metrics/percentiles.js +++ b/src/ui/public/agg_types/metrics/percentiles.js @@ -35,6 +35,11 @@ export default function AggTypeMetricPercentilesProvider(Private) { name: 'percents', editor: percentsEditor, default: [1, 5, 25, 50, 75, 95, 99] + }, + { + write(agg, output) { + output.params.keyed = false; + } } ], getResponseAggs: function (agg) { diff --git a/src/ui/public/index_patterns/__tests__/_field_format.js b/src/ui/public/index_patterns/__tests__/_field_format.js index f9e156b7ff5c..504a1eb2f93a 100644 --- a/src/ui/public/index_patterns/__tests__/_field_format.js +++ b/src/ui/public/index_patterns/__tests__/_field_format.js @@ -85,8 +85,8 @@ describe('FieldFormat class', function () { var text = f.getConverterFor('text'); var html = f.getConverterFor('html'); expect(text).to.not.be(html); - expect(text()).to.be('formatted'); - expect(html()).to.be('formatted'); + expect(text('formatted')).to.be('formatted'); + expect(html('formatted')).to.be('formatted'); }); it('can be an object, with seperate text and html converter', function () { @@ -99,8 +99,8 @@ describe('FieldFormat class', function () { var text = f.getConverterFor('text'); var html = f.getConverterFor('html'); expect(text).to.not.be(html); - expect(text()).to.be('formatted text'); - expect(html()).to.be('formatted html'); + expect(text('formatted text')).to.be('formatted text'); + expect(html('formatted html')).to.be('formatted html'); }); it('does not escape the output of the text converter', function () { @@ -147,6 +147,11 @@ describe('FieldFormat class', function () { var f = new TestFormat(); expect(f.convert('val', 'html')).to.be('html'); }); + + it('formats a value as " - " when no value is specified', function () { + var f = new TestFormat(); + expect(f.convert()).to.be(' - '); + }); }); }); diff --git a/src/ui/public/index_patterns/_field_format/content_types.js b/src/ui/public/index_patterns/_field_format/content_types.js index 8defa4b03d13..a49f1d3ad674 100644 --- a/src/ui/public/index_patterns/_field_format/content_types.js +++ b/src/ui/public/index_patterns/_field_format/content_types.js @@ -6,6 +6,10 @@ export default function contentTypesProvider(highlightFilter) { var types = { html: function (format, convert) { return function recurse(value, field, hit) { + if (value == null) { + return _.asPrettyString(value); + } + if (!value || typeof value.map !== 'function') { return convert.call(format, value, field, hit); } diff --git a/src/ui/public/notify/__tests__/notifier.js b/src/ui/public/notify/__tests__/notifier.js index ed2df33aeede..a7d767e979d4 100644 --- a/src/ui/public/notify/__tests__/notifier.js +++ b/src/ui/public/notify/__tests__/notifier.js @@ -43,8 +43,8 @@ describe('Notifier', function () { expect(notify('error').title).to.equal('Error'); }); - it('sets lifetime to Infinity', function () { - expect(notify('error').lifetime).to.equal(Infinity); + it('sets lifetime to 5 minutes', function () { + expect(notify('error').lifetime).to.equal(300000); }); it('allows reporting', function () { diff --git a/src/ui/public/notify/notifier.js b/src/ui/public/notify/notifier.js index 332961e94e41..e2ff33b3901e 100644 --- a/src/ui/public/notify/notifier.js +++ b/src/ui/public/notify/notifier.js @@ -229,7 +229,7 @@ Notifier.prototype.error = function (err, cb) { content: formatMsg(err, this.from), icon: 'warning', title: 'Error', - lifetime: Infinity, + lifetime: 300000, actions: ['report', 'accept'], stack: formatStack(err) }, cb); diff --git a/src/ui/public/stringify/__tests__/_string.js b/src/ui/public/stringify/__tests__/_string.js index 46390667ccbd..7a04c3ddfb07 100644 --- a/src/ui/public/stringify/__tests__/_string.js +++ b/src/ui/public/stringify/__tests__/_string.js @@ -17,4 +17,14 @@ describe('String Format', function () { expect(string.convert('Zm9vYmFy')).to.be('foobar'); }); + it('convert a string to title case', function () { + var StringFormat = fieldFormats.getType('string'); + var string = new StringFormat({ + transform: 'title' + }); + expect(string.convert('PLEASE DO NOT SHOUT')).to.be('Please Do Not Shout'); + expect(string.convert('Mean, variance and standard_deviation.')).to.be('Mean, Variance And Standard_deviation.'); + expect(string.convert('Stay CALM!')).to.be('Stay Calm!'); + }); + }); diff --git a/src/ui/public/stringify/types/string.js b/src/ui/public/stringify/types/string.js index 9545984b3ad7..7b0a4583cb2e 100644 --- a/src/ui/public/stringify/types/string.js +++ b/src/ui/public/stringify/types/string.js @@ -36,12 +36,14 @@ export default function StringFormatProvider(Private) { { id: false, name: '- none -' }, { id: 'lower', name: 'Lower Case' }, { id: 'upper', name: 'Upper Case' }, + { id: 'title', name: 'Title Case' }, { id: 'short', name: 'Short Dots' }, { id: 'base64', name: 'Base64 Decode'} ]; _String.sampleInputs = [ 'A Quick Brown Fox.', + 'STAY CALM!', 'com.organizations.project.ClassName', 'hostname.net', 'SGVsbG8gd29ybGQ=' @@ -55,10 +57,15 @@ export default function StringFormatProvider(Private) { } }; + _String.prototype._toTitleCase = function (val) { + return val.replace(/\w\S*/g, txt => { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); }); + }; + _String.prototype._convert = function (val) { switch (this.param('transform')) { case 'lower': return String(val).toLowerCase(); case 'upper': return String(val).toUpperCase(); + case 'title': return this._toTitleCase(val); case 'short': return _.shortenDottedString(val); case 'base64': return this._base64Decode(val); default: return _.asPrettyString(val); diff --git a/src/ui/public/styles/base.less b/src/ui/public/styles/base.less index 0f71e27f7f14..2c7766d9bbd5 100644 --- a/src/ui/public/styles/base.less +++ b/src/ui/public/styles/base.less @@ -142,9 +142,15 @@ a { .kibana-nav-options { padding-bottom: 0; + padding-right: 0px; + .kibana-nav-actions { margin-left: auto; line-height: 20px; + + .button-group > :last-child { + border-radius: 0; + } } .kibana-nav-info { line-height: 30px; diff --git a/src/ui/ui_exports.js b/src/ui/ui_exports.js index 0f9ac3e06cf1..e76dcd1a69e6 100644 --- a/src/ui/ui_exports.js +++ b/src/ui/ui_exports.js @@ -13,6 +13,7 @@ class UiExports { this.exportConsumer = _.memoize(this.exportConsumer); this.consumers = []; this.bundleProviders = []; + this.defaultInjectedVars = {}; } consumePlugin(plugin) { @@ -52,7 +53,17 @@ class UiExports { return (plugin, specs) => { const id = plugin.id; for (let spec of [].concat(specs || [])) { - const app = this.apps.new({ id, ...spec }); + + let app = this.apps.new(_.defaults({}, spec, { + id: plugin.id, + urlBasePath: this.urlBasePath + })); + + plugin.extendInit((server, options) => { // eslint-disable-line no-loop-func + const wrapped = app.getInjectedVars; + app.getInjectedVars = () => wrapped.call(plugin, server, options); + }); + plugin.apps.add(app); } }; @@ -88,6 +99,13 @@ class UiExports { this.aliases[adhocType] = _.union(this.aliases[adhocType] || [], spec); }); }; + + case 'injectDefaultVars': + return (plugin, injector) => { + plugin.extendInit(async (server, options) => { + _.merge(this.defaultInjectedVars, await injector.call(plugin, server, options)); + }); + }; } } diff --git a/tasks/build/index.js b/tasks/build/index.js index 0f7c4d00de24..6a5ea09914a9 100644 --- a/tasks/build/index.js +++ b/tasks/build/index.js @@ -22,6 +22,7 @@ module.exports = function (grunt) { 'stop:optimizeBuild', '_build:downloadNodeBuilds:finish', '_build:versionedLinks', + '_build:osShellScripts', '_build:archives', grunt.option('os-packages') ? [ '_build:pleaseRun', diff --git a/tasks/build/os_shell_scripts.js b/tasks/build/os_shell_scripts.js new file mode 100644 index 000000000000..60817935387d --- /dev/null +++ b/tasks/build/os_shell_scripts.js @@ -0,0 +1,42 @@ +import {join, extname} from 'path'; +import {promisify} from 'bluebird'; +import {ncp} from 'ncp'; +import rimraf from 'rimraf'; +const pncp = promisify(ncp); +const primraf = promisify(rimraf); + +export default function (grunt) { + grunt.registerTask('_build:osShellScripts', async function osShellScripts() { + const done = this.async(); + const source = 'build/kibana/bin'; + const platforms = grunt.config.get('platforms'); + const allPlatforms = fn => invokeAllAsync(platforms, fn); + + try { + await allPlatforms(platform => primraf(join(platform.buildDir, 'bin'))); + await allPlatforms(platform => pncp(source, join(platform.buildDir, 'bin'))); + await allPlatforms(platform => removeExtraneousShellScripts(grunt, platform)); + done(); + } catch (err) { + done(err); + } + }); +}; + +function invokeAllAsync(all, fn) { + return Promise.all(all.map(fn)); +} + +function removeExtraneousShellScripts(grunt, platform) { + return Promise.all(grunt.file + .expand(join(platform.buildDir, 'bin', '*')) + .filter(file => isExtraneous(platform, file)) + .map(file => primraf(file))); +} + +function isExtraneous(platform, file) { + const ext = extname(file); + if (platform.win && ext === '') { return true; } + if (!platform.win && ext === '.bat') { return true; } + return false; +} diff --git a/tasks/config/downloadSelenium.js b/tasks/config/downloadSelenium.js index d69fa8d40627..132f44fafb39 100644 --- a/tasks/config/downloadSelenium.js +++ b/tasks/config/downloadSelenium.js @@ -1,14 +1,18 @@ var path = require('path'); +import { resolve as resolveUrl } from 'url'; +const URL = 'https://selenium-release.storage.googleapis.com/2.48/selenium-server-standalone-2.48.2.jar'; +const DIR = resolveUrl(URL, './'); +const FILE = URL.replace(DIR, ''); module.exports = function (grunt) { return { options: { selenium: { - filename: 'selenium-server-standalone-2.48.2.jar', - server: 'https://selenium-release.storage.googleapis.com/2.48/', + filename: FILE, + server: DIR, md5: 'b2784fc67c149d3c13c83d2108104689', - directory: path.join(grunt.config.get('root'), 'selenium') + path: path.join(grunt.config.get('root'), 'selenium', FILE) } } }; diff --git a/tasks/config/run.js b/tasks/config/run.js index 36f166b5477b..e98bfe894e72 100644 --- a/tasks/config/run.js +++ b/tasks/config/run.js @@ -122,7 +122,7 @@ module.exports = function (grunt) { cmd: 'java', args: [ '-jar', - 'selenium/selenium-server-standalone-2.48.2.jar', + '<%= downloadSelenium.options.selenium.path %>', '-port', uiConfig.servers.webdriver.port, ] @@ -138,7 +138,7 @@ module.exports = function (grunt) { cmd: 'java', args: [ '-jar', - 'selenium/selenium-server-standalone-2.48.2.jar', + '<%= downloadSelenium.options.selenium.path %>', '-port', uiConfig.servers.webdriver.port, ] diff --git a/tasks/download_selenium.js b/tasks/download_selenium.js index 62840493ee2a..cdce3e0ade21 100644 --- a/tasks/download_selenium.js +++ b/tasks/download_selenium.js @@ -11,8 +11,9 @@ module.exports = function (grunt) { const done = this.async(); const config = this.options(); - const SELENIUM_FILE_PATH = path.join(config.selenium.directory, config.selenium.filename); - const SELENIUM_DOWNLOAD_URL = config.selenium.server + config.selenium.filename; + const FILE = config.selenium.path; + const DIR = path.dirname(config.selenium.path); + const URL = config.selenium.server + config.selenium.filename; function validateDownload(path, expectedHash, success) { grunt.log.write('Validating hash...'); @@ -28,27 +29,27 @@ module.exports = function (grunt) { } function downloadSelenium(success) { - grunt.log.write(`Downloading ${SELENIUM_DOWNLOAD_URL}...`); - request.get(SELENIUM_DOWNLOAD_URL) - .pipe(fs.createWriteStream(SELENIUM_FILE_PATH)) + grunt.log.write(`Downloading ${URL}...`); + request.get(URL) + .pipe(fs.createWriteStream(FILE)) .on('error', function downloadError(err) { grunt.fail.warn(err); }) .on('finish', function downloadFinish() { grunt.log.writeln('done'); - validateDownload(SELENIUM_FILE_PATH, config.selenium.md5, success); + validateDownload(FILE, config.selenium.md5, success); }); } function start() { try { - fs.mkdirSync(config.selenium.directory); + fs.mkdirSync(DIR); } catch (err) { if (err && err.code !== 'EEXIST') grunt.fail.warn(err); } - if (fs.existsSync(SELENIUM_FILE_PATH)) { - validateDownload(SELENIUM_FILE_PATH, config.selenium.md5, done); + if (fs.existsSync(FILE)) { + validateDownload(FILE, config.selenium.md5, done); } else { downloadSelenium(done); } diff --git a/tasks/jenkins.js b/tasks/jenkins.js index b1601ff0fbb3..77a2673cffb7 100644 --- a/tasks/jenkins.js +++ b/tasks/jenkins.js @@ -1,7 +1,18 @@ +import { compact } from 'lodash'; +import { delimiter } from 'path'; + module.exports = function (grunt) { - let { compact } = require('lodash'); grunt.registerTask('jenkins', 'Jenkins build script', function () { - process.env.JAVA_HOME = '/usr/lib/jvm/jdk8'; + // make sure JAVA_HOME points to JDK8 + const HOME = '/usr/lib/jvm/jdk8'; + process.env.JAVA_HOME = HOME; + + // extend PATH to point to JDK8 + const path = process.env.PATH.split(delimiter); + path.unshift(`${HOME}/bin`); + process.env.PATH = path.join(delimiter); + + // always build os packages on jenkins grunt.option('os-packages', true); grunt.task.run(compact([ diff --git a/test/functional/apps/discover/_field_data.js b/test/functional/apps/discover/_field_data.js index e49496ac0ed4..7e90ec9b2e79 100644 --- a/test/functional/apps/discover/_field_data.js +++ b/test/functional/apps/discover/_field_data.js @@ -141,7 +141,7 @@ define(function (require) { + ' Here’s...", "twitter:card": "summary", "twitter:image": "' + 'http://IMAGES1.laweekly.com/imager/the-rapture-at-the-mayan-7-25/u/original/2463272/rapturetn05.jpg",' + ' "twitter:site": "@laweekly" } machine.os:win 7 machine.ram:7,516,192,768 _id:AU_x3_g4GFA8no6QjkYX' - + ' _type:apache _index:logstash-2015.09.22 _score: relatedContent.article:modified_time:November 27th' + + ' _type:apache _index:logstash-2015.09.22 _score: - relatedContent.article:modified_time:November 27th' + ' 2014, 16:00:51.000, November 27th 2014, 16:28:42.000 relatedContent.article:published_time:July 26th' + ' 2007, 19:42:30.000, December 13th 2007, 20:19:35.000'; return discoverPage.getDocTableIndex(1) @@ -219,7 +219,7 @@ define(function (require) { + ' "Arts", "og:site_name": "LA Weekly", "twitter:title": "Shopping Daze", "twitter:description": "LA' + ' Weekly is the definitive source of information for news, music, movies, restaurants, reviews, and' + ' events in Los Angeles.", "twitter:card": "summary", "twitter:site": "@laweekly" } machine.os:osx' - + ' machine.ram:15,032,385,536 _id:AU_x3_g3GFA8no6QjkFm _type:apache _index:logstash-2015.09.20 _score:' + + ' machine.ram:15,032,385,536 _id:AU_x3_g3GFA8no6QjkFm _type:apache _index:logstash-2015.09.20 _score: -' + ' relatedContent.article:modified_time:October 28th 2014, 22:00:08.000, November 26th 2014,' + ' 01:05:47.000, November 26th 2014, 03:52:35.000, November 26th 2014, 04:15:21.000, November 27th 2014,' + ' 16:01:03.000 relatedContent.article:published_time:October 21st 2005, 01:10:25.000, March 5th 2006,' diff --git a/test/functional/apps/visualize/_area_chart.js b/test/functional/apps/visualize/_area_chart.js index a456dfec3ed7..b7c5617f406c 100644 --- a/test/functional/apps/visualize/_area_chart.js +++ b/test/functional/apps/visualize/_area_chart.js @@ -150,7 +150,7 @@ define(function (require) { expect(labels).to.eql(yAxisLabels); }) .then(function getAreaChartData() { - return visualizePage.getAreaChartData(); + return visualizePage.getAreaChartData('Count'); }) .then(function (paths) { common.debug('expectedAreaChartData = ' + expectedAreaChartData); diff --git a/test/functional/apps/visualize/_line_chart.js b/test/functional/apps/visualize/_line_chart.js index 539758e69bb6..823a34acc51f 100644 --- a/test/functional/apps/visualize/_line_chart.js +++ b/test/functional/apps/visualize/_line_chart.js @@ -122,7 +122,7 @@ define(function (require) { // sleep a bit before trying to get the chart data return common.sleep(3000) .then(function () { - return visualizePage.getLineChartData() + return visualizePage.getLineChartData('fill="#57c17b"') .then(function showData(data) { var tolerance = 10; // the y-axis scale is 10000 so 10 is 0.1% for (var x = 0; x < data.length; x++) { diff --git a/test/support/pages/visualize_page.js b/test/support/pages/visualize_page.js index cffcd7e79731..b19d938d4d18 100644 --- a/test/support/pages/visualize_page.js +++ b/test/support/pages/visualize_page.js @@ -326,19 +326,53 @@ define(function (require) { }); }, - // saved visualizations are paginated 5 to a page! - loadSavedVisualization: function loadSavedVisualization(vizName) { + clickLoadSavedVisButton: function clickLoadSavedVisButton() { + return this.remote + .setFindTimeout(defaultTimeout) + .findDisplayedByCssSelector('button[aria-label="Load Saved Visualization"]') + .click(); + }, + + filterVisByName: function filterVisByName(vizName) { + return this.remote + .findByCssSelector('input[name="filter"]') + .click() + // can't uses dashes in saved visualizations when filtering + // or extended character sets + // https://github.com/elastic/kibana/issues/6300 + .type(vizName.replace('-',' ')); + }, + + clickVisualizationByLinkText: function clickVisualizationByLinkText(vizName) { var self = this; + common.debug('clickVisualizationByLinkText(' + vizName + ')'); + return this.remote - .setFindTimeout(defaultTimeout) - .findByCssSelector('button[aria-label="Load Saved Visualization"]') - .click() - .then(function findVizByLinkedText() { - common.debug('Load Saved Vis button clicked'); - return self.remote .setFindTimeout(defaultTimeout) .findByLinkText(vizName) .click(); + }, + + // this starts by clicking the Load Saved Viz button, not from the + // bottom half of the "Create a new visualization Step 1" page + loadSavedVisualization: function loadSavedVisualization(vizName) { + var self = this; + return this.clickLoadSavedVisButton() + .then(function filterVisualization() { + return self.openSavedVisualization(vizName); + }); + }, + + // this is for starting on the + // bottom half of the "Create a new visualization Step 1" page + openSavedVisualization: function openSavedVisualization(vizName) { + var self = this; + return self.filterVisByName(vizName) + .then(function () { + return common.sleep(1000); + }) + .then(function clickDashboardByLinkedText() { + return self.clickVisualizationByLinkText(vizName); }); }, @@ -384,7 +418,7 @@ define(function (require) { ** This method gets the chart data and scales it based on chart height and label. ** Returns an array of height values */ - getAreaChartData: function getAreaChartData() { + getAreaChartData: function getAreaChartData(aggregateName) { var self = this.remote; var chartData = []; @@ -399,11 +433,11 @@ define(function (require) { return this.remote .setFindTimeout(defaultTimeout) .findByCssSelector('div.y-axis-div-wrapper > div > svg > g > g:last-of-type') - .then(function setYAxisLabel(y) { - return y.getVisibleText(); - }) + .getVisibleText() .then(function (yLabel) { - yAxisLabel = yLabel.replace(',', ''); + // since we're going to use the y-axis 'last' (top) label as a number to + // scale the chart pixel data, we need to clean out commas and % marks. + yAxisLabel = yLabel.replace(/(%|,)/g, ''); common.debug('yAxisLabel = ' + yAxisLabel); return yLabel; }) @@ -411,10 +445,8 @@ define(function (require) { .then(function () { return self .setFindTimeout(defaultTimeout) - .findByCssSelector('rect.background'); // different here - }) - .then(function (chartAreaObj) { - return chartAreaObj.getAttribute('height'); + .findByCssSelector('rect.background') // different here + .getAttribute('height'); }) .then(function (chartH) { yAxisHeight = chartH; @@ -422,43 +454,29 @@ define(function (require) { }) .then(function () { return self.setFindTimeout(defaultTimeout * 2) - .findAllByCssSelector('path') - .then(function (chartTypes) { - - function getChartType(chart) { - return chart - .getAttribute('data-label') - .then(function (chartString) { - //common.debug('data-label = ' + chartString); - if (chartString === 'Count') { - return chart.getAttribute('d') - .then(function (data) { - common.debug(data); - tempArray = data.split('L'); - chartSections = tempArray.length / 2; - common.debug('chartSections = ' + chartSections + ' height = ' + yAxisHeight + ' yAxisLabel = ' + yAxisLabel); - chartData[0] = Math.round((yAxisHeight - tempArray[0].split(',')[1]) / yAxisHeight * yAxisLabel); - common.debug('chartData[0] =' + chartData[0]); - for (var i = 1; i < chartSections; i++) { - chartData[i] = Math.round((yAxisHeight - tempArray[i].split(',')[1]) / yAxisHeight * yAxisLabel); - common.debug('chartData[i] =' + chartData[i]); - } - return chartData; - }); - } - }); - } - var getChartTypesPromises = chartTypes.map(getChartType); - return Promise.all(getChartTypesPromises); - }); + .findByCssSelector('path[data-label="' + aggregateName + '"]') + .getAttribute('d'); }) - .then(function (chartData) { - return chartData[1]; // MAGIC NUMBER - we find multiple 'path's and only one of them is the right one. + .then(function (data) { + common.debug(data); + // This area chart data starts with a 'M'ove to a x,y location, followed + // by a bunch of 'L'ines from that point to the next. Those points are + // the values we're going to use to calculate the data values we're testing. + // So git rid of the one 'M' and split the rest on the 'L's. + tempArray = data.replace('M','').split('L'); + chartSections = tempArray.length / 2; + common.debug('chartSections = ' + chartSections + ' height = ' + yAxisHeight + ' yAxisLabel = ' + yAxisLabel); + for (var i = 0; i < chartSections; i++) { + chartData[i] = Math.round((yAxisHeight - tempArray[i].split(',')[1]) / yAxisHeight * yAxisLabel); + common.debug('chartData[i] =' + chartData[i]); + } + return chartData; }); }, + // The current test shows dots, not a line. This function gets the dots and normalizes their height. - getLineChartData: function getLineChartData() { + getLineChartData: function getLineChartData(cssPart) { var self = this.remote; var yAxisLabel = 0; var yAxisHeight; @@ -467,10 +485,7 @@ define(function (require) { return this.remote .setFindTimeout(defaultTimeout) .findByCssSelector('div.y-axis-div-wrapper > div > svg > g > g:last-of-type') - .then(function setYAxisLabel(y) { - return y - .getVisibleText(); - }) + .getVisibleText() .then(function (yLabel) { yAxisLabel = yLabel.replace(',', ''); common.debug('yAxisLabel = ' + yAxisLabel); @@ -481,10 +496,7 @@ define(function (require) { return self .setFindTimeout(defaultTimeout) .findByCssSelector('clipPath rect') - .then(function getRectHeight(chartAreaObj) { - return chartAreaObj - .getAttribute('height'); - }) + .getAttribute('height') .then(function (theHeight) { yAxisHeight = theHeight - 5; // MAGIC NUMBER - clipPath extends a bit above the top of the y-axis and below x-axis common.debug('theHeight = ' + theHeight); @@ -635,6 +647,13 @@ define(function (require) { .getVisibleText(); }, + getMarkdownData: function getMarkdownData() { + return this.remote + .setFindTimeout(defaultTimeout) + .findByCssSelector('visualize.ng-isolate-scope') + .getVisibleText(); + }, + clickColumns: function clickColumns() { return this.remote .setFindTimeout(defaultTimeout) diff --git a/test/unit/api/ingest/_post.js b/test/unit/api/ingest/_post.js index d2e986ca176a..06df94ec319a 100644 --- a/test/unit/api/ingest/_post.js +++ b/test/unit/api/ingest/_post.js @@ -12,7 +12,12 @@ define(function (require) { }); bdd.afterEach(function () { - return request.del('/kibana/ingest/logstash-*'); + return request.del('/kibana/ingest/logstash-*') + .then(function () { + return scenarioManager.client.indices.delete({ + index: 'logstash-*' + }); + }); }); bdd.it('should return 400 for an invalid payload', function invalidPayload() { @@ -57,6 +62,29 @@ define(function (require) { }); }); + bdd.it('should successfully create new indices based on the template', function newIndices() { + return request.post('/kibana/ingest') + .send(createTestData()) + .expect(204) + .then(function () { + return scenarioManager.client.create({ + index: 'logstash-1', + type: 'foo', + id: '1', + body: { + ip: '192.168.1.1', + '@timestamp': '2015-09-20T10:28:22.684Z', + agent: 'Jack', + bytes: 9001, + geo: {coordinates: {lat: 43.07260861, lon: -92.61077833}} + } + }) + .then(function (response) { + expect(response.created).to.be.ok(); + }); + }); + }); + bdd.it('should provide defaults for field properties', function createTemplate() { return request.post('/kibana/ingest') .send(createTestData()) @@ -92,15 +120,15 @@ define(function (require) { .then(function (template) { var mappings = template['kibana-logstash-*'].mappings._default_.properties; expect(mappings).to.be.ok(); - expect(_.isEqual(mappings.ip, {index: 'not_analyzed', type: 'ip', doc_values: true})).to.be.ok(); - expect(_.isEqual(mappings['@timestamp'], {index: 'not_analyzed', type: 'date', doc_values: true})).to.be.ok(); - expect(_.isEqual(mappings.bytes, {index: 'not_analyzed', type: 'double', doc_values: true})).to.be.ok(); + expect(_.isEqual(mappings.ip, {index: true, type: 'ip', doc_values: true})).to.be.ok(); + expect(_.isEqual(mappings['@timestamp'], {index: true, type: 'date', doc_values: true})).to.be.ok(); + expect(_.isEqual(mappings.bytes, {index: true, type: 'double', doc_values: true})).to.be.ok(); // object fields are mapped as such, with individual mappings for each of their properties expect(_.isEqual(mappings.geo, { properties: { coordinates: { - index: 'not_analyzed', + index: true, type: 'geo_point', doc_values: true } diff --git a/test/utils/kbn_server.js b/test/utils/kbn_server.js index 6f854283e8b0..52e243142465 100644 --- a/test/utils/kbn_server.js +++ b/test/utils/kbn_server.js @@ -3,7 +3,6 @@ import { defaultsDeep, set } from 'lodash'; import { header as basicAuthHeader } from './base_auth'; import { kibanaUser, kibanaServer } from '../shield'; import KbnServer from '../../src/server/kbn_server'; -import fromRoot from '../../src/utils/from_root'; import serverConfig from '../server_config'; const SERVER_DEFAULTS = { @@ -16,11 +15,7 @@ const SERVER_DEFAULTS = { logging: { quiet: true }, - plugins: { - scanDirs: [ - fromRoot('src/plugins') - ] - }, + plugins: {}, optimize: { enabled: false },