Merge branch '4.2' of github.com:elastic/kibana into 4.2

This commit is contained in:
Rashid Khan 2015-11-17 09:11:18 -07:00
commit 60cc2393fb
52 changed files with 1883 additions and 154 deletions

2
.gitignore vendored
View file

@ -10,6 +10,7 @@ target
.idea
*.iml
*.log
/test/output
/esvm
.htpasswd
installedPlugins
@ -18,3 +19,4 @@ webpackstats.json
config/kibana.dev.yml
coverage
selenium
.babelcache.json

View file

@ -102,6 +102,41 @@ The standard `npm run test` task runs several sub tasks and can take several min
</dd>
</dl>
### Functional UI Testing
#### Handy references
- https://theintern.github.io/
- https://theintern.github.io/leadfoot/Element.html
#### Running tests using npm task:
*The Selenium server that is started currently only runs the tests in Firefox*
To runt the functional UI tests, execute the following command:
`npm run test:ui`
The task above takes a little time to start the servers. You can also start the servers and leave them running, and then run the tests separately:
`npm run test:ui:server` will start the server required to run the selenium tests, leave this open
`npm run test:ui:runner` will run the frontend tests and close when complete
#### Running tests locally with your existing (and already running) ElasticSearch, Kibana, and Selenium Server:
Set your es and kibana ports in `test/intern.js` to 9220 and 5620, respecitively. You can configure your Selenium server to run the tests on Chrome,IE, or other browsers here.
Once you've got the services running, execute the following:
`npm run test:ui:runner`
#### General notes:
- Using Page Objects pattern (https://theintern.github.io/intern/#writing-functional-test)
- At least the initial tests for the Settings, Discover, and Visualize tabs all depend on a very specific set of logstash-type data (generated with makelogs). Since that is a static set of data, all the Discover and Visualize tests use a specific Absolute time range. This gaurantees the same results each run.
- 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.
### Submit a pull request

View file

@ -43,6 +43,7 @@ module.exports = function (grunt) {
lintThese: [
'Gruntfile.js',
'<%= root %>/tasks/**/*.js',
'<%= root %>/test/**/*.js',
'<%= src %>/**/*.js',
'!<%= src %>/fixtures/**/*.js'
],

View file

@ -1,4 +1,4 @@
# Kibana 4.2.1-snapshot
# Kibana 4.2.1
[![Build Status](https://travis-ci.org/elastic/kibana.svg?branch=master)](https://travis-ci.org/elastic/kibana?branch=master)
@ -39,7 +39,7 @@ For the daring, snapshot builds are available. These builds are created after ea
| platform | | |
| --- | --- | --- |
| OSX | [tar](http://download.elastic.co/kibana/kibana-snapshot/kibana-4.2.1-snapshot-darwin-x64.tar.gz) | [zip](http://download.elastic.co/kibana/kibana-snapshot/kibana-4.2.1-snapshot-darwin-x64.zip) |
| Linux x64 | [tar](http://download.elastic.co/kibana/kibana-snapshot/kibana-4.2.1-snapshot-linux-x64.tar.gz) | [zip](http://download.elastic.co/kibana/kibana-snapshot/kibana-4.2.1-snapshot-linux-x64.zip) |
| Linux x86 | [tar](http://download.elastic.co/kibana/kibana-snapshot/kibana-4.2.1-snapshot-linux-x86.tar.gz) | [zip](http://download.elastic.co/kibana/kibana-snapshot/kibana-4.2.1-snapshot-linux-x86.zip) |
| Windows | [tar](http://download.elastic.co/kibana/kibana-snapshot/kibana-4.2.1-snapshot-windows.tar.gz) | [zip](http://download.elastic.co/kibana/kibana-snapshot/kibana-4.2.1-snapshot-windows.zip) |
| OSX | [tar](http://download.elastic.co/kibana/kibana-snapshot/kibana-4.2.1-darwin-x64.tar.gz) | [zip](http://download.elastic.co/kibana/kibana-snapshot/kibana-4.2.1-darwin-x64.zip) |
| Linux x64 | [tar](http://download.elastic.co/kibana/kibana-snapshot/kibana-4.2.1-linux-x64.tar.gz) | [zip](http://download.elastic.co/kibana/kibana-snapshot/kibana-4.2.1-linux-x64.zip) |
| Linux x86 | [tar](http://download.elastic.co/kibana/kibana-snapshot/kibana-4.2.1-linux-x86.tar.gz) | [zip](http://download.elastic.co/kibana/kibana-snapshot/kibana-4.2.1-linux-x86.zip) |
| Windows | [tar](http://download.elastic.co/kibana/kibana-snapshot/kibana-4.2.1-windows.tar.gz) | [zip](http://download.elastic.co/kibana/kibana-snapshot/kibana-4.2.1-windows.zip) |

View file

@ -4,6 +4,10 @@
# The host to bind the server to.
# server.host: "0.0.0.0"
# A value to use as a XSRF token. This token is sent back to the server on each request
# and required if you want to execute requests from other clients (like curl).
# server.xsrf.token: ""
# The Elasticsearch instance to use for all your queries.
# elasticsearch.url: "http://localhost:9200"

View file

@ -102,8 +102,6 @@ Move the cursor to the bottom right corner of the container until the cursor cha
cursor changes, click and drag the corner of the container to change the container's size. Release the mouse button to
confirm the new container size.
// enhancement request: a way to specify specific dimensions for a container in pixels, or at least display that info?
[float]
[[removing-containers]]
==== Removing Containers

View file

@ -30,7 +30,7 @@ The tutorials in this section rely on the following data sets:
* A set of fictitious accounts with randomly generated data. Download this data set by clicking here:
https://github.com/bly2k/files/blob/master/accounts.zip?raw=true[accounts.zip]
* A set of randomly generated log files. Download this data set by clicking here:
https://download.elastic.co/demos/kibana/gettingstarted/logs.jsonl.gz[logstash.jsonl.gz]
https://download.elastic.co/demos/kibana/gettingstarted/logs.jsonl.gz[logs.jsonl.gz]
Two of the data sets are compressed. Use the following commands to extract the files:
@ -103,15 +103,77 @@ This mapping specifies the following qualities for the data set:
* The _speaker_ field is a string that isn't analyzed. The string in this field is treated as a single unit, even if
there are multiple words in the field.
* The same applies to the _play_name_ field.
* The line_id and speech_number fields are integers.
* The _line_id_ and _speech_number_ fields are integers.
The accounts and logstash data sets don't require any mappings, so at this point we're ready to load the data sets into
Elasticsearch with the following commands:
The logs data set requires a mapping to label the latitude/longitude pairs in the logs as geographic locations by
applying the `geo_point` type to those fields.
Use the following commands to establish `geo_point` mapping for the logs:
[source,shell]
curl -XPOST 'localhost:9200/bank/_bulk?pretty' --data-binary @accounts.json
curl -XPUT http://localhost:9200/logstash-2015.05.18 -d '
{
"mappings": {
"log": {
"properties": {
"geo": {
"properties": {
"coordinates": {
"type": "geo_point"
}
}
}
}
}
}
}
';
[source,shell]
curl -XPUT http://localhost:9200/logstash-2015.05.19 -d '
{
"mappings": {
"log": {
"properties": {
"geo": {
"properties": {
"coordinates": {
"type": "geo_point"
}
}
}
}
}
}
}
';
[source,shell]
curl -XPUT http://localhost:9200/logstash-2015.05.20 -d '
{
"mappings": {
"log": {
"properties": {
"geo": {
"properties": {
"coordinates": {
"type": "geo_point"
}
}
}
}
}
}
}
';
The accounts data set doesn't require any mappings, so at this point we're ready to use the Elasticsearch
{ref}/docs-bulk.html[`bulk`] API to load the data sets with the following commands:
[source,shell]
curl -XPOST 'localhost:9200/bank/account/_bulk?pretty' --data-binary @accounts.json
curl -XPOST 'localhost:9200/shakespeare/_bulk?pretty' --data-binary @shakespeare.json
curl -XPOST 'localhost:9200/_bulk?pretty' --data-binary @logstash.json
curl -XPOST 'localhost:9200/_bulk?pretty' --data-binary @logs.jsonl
These commands may take some time to execute, depending on the computing resources available.
@ -133,16 +195,23 @@ yellow open logstash-2015.05.20 5 1 4750 0 16.4mb
[[tutorial-define-index]]
=== Defining Your Index Patterns
Each set of data loaded to Elasticsearch has an https://www.elastic.co/guide/en/kibana/current/settings.html#settings-create-pattern[index pattern]. In the previous section, the Shakespeare data set has an index named `shakespeare`, and the accounts
Each set of data loaded to Elasticsearch has an
https://www.elastic.co/guide/en/kibana/current/settings.html#settings-create-pattern[index pattern]. In the previous
section, the Shakespeare data set has an index named `shakespeare`, and the accounts
data set has an index named `bank`. An _index pattern_ is a string with optional wildcards that can match multiple
indices. For example, in the common logging use case, a typical index name contains the date in MM-DD-YYYY
format, and an index pattern for May would look something like `logstash-2015.05*`.
For this tutorial, any pattern that matches either of the two indices we've loaded will work. Open a browser and
For this tutorial, any pattern that matches the name of an index we've loaded will work. Open a browser and
navigate to `localhost:5601`. Click the *Settings* tab, then the *Indices* tab. Click *Add New* to define a new index
pattern. Since these data sets don't contain time-series data, make sure the *Index contains time-based events* box is
unchecked. Specify `shakes*` as the index pattern for the Shakespeare data set and click *Create* to define the index
pattern, then define a second index pattern named `ba*`.
pattern. Two of the sample data sets, the Shakespeare plays and the financial accounts, don't contain time-series data.
Make sure the *Index contains time-based events* box is unchecked when you create index patterns for these data sets.
Specify `shakes*` as the index pattern for the Shakespeare data set and click *Create* to define the index pattern, then
define a second index pattern named `ba*`.
The Logstash data set does contain time-series data, so after clicking *Add New* to define the index for this data
set, make sure the *Index contains time-based events* box is checked and select the `@timestamp` field from the
*Time-field name* drop-down.
[float]
[[tutorial-discovering]]
@ -213,7 +282,7 @@ total number of ranges to six. Enter the following ranges:
15000 30999
31000 50000
Click the green *Apply changes* to display the chart:
Click the green *Apply changes* button image:images/apply-changes-button.png[] to display the chart:
image::images/tutorial-visualize-pie-2.png[]
@ -221,8 +290,8 @@ This shows you what proportion of the 1000 accounts fall in these balance ranges
we're going to add another bucket aggregation. We can break down each of the balance ranges further by the account
holder's age.
Click *Add sub-buckets* at the bottom, then select the *Terms* aggregation and the *age* field from the drop-downs.
Click the green *Apply changes* button to add an external ring with the new results.
Click *Add sub-buckets* at the bottom, then select *Split Slices*. Choose the *Terms* aggregation and the *age* field from the drop-downs.
Click the green *Apply changes* button image:images/apply-changes-button.png[] to add an external ring with the new results.
image::images/tutorial-visualize-pie-3.png[]
@ -237,9 +306,9 @@ image::images/tutorial-visualize-bar-1.png[]
For the Y-axis metrics aggregation, select *Unique Count*, with *speaker* as the field. For Shakespeare plays, it might
be useful to know which plays have the lowest number of distinct speaking parts, if your theater company is short on
actors. For the X-Axis buckets, select the *Terms* aggregation with the *play_name* field. For the *Order*, select
*Bottom*, leaving the *Size* at 5.
*Ascending*, leaving the *Size* at 5.
Leave the other elements at their default values and click the green *Apply changes* button. Your chart should now look
Leave the other elements at their default values and click the green *Apply changes* button image:images/apply-changes-button.png[]. Your chart should now look
like this:
image::images/tutorial-visualize-bar-2.png[]
@ -254,7 +323,7 @@ as well as change many other options for your visualizations, by clicking the *O
Now that you have a list of the smallest casts for Shakespeare plays, you might also be curious to see which of these
plays makes the greatest demands on an individual actor by showing the maximum number of speeches for a given part. Add
a Y-axis aggregation with the *Add metrics* button, then choose the *Max* aggregation for the *speech_number* field. In
the *Options* tab, change the *Bar Mode* drop-down to *grouped*, then click the green *Apply changes* button. Your
the *Options* tab, change the *Bar Mode* drop-down to *grouped*, then click the green *Apply changes* button image:images/apply-changes-button.png[]. Your
chart should now look like this:
image::images/tutorial-visualize-bar-3.png[]
@ -265,9 +334,9 @@ might therefore make more demands on an actor's memory.
Save this chart with the name _Bar Example_.
Next, we're going to make a tile map chart to visualize some geographic data. Click on *New Visualization*, then
*Tile map*. Select *From a new search* and the `logstash-*` index pattern. Define the time window for the events we're
exploring by clicking the time selector at the top right of the Kibana interface. Click on *Absolute*, then set the
end time for the range to May 20, 2015 and the start time to May 18, 2015:
*Tile map*. Select *From a new search* and the `logstash-*` index pattern. Define the time window for the events
we're exploring by clicking the time selector at the top right of the Kibana interface. Click on *Absolute*, then set
the start time to May 18, 2015 and the end time for the range to May 20, 2015:
image::images/tutorial-timepicker.png[]
@ -276,7 +345,7 @@ at the bottom. You'll see a map of the world, since we haven't defined any bucke
image::images/tutorial-visualize-map-1.png[]
Select *Geo Coordinates* as the bucket, then click the green *Apply changes* button. Your chart should now look like
Select *Geo Coordinates* as the bucket, then click the green *Apply changes* button image:images/apply-changes-button.png[]. Your chart should now look like
this:
image::images/tutorial-visualize-map-2.png[]
@ -304,7 +373,7 @@ Write the following text in the field:
The Markdown widget uses **markdown** syntax.
> Blockquotes in Markdown use the > character.
Click the green *Apply changes* button to display the rendered Markdown in the preview pane:
Click the green *Apply changes* button image:images/apply-changes-button.png[] to display the rendered Markdown in the preview pane:
image::images/tutorial-visualize-md-2.png[]

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 238 KiB

Before After
Before After

View file

@ -60,8 +60,8 @@ To encrypt communications between the browser and the Kibana server, you configu
[source,text]
----
# SSL for outgoing requests from the Kibana Server (PEM formatted)
ssl_key_file: /path/to/your/server.key
ssl_cert_file: /path/to/your/server.crt
server.ssl.key: /path/to/your/server.key
server.ssl.cert: /path/to/your/server.crt
----
If you are using Shield or a proxy that provides an HTTPS endpoint for Elasticsearch,

0
optimize/.empty Normal file
View file

View file

@ -11,7 +11,7 @@
"dashboarding"
],
"private": false,
"version": "4.2.1-snapshot",
"version": "4.2.1",
"build": {
"number": 8467,
"sha": "6cb7fec4e154faa0a4a3fee4b33dfef91b9870d9"
@ -167,7 +167,7 @@
"npm": "2.11.0",
"portscanner": "1.0.0",
"simple-git": "1.8.0",
"sinon": "1.16.1",
"sinon": "1.17.2",
"source-map": "0.4.4",
"wreck": "6.2.0"
},

View file

@ -1,2 +1,5 @@
require('babel/register')(require('../optimize/babelOptions').node);
// load the babel options seperately so that they can modify the process.env
// before calling babel/register
const babelOptions = require('../optimize/babelOptions').node;
require('babel/register')(babelOptions);
require('./cli');

View file

@ -1,4 +1,9 @@
var fromRoot = require('requirefrom')('src/utils')('fromRoot');
var cloneDeep = require('lodash').cloneDeep;
var fromRoot = require('path').resolve.bind(null, __dirname, '../../');
if (!process.env.BABEL_CACHE_PATH) {
process.env.BABEL_CACHE_PATH = fromRoot('optimize/.babelcache.json');
}
exports.webpack = {
stage: 1,
@ -6,7 +11,7 @@ exports.webpack = {
optional: ['runtime']
};
exports.node = Object.assign({
exports.node = cloneDeep({
ignore: [
fromRoot('src'),
/[\\\/](node_modules|bower_components)[\\\/]/

View file

@ -1,7 +1,14 @@
var cloneDeep = require('lodash').cloneDeep;
var fromRoot = require('path').resolve.bind(null, __dirname, '../../');
if (!process.env.BABEL_CACHE_PATH) {
process.env.BABEL_CACHE_PATH = fromRoot('optimize/.babelcache.json');
}
exports.webpack = {
stage: 1,
nonStandard: false,
optional: ['runtime']
};
exports.node = Object.assign({}, exports.webpack);
exports.node = cloneDeep(exports.webpack);

View file

@ -13,7 +13,12 @@ describe('plugins/elasticsearch', function () {
before(function () {
kbnServer = new KbnServer({
server: { autoListen: false },
server: {
autoListen: false,
xsrf: {
disableProtection: true
}
},
logging: { quiet: true },
plugins: {
scanDirs: [
@ -104,5 +109,3 @@ describe('plugins/elasticsearch', function () {
});
});

View file

@ -5,8 +5,9 @@ let path = require('path');
let utils = require('requirefrom')('src/utils');
let fromRoot = utils('fromRoot');
const randomBytes = require('crypto').randomBytes;
module.exports = Joi.object({
module.exports = () => Joi.object({
pkg: Joi.object({
version: Joi.string().default(Joi.ref('$version')),
buildNum: Joi.number().default(Joi.ref('$buildNum')),
@ -39,7 +40,11 @@ module.exports = Joi.object({
origin: ['*://localhost:9876'] // karma test server
}),
otherwise: Joi.boolean().default(false)
})
}),
xsrf: Joi.object({
token: Joi.string().default(randomBytes(32).toString('hex')),
disableProtection: Joi.boolean().default(false),
}).default(),
}).default(),
logging: Joi.object().keys({
@ -106,4 +111,3 @@ module.exports = Joi.object({
}).default()
}).default();

View file

@ -1,6 +1,6 @@
module.exports = function (kbnServer) {
let Config = require('./Config');
let schema = require('./schema');
let schema = require('./schema')();
kbnServer.config = new Config(schema, kbnServer.settings || {});
};

View file

@ -0,0 +1,145 @@
import expect from 'expect.js';
import { fromNode as fn } from 'bluebird';
import { resolve } from 'path';
import KbnServer from '../../KbnServer';
const nonDestructiveMethods = ['GET'];
const destructiveMethods = ['POST', 'PUT', 'DELETE'];
const src = resolve.bind(null, __dirname, '../../../../src');
describe('xsrf request filter', function () {
function inject(kbnServer, opts) {
return fn(cb => {
kbnServer.server.inject(opts, (resp) => {
cb(null, resp);
});
});
}
const makeServer = async function (token) {
const kbnServer = new KbnServer({
server: { autoListen: false, xsrf: { token } },
plugins: { scanDirs: [src('plugins')] },
logging: { quiet: true },
optimize: { enabled: false },
});
await kbnServer.ready();
kbnServer.server.route({
path: '/xsrf/test/route',
method: [...nonDestructiveMethods, ...destructiveMethods],
handler: function (req, reply) {
reply(null, 'ok');
}
});
return kbnServer;
};
describe('issuing tokens', function () {
const token = 'secur3';
let kbnServer;
beforeEach(async () => kbnServer = await makeServer(token));
afterEach(async () => await kbnServer.close());
it('sends a token when rendering an app', async function () {
var resp = await inject(kbnServer, {
method: 'GET',
url: '/app/kibana',
});
expect(resp.payload).to.contain(`"xsrfToken":"${token}"`);
});
});
context('without configured token', function () {
let kbnServer;
beforeEach(async () => kbnServer = await makeServer());
afterEach(async () => await kbnServer.close());
it('responds with a random token', async function () {
var resp = await inject(kbnServer, {
method: 'GET',
url: '/app/kibana',
});
expect(resp.payload).to.match(/"xsrfToken":".{64}"/);
});
});
context('with configured token', function () {
const token = 'mytoken';
let kbnServer;
beforeEach(async () => kbnServer = await makeServer(token));
afterEach(async () => await kbnServer.close());
for (const method of nonDestructiveMethods) {
context(`nonDestructiveMethod: ${method}`, function () { // eslint-disable-line no-loop-func
it('accepts requests without a token', async function () {
const resp = await inject(kbnServer, {
url: '/xsrf/test/route',
method: method
});
expect(resp.statusCode).to.be(200);
expect(resp.payload).to.be('ok');
});
it('ignores invalid tokens', async function () {
const resp = await inject(kbnServer, {
url: '/xsrf/test/route',
method: method,
headers: {
'kbn-xsrf-token': `invalid:${token}`,
},
});
expect(resp.statusCode).to.be(200);
expect(resp.headers).to.not.have.property('kbn-xsrf-token');
});
});
}
for (const method of destructiveMethods) {
context(`destructiveMethod: ${method}`, function () { // eslint-disable-line no-loop-func
it('accepts requests with the correct token', async function () {
const resp = await inject(kbnServer, {
url: '/xsrf/test/route',
method: method,
headers: {
'kbn-xsrf-token': token,
},
});
expect(resp.statusCode).to.be(200);
expect(resp.payload).to.be('ok');
});
it('rejects requests without a token', async function () {
const resp = await inject(kbnServer, {
url: '/xsrf/test/route',
method: method
});
expect(resp.statusCode).to.be(403);
expect(resp.payload).to.match(/"Missing XSRF token"/);
});
it('rejects requests with an invalid token', async function () {
const resp = await inject(kbnServer, {
url: '/xsrf/test/route',
method: method,
headers: {
'kbn-xsrf-token': `invalid:${token}`,
},
});
expect(resp.statusCode).to.be(403);
expect(resp.payload).to.match(/"Invalid XSRF token"/);
});
});
}
});
});

View file

@ -119,4 +119,6 @@ module.exports = function (kbnServer, server, config) {
.permanent(true);
}
});
return kbnServer.mixin(require('./xsrf'));
};

20
src/server/http/xsrf.js Normal file
View file

@ -0,0 +1,20 @@
import { forbidden } from 'boom';
export default function (kbnServer, server, config) {
const token = config.get('server.xsrf.token');
const disabled = config.get('server.xsrf.disableProtection');
server.decorate('reply', 'issueXsrfToken', function () {
return token;
});
server.ext('onPostAuth', function (req, reply) {
if (disabled || req.method === 'get') return reply.continue();
const attempt = req.headers['kbn-xsrf-token'];
if (!attempt) return reply(forbidden('Missing XSRF token'));
if (attempt !== token) return reply(forbidden('Invalid XSRF token'));
return reply.continue();
});
}

View file

@ -21,12 +21,13 @@ module.exports = class KbnLogger {
}
init(readstream, emitter, callback) {
readstream
.pipe(this.squeeze)
.pipe(this.format)
.pipe(this.dest);
emitter.on('stop', _.noop);
this.output = readstream.pipe(this.squeeze).pipe(this.format);
this.output.pipe(this.dest);
emitter.on('stop', () => {
this.output.unpipe(this.dest);
});
callback();
}

View file

@ -19,7 +19,7 @@ module.exports = async function (kbnServer, server, config) {
let path = [];
async function initialize(id) {
const initialize = async function (id) {
let plugin = plugins.byId[id];
if (includes(path, id)) {

View file

@ -64,13 +64,14 @@ module.exports = async (kbnServer, server, config) => {
}
server.decorate('reply', 'renderApp', function (app) {
let payload = {
const payload = {
app: app,
nav: uiExports.apps,
version: kbnServer.version,
buildNum: config.get('pkg.buildNum'),
buildSha: config.get('pkg.buildSha'),
vars: defaults(app.getInjectedVars(), defaultInjectedVars),
xsrfToken: this.issueXsrfToken(),
};
return this.view(app.templateName, {

View file

@ -0,0 +1,132 @@
import $ from 'jquery';
import expect from 'expect.js';
import { stub } from 'auto-release-sinon';
import ngMock from 'ngMock';
import xsrfChromeApi from '../xsrf';
const xsrfHeader = 'kbn-xsrf-token';
const xsrfToken = 'xsrfToken';
describe('chrome xsrf apis', function () {
describe('#getXsrfToken()', function () {
it('exposes the token', function () {
const chrome = {};
xsrfChromeApi(chrome, { xsrfToken });
expect(chrome.getXsrfToken()).to.be(xsrfToken);
});
});
context('jQuery support', function () {
it('adds a global jQuery prefilter', function () {
stub($, 'ajaxPrefilter');
xsrfChromeApi({}, {});
expect($.ajaxPrefilter.callCount).to.be(1);
});
context('jQuery prefilter', function () {
let prefilter;
const xsrfToken = 'xsrfToken';
beforeEach(function () {
stub($, 'ajaxPrefilter');
xsrfChromeApi({}, { xsrfToken });
prefilter = $.ajaxPrefilter.args[0][0];
});
it('sets the kbn-xsrf-token header', function () {
const setHeader = stub();
prefilter({}, {}, { setRequestHeader: setHeader });
expect(setHeader.callCount).to.be(1);
expect(setHeader.args[0]).to.eql([
xsrfHeader,
xsrfToken
]);
});
it('can be canceled by setting the kbnXsrfToken option', function () {
const setHeader = stub();
prefilter({ kbnXsrfToken: false }, {}, { setRequestHeader: setHeader });
expect(setHeader.callCount).to.be(0);
});
});
context('Angular support', function () {
let $http;
let $httpBackend;
beforeEach(function () {
stub($, 'ajaxPrefilter');
const chrome = {};
xsrfChromeApi(chrome, { xsrfToken });
ngMock.module(chrome.$setupXsrfRequestInterceptor);
});
beforeEach(ngMock.inject(function ($injector) {
$http = $injector.get('$http');
$httpBackend = $injector.get('$httpBackend');
$httpBackend
.when('POST', '/api/test')
.respond('ok');
}));
afterEach(function () {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('injects a kbn-xsrf-token header on every request', function () {
$httpBackend.expectPOST('/api/test', undefined, function (headers) {
return headers[xsrfHeader] === xsrfToken;
}).respond(200, '');
$http.post('/api/test');
$httpBackend.flush();
});
it('skips requests with the kbnXsrfToken set falsey', function () {
$httpBackend.expectPOST('/api/test', undefined, function (headers) {
return !(xsrfHeader in headers);
}).respond(200, '');
$http({
method: 'POST',
url: '/api/test',
kbnXsrfToken: 0
});
$http({
method: 'POST',
url: '/api/test',
kbnXsrfToken: ''
});
$http({
method: 'POST',
url: '/api/test',
kbnXsrfToken: false
});
$httpBackend.flush();
});
it('accepts alternate tokens to use', function () {
const customToken = `custom:${xsrfToken}`;
$httpBackend.expectPOST('/api/test', undefined, function (headers) {
return headers[xsrfHeader] === customToken;
}).respond(200, '');
$http({
method: 'POST',
url: '/api/test',
kbnXsrfToken: customToken
});
$httpBackend.flush();
});
});
});
});

View file

@ -24,6 +24,7 @@ module.exports = function (chrome, internals) {
a.href = '/elasticsearch';
return a.href;
}()))
.config(chrome.$setupXsrfRequestInterceptor)
.directive('kbnChrome', function ($rootScope) {
return {
template: function ($el) {

View file

@ -0,0 +1,29 @@
import $ from 'jquery';
import { set } from 'lodash';
export default function (chrome, internals) {
chrome.getXsrfToken = function () {
return internals.xsrfToken;
};
$.ajaxPrefilter(function ({ kbnXsrfToken = internals.xsrfToken }, originalOptions, jqXHR) {
if (kbnXsrfToken) {
jqXHR.setRequestHeader('kbn-xsrf-token', kbnXsrfToken);
}
});
chrome.$setupXsrfRequestInterceptor = function ($httpProvider) {
$httpProvider.interceptors.push(function () {
return {
request: function (opts) {
const { kbnXsrfToken = internals.xsrfToken } = opts;
if (kbnXsrfToken) {
set(opts, ['headers', 'kbn-xsrf-token'], kbnXsrfToken);
}
return opts;
}
};
});
};
}

View file

@ -18,6 +18,7 @@ var internals = _.defaults(
rootController: null,
rootTemplate: null,
showAppsLink: null,
xsrfToken: null,
brand: null,
nav: [],
applicationClasses: []
@ -30,6 +31,7 @@ $('<link>').attr({
}).appendTo('head');
require('./api/apps')(chrome, internals);
require('./api/xsrf')(chrome, internals);
require('./api/nav')(chrome, internals);
require('./api/angular')(chrome, internals);
require('./api/controls')(chrome, internals);

View file

@ -1,4 +1,8 @@
/* global mocha */
// chrome expects to be loaded first, let it get its way
var chrome = require('ui/chrome');
var Nonsense = require('Nonsense');
var sinon = require('sinon');
var $ = require('jquery');
@ -6,8 +10,6 @@ var _ = require('lodash');
var parse = require('url').parse;
var StackTraceMapper = require('ui/StackTraceMapper');
var chrome = require('ui/chrome');
/*** the vislib tests have certain style requirements, so lets make sure they are met ***/
$('body').attr('id', 'test-harness-body'); // so we can make high priority selectors

View file

@ -668,16 +668,21 @@ define(function (require) {
* @return {undefined}
*/
Data.prototype._normalizeOrdered = function () {
if (!this.data.ordered || !this.data.ordered.date) return;
var data = this.getVisData();
var self = this;
var missingMin = this.data.ordered.min == null;
var missingMax = this.data.ordered.max == null;
data.forEach(function (d) {
if (!d.ordered || !d.ordered.date) return;
if (missingMax || missingMin) {
var extent = d3.extent(this.xValues());
if (missingMin) this.data.ordered.min = extent[0];
if (missingMax) this.data.ordered.max = extent[1];
}
var missingMin = d.ordered.min == null;
var missingMax = d.ordered.max == null;
if (missingMax || missingMin) {
var extent = d3.extent(self.xValues());
if (missingMin) d.ordered.min = extent[0];
if (missingMax) d.ordered.max = extent[1];
}
});
};
/**

View file

@ -8,6 +8,7 @@ module.exports = function (grunt) {
grunt.registerTask('_build:babelOptions', function () {
unlink(srcFile);
rename(buildFile, srcFile);
grunt.file.mkdir('build/kibana/optimize');
});
};

View file

@ -44,7 +44,7 @@ module.exports = function (grunt) {
purge: true,
config: {
http: {
port: uiConfig.elasticsearch.port
port: uiConfig.servers.elasticsearch.port
}
}
}

View file

@ -33,8 +33,9 @@ module.exports = function (grunt) {
},
cmd: /^win/.test(platform) ? '.\\bin\\kibana.bat' : './bin/kibana',
args: [
'--server.port=' + uiConfig.kibana.port,
'--elasticsearch.url=' + format(uiConfig.elasticsearch),
'--server.port=' + uiConfig.servers.kibana.port,
'--env.name=development',
'--elasticsearch.url=' + format(uiConfig.servers.elasticsearch),
'--logging.json=false'
]
},
@ -89,7 +90,7 @@ module.exports = function (grunt) {
'-jar',
'selenium/selenium-server-standalone-2.47.1.jar',
'-port',
uiConfig.webdriver.port
uiConfig.servers.webdriver.port
]
},
@ -105,7 +106,7 @@ module.exports = function (grunt) {
'-jar',
'selenium/selenium-server-standalone-2.47.1.jar',
'-port',
uiConfig.webdriver.port
uiConfig.servers.webdriver.port
]
},

View file

@ -21,11 +21,11 @@ describe('scenario manager', function () {
it('should be able to load scenarios', function () {
return manager.load('makelogs')
.then(function () {
expect(create.getCall(0).args[0].index).to.be('logstash-2015.09.17');
expect(create.getCall(1).args[0].index).to.be('logstash-2015.09.18');
expect(bulk.called).to.be(true);
});
.then(function () {
expect(create.getCall(0).args[0].index).to.be('logstash-2015.09.17');
expect(create.getCall(1).args[0].index).to.be('logstash-2015.09.18');
expect(bulk.called).to.be(true);
});
});
it('should be able to delete all indices', function () {
@ -55,6 +55,52 @@ describe('scenario manager', function () {
});
});
it('should load if the index does not exist', function () {
var load = sinon.stub(manager, 'load', Promise.resolve);
var throwError = sinon.stub(manager.client, 'count', Promise.reject);
var id = 'makelogs';
return manager.loadIfEmpty(id).then(function () {
expect(load.calledWith(id)).to.be(true);
load.restore();
throwError.restore();
});
});
it('should load if the index is empty', function () {
var load = sinon.stub(manager, 'load', Promise.resolve);
var returnZero = sinon.stub(manager.client, 'count', function () {
return Promise.resolve({
'count': 0
});
});
var id = 'makelogs';
return manager.loadIfEmpty(id).then(function () {
expect(load.calledWith(id)).to.be(true);
load.restore();
returnZero.restore();
});
});
it('should not load if the index is not empty', function () {
var load = sinon.stub(manager, 'load', Promise.resolve);
var returnOne = sinon.stub(manager.client, 'count', function () {
return Promise.resolve({
'count': 1
});
});
var id = 'makelogs';
return manager.loadIfEmpty(id).then(function () {
expect(load.called).to.be(false);
load.restore();
returnOne.restore();
});
});
afterEach(function () {
bulk.restore();
create.restore();
@ -62,12 +108,40 @@ describe('scenario manager', function () {
});
});
it('should throw an error if the scenario is not defined', function () {
expect(manager.load).withArgs('makelogs').to.throwError();
describe('load', function () {
it('should reject if the scenario is not specified', function () {
return manager.load()
.then(function () {
throw new Error('Promise should reject');
})
.catch(function () { return; });
});
it('should reject if the scenario is not defined', function () {
return manager.load('idonotexist')
.then(function () {
throw new Error('Promise should reject');
})
.catch(function () { return; });
});
});
it('should throw an error if an index is not defined when clearing', function () {
expect(manager.unload).to.throwError();
describe('unload', function () {
it('should reject if the scenario is not specified', function () {
return manager.unload()
.then(function () {
throw new Error('Promise should reject');
})
.catch(function () { return; });
});
it('should reject if the scenario is not defined', function () {
return manager.unload('idonotexist')
.then(function () {
throw new Error('Promise should reject');
})
.catch(function () { return; });
});
});
it('should throw an error if an es server is not specified', function () {

View file

@ -2,23 +2,27 @@ var path = require('path');
var rootDir = path.join(__dirname, 'scenarios');
module.exports = {
makelogs: {
baseDir: path.join(rootDir, 'makelogs'),
bulk: [{
indexDefinition: 'makelogsIndexDefinition.js',
indexName: 'logstash-2015.09.17',
source: 'logstash-2015.09.17.js'
}, {
indexDefinition: 'makelogsIndexDefinition.js',
indexName: 'logstash-2015.09.18',
source: 'logstash-2015.09.18.js'
}]
},
emptyKibana: {
baseDir: path.join(rootDir, 'emptyKibana'),
bulk: [{
indexName: '.kibana',
source: 'kibana.js'
}]
scenarios: {
makelogs: {
baseDir: path.join(rootDir, 'makelogs'),
bulk: [{
indexName: 'logstash-2015.09.17',
indexDefinition: 'makelogsIndexDefinition.js',
source: 'logstash-2015.09.17.js'
}, {
indexName: 'logstash-2015.09.18',
indexDefinition: 'makelogsIndexDefinition.js',
source: 'logstash-2015.09.18.js'
}]
},
emptyKibana: {
baseDir: path.join(rootDir, 'emptyKibana'),
bulk: [{
indexName: '.kibana',
indexDefinition: 'kibanaDefinition.js',
source: 'kibana.js',
haltOnFailure: false
}]
}
}
};

View file

@ -1,6 +1,7 @@
var path = require('path');
var config = require('./config');
var elasticsearch = require('elasticsearch');
var Promise = require('bluebird');
var config = require('./config').scenarios;
function ScenarioManager(server) {
if (!server) throw new Error('No server defined');
@ -16,28 +17,33 @@ function ScenarioManager(server) {
* @return {Promise} A promise that is resolved when elasticsearch has a response
*/
ScenarioManager.prototype.load = function (id) {
var scenario = config[id];
if (!scenario) throw new Error('No scenario found for ' + id);
var self = this;
var scenario = config[id];
if (!scenario) return Promise.reject('No scenario found for ' + id);
return Promise.all(scenario.bulk.map(function mapBulk(bulk) {
var loadIndexDefinition;
if (bulk.indexDefinition) {
var body = require(path.join(scenario.baseDir, bulk.indexDefinition));
loadIndexDefinition = self.client.indices.create({
index: bulk.indexName,
body: require(path.join(scenario.baseDir, bulk.indexDefinition))
body: body
});
} else {
loadIndexDefinition = Promise.resolve();
}
return loadIndexDefinition.then(function bulkRequest() {
self.client.bulk({
body: require(path.join(scenario.baseDir, bulk.source)),
return loadIndexDefinition
.then(function bulkRequest() {
var body = require(path.join(scenario.baseDir, bulk.source));
return self.client.bulk({
body: body
});
})
.catch(function (err) {
if (bulk.haltOnFailure === false) return;
throw err;
});
}));
};
@ -48,7 +54,7 @@ ScenarioManager.prototype.load = function (id) {
*/
ScenarioManager.prototype.unload = function (id) {
var scenario = config[id];
if (!scenario) throw new Error('Expected index');
if (!scenario) return Promise.reject('No scenario found for ' + id);
var indices = scenario.bulk.map(function mapBulk(bulk) {
return bulk.indexName;
@ -67,7 +73,8 @@ ScenarioManager.prototype.unload = function (id) {
ScenarioManager.prototype.reload = function (id) {
var self = this;
return this.unload(id).then(function load() {
return self.unload(id)
.then(function load() {
return self.load(id);
});
};
@ -82,4 +89,32 @@ ScenarioManager.prototype.deleteAll = function () {
});
};
module.exports = ScenarioManager;
/**
* Load a testing scenario if not already loaded
* @param {string} id The scenario id to load
* @return {Promise} A promise that is resolved when elasticsearch has a response
*/
ScenarioManager.prototype.loadIfEmpty = function (id) {
var self = this;
var scenario = config[id];
if (!scenario) throw new Error('No scenario found for ' + id);
var self = this;
return Promise.all(scenario.bulk.map(function mapBulk(bulk) {
var loadIndexDefinition;
return self.client.count({
index: bulk.indexName
})
.then(function handleCountResponse(response) {
if (response.count === 0) {
return self.load(id);
}
});
}))
.catch(function (reason) {
return self.load(id);
});
};
module.exports = ScenarioManager;

View file

@ -0,0 +1,16 @@
module.exports = {
settings: {
number_of_shards: 1,
number_of_replicas: 1
},
mappings: {
config: {
properties: {
buildNum: {
type: 'string',
index: 'not_analyzed'
}
}
}
}
};

View file

@ -0,0 +1,60 @@
define(function (require) {
var Common = require('../../../support/pages/Common');
var SettingsPage = require('../../../support/pages/SettingsPage');
var expect = require('intern/dojo/node!expect.js');
return function (bdd, scenarioManager) {
bdd.describe('user input reactions', function () {
var common;
var settingsPage;
bdd.before(function () {
common = new Common(this.remote);
settingsPage = new SettingsPage(this.remote);
});
bdd.beforeEach(function () {
return scenarioManager.reload('emptyKibana')
.then(function () {
return settingsPage.navigateTo();
});
});
bdd.it('should hide time-based index pattern when time-based option is unchecked', function () {
var self = this;
return settingsPage.getTimeBasedEventsCheckbox()
.then(function (selected) {
// uncheck the 'time-based events' checkbox
return selected.click();
})
// try to find the checkbox (this shouldn fail)
.then(function () {
var waitTime = 10000;
return settingsPage.getTimeBasedIndexPatternCheckbox(waitTime);
})
.then(function () {
// we expect the promise above to fail
var handler = common.handleError(self);
var msg = 'Found time based index pattern checkbox';
handler(msg);
})
.catch(function () {
// we expect this failure since checkbox should be hidden
return;
});
});
bdd.it('should enable creation after selecting time field', function () {
// select a time field and check that Create button is enabled
return settingsPage.selectTimeFieldOption('@timestamp')
.then(function () {
return settingsPage.getCreateButton().isEnabled()
.then(function (enabled) {
expect(enabled).to.be.ok();
});
})
.catch(common.handleError(this));
});
});
};
});

View file

@ -0,0 +1,104 @@
define(function (require) {
var Common = require('../../../support/pages/Common');
var SettingsPage = require('../../../support/pages/SettingsPage');
var expect = require('intern/dojo/node!expect.js');
var Promise = require('bluebird');
return function (bdd, scenarioManager) {
bdd.describe('creating and deleting default index', function describeIndexTests() {
var common;
var settingsPage;
var remote;
bdd.before(function () {
common = new Common(this.remote);
settingsPage = new SettingsPage(this.remote);
remote = this.remote;
return scenarioManager.reload('emptyKibana')
.then(function () {
return settingsPage.navigateTo();
});
});
bdd.describe('index pattern creation', function indexPatternCreation() {
bdd.before(function () {
return settingsPage.createIndexPattern();
});
bdd.it('should have index pattern in page header', function pageHeader() {
return settingsPage.getIndexPageHeading().getVisibleText()
.then(function (patternName) {
expect(patternName).to.be('logstash-*');
})
.catch(common.handleError(this));
});
bdd.it('should have index pattern in url', function url() {
return common.tryForTime(5000, function () {
return remote.getCurrentUrl()
.then(function (currentUrl) {
expect(currentUrl).to.contain('logstash-*');
});
})
.catch(common.handleError(this));
});
bdd.it('should have expected table headers', function checkingHeader() {
return settingsPage.getTableHeader()
.then(function (headers) {
var expectedHeaders = [
'name',
'type',
'format',
'analyzed',
'indexed',
'controls'
];
// 6 name type format analyzed indexed controls
expect(headers.length).to.be(expectedHeaders.length);
var comparedHeaders = headers.map(function compareHead(header, i) {
return header.getVisibleText()
.then(function (text) {
expect(text).to.be(expectedHeaders[i]);
});
});
return Promise.all(comparedHeaders);
})
.catch(common.handleError(this));
});
});
bdd.describe('index pattern deletion', function indexDelete() {
bdd.before(function () {
var expectedAlertText = 'Are you sure you want to remove this index pattern?';
return settingsPage.removeIndexPattern()
.then(function (alertText) {
expect(alertText).to.be(expectedAlertText);
});
});
bdd.it('should return to index pattern creation page', function returnToPage() {
return common.tryForTime(5000, function () {
return settingsPage.getCreateButton();
})
.catch(common.handleError(this));
});
bdd.it('should remove index pattern from url', function indexNotInUrl() {
// give the url time to settle
return common.tryForTime(5000, function () {
return remote.getCurrentUrl()
.then(function (currentUrl) {
expect(currentUrl).to.not.contain('logstash-*');
});
})
.catch(common.handleError(this));
});
});
});
};
});

View file

@ -0,0 +1,111 @@
define(function (require) {
var Common = require('../../../support/pages/Common');
var SettingsPage = require('../../../support/pages/SettingsPage');
var expect = require('intern/dojo/node!expect.js');
//var Promise = require('bluebird');
return function (bdd, scenarioManager) {
bdd.describe('index result popularity', function describeIndexTests() {
var common;
var settingsPage;
var remote;
bdd.before(function () {
common = new Common(this.remote);
settingsPage = new SettingsPage(this.remote);
remote = this.remote;
return scenarioManager.reload('emptyKibana')
.then(function () {
return settingsPage.navigateTo();
});
});
bdd.beforeEach(function be() {
return settingsPage.createIndexPattern();
});
bdd.afterEach(function ae() {
return settingsPage.removeIndexPattern();
});
bdd.describe('change popularity', function indexPatternCreation() {
var fieldName = 'geo.coordinates';
// set the page size to All again, https://github.com/elastic/kibana/issues/5030
// TODO: remove this after issue #5030 is closed
function fix5030() {
return settingsPage.setPageSize('All')
.then(function () {
return common.sleep(1000);
});
}
bdd.beforeEach(function () {
// increase Popularity of geo.coordinates
return settingsPage.setPageSize('All')
.then(function () {
return common.sleep(1000);
})
.then(function openControlsByName() {
return settingsPage.openControlsByName(fieldName);
})
.then(function increasePopularity() {
return settingsPage.increasePopularity();
});
});
bdd.afterEach(function () {
// Cancel saving the popularity change (we didn't make a change in this case, just checking the value)
return settingsPage.controlChangeCancel();
});
bdd.it('should update the popularity input', function () {
return settingsPage.getPopularity()
.then(function (popularity) {
expect(popularity).to.be('1');
})
.catch(common.handleError(this));
});
bdd.it('should be reset on cancel', function pageHeader() {
// Cancel saving the popularity change
return settingsPage.controlChangeCancel()
.then(function () {
return fix5030();
})
.then(function openControlsByName() {
return settingsPage.openControlsByName(fieldName);
})
// check that its 0 (previous increase was cancelled)
.then(function getPopularity() {
return settingsPage.getPopularity();
})
.then(function (popularity) {
expect(popularity).to.be('0');
})
.catch(common.handleError(this));
});
bdd.it('can be saved', function pageHeader() {
// Saving the popularity change
return settingsPage.controlChangeSave()
.then(function () {
return fix5030();
})
.then(function openControlsByName() {
return settingsPage.openControlsByName(fieldName);
})
// check that its 0 (previous increase was cancelled)
.then(function getPopularity() {
return settingsPage.getPopularity();
})
.then(function (popularity) {
expect(popularity).to.be('1');
})
.catch(common.handleError(this));
});
}); // end 'change popularity'
}); // end index result popularity
};
});

View file

@ -0,0 +1,134 @@
define(function (require) {
var Common = require('../../../support/pages/Common');
var SettingsPage = require('../../../support/pages/SettingsPage');
var expect = require('intern/dojo/node!expect.js');
var Promise = require('bluebird');
return function (bdd, scenarioManager) {
bdd.describe('index result field sort', function describeIndexTests() {
var common;
var settingsPage;
var remote;
bdd.before(function () {
common = new Common(this.remote);
settingsPage = new SettingsPage(this.remote);
remote = this.remote;
return scenarioManager.reload('emptyKibana');
});
var columns = [{
heading: 'name',
first: '@message',
last: 'xss.raw',
selector: function () {
return settingsPage.getTableRow(0, 0).getVisibleText();
}
}, {
heading: 'type',
first: '_source',
last: 'string',
selector: function () {
return settingsPage.getTableRow(0, 1).getVisibleText();
}
}];
columns.forEach(function (col) {
bdd.describe('sort by heading - ' + col.heading, function indexPatternCreation() {
bdd.before(function () {
return settingsPage.navigateTo();
});
bdd.beforeEach(function () {
return settingsPage.createIndexPattern();
});
bdd.afterEach(function () {
return settingsPage.removeIndexPattern();
});
bdd.it('should sort ascending', function pageHeader() {
return settingsPage.sortBy(col.heading)
.then(function getText() {
return col.selector();
})
.then(function (rowText) {
expect(rowText).to.be(col.first);
})
.catch(common.handleError(this));
});
bdd.it('should sort descending', function pageHeader() {
return settingsPage.sortBy(col.heading)
.then(function sortAgain() {
return settingsPage.sortBy(col.heading);
})
.then(function getText() {
return col.selector();
})
.then(function (rowText) {
expect(rowText).to.be(col.last);
})
.catch(common.handleError(this));
});
});
});
bdd.describe('field list pagination', function () {
var expectedDefaultPageSize = 25;
var expectedFieldCount = 85;
var expectedLastPageCount = 10;
var pages = [1, 2, 3, 4];
bdd.before(function () {
return settingsPage.navigateTo()
.then(function () {
return settingsPage.createIndexPattern();
});
});
bdd.after(function () {
return settingsPage.removeIndexPattern();
});
bdd.it('makelogs data should have expected number of fields', function () {
return settingsPage.getFieldsTabCount()
.then(function (tabCount) {
expect(tabCount).to.be('' + expectedFieldCount);
})
.catch(common.handleError(this));
});
bdd.it('should have correct default page size selected', function () {
return settingsPage.getPageSize()
.then(function (pageSize) {
expect(pageSize).to.be('' + expectedDefaultPageSize);
})
.catch(common.handleError(this));
});
bdd.it('should have the correct number of rows per page', function () {
var pageCount = Math.ceil(expectedFieldCount / expectedDefaultPageSize);
var chain = pages.reduce(function (chain, val) {
return chain.then(function () {
return settingsPage.goToPage(val)
.then(function () {
return common.sleep(1000);
})
.then(function () {
return settingsPage.getPageFieldCount();
})
.then(function (pageCount) {
var expectedSize = (val < 4) ? expectedDefaultPageSize : expectedLastPageCount;
expect(pageCount.length).to.be(expectedSize);
});
});
}, Promise.resolve());
return chain.catch(common.handleError(this));
});
}); // end describe pagination
}); // end index result field sort
};
});

View file

@ -0,0 +1,64 @@
define(function (require) {
var expect = require('intern/dojo/node!expect.js');
var Common = require('../../../support/pages/Common');
var SettingsPage = require('../../../support/pages/SettingsPage');
return function (bdd, scenarioManager) {
bdd.describe('initial state', function () {
var common;
var settingsPage;
bdd.before(function () {
common = new Common(this.remote);
settingsPage = new SettingsPage(this.remote);
return scenarioManager.reload('emptyKibana')
.then(function () {
return settingsPage.navigateTo();
});
});
bdd.it('should load with time pattern checked', function () {
return settingsPage.getTimeBasedEventsCheckbox().isSelected()
.then(function (selected) {
expect(selected).to.be.ok();
})
.catch(common.handleError(this));
});
bdd.it('should load with name pattern unchecked', function () {
return settingsPage.getTimeBasedIndexPatternCheckbox().isSelected()
.then(function (selected) {
expect(selected).to.not.be.ok();
})
.catch(common.handleError(this));
});
bdd.it('should contain default index pattern', function () {
var defaultPattern = 'logstash-*';
return settingsPage.getIndexPatternField().getProperty('value')
.then(function (pattern) {
expect(pattern).to.be(defaultPattern);
})
.catch(common.handleError(this));
});
bdd.it('should not select the time field', function () {
return settingsPage.getTimeFieldNameField().isSelected()
.then(function (timeFieldIsSelected) {
expect(timeFieldIsSelected).to.not.be.ok();
})
.catch(common.handleError(this));
});
bdd.it('should not be enable creation', function () {
return settingsPage.getCreateButton().isEnabled()
.then(function (enabled) {
expect(enabled).to.not.be.ok();
})
.catch(common.handleError(this));
});
});
};
});

View file

@ -0,0 +1,38 @@
define(function (require) {
var bdd = require('intern!bdd');
var config = require('intern').config;
var url = require('intern/dojo/node!url');
var ScenarioManager = require('intern/dojo/node!../../../fixtures/scenarioManager');
var initialStateTest = require('./_initial_state');
var creationChangesTest = require('./_creation_form_changes');
var indexPatternCreateDeleteTest = require('./_index_pattern_create_delete');
var indexPatternResultsSortTest = require('./_index_pattern_results_sort');
var indexPatternPopularityTest = require('./_index_pattern_popularity');
bdd.describe('settings app', function () {
var scenarioManager = new ScenarioManager(url.format(config.servers.elasticsearch));
// on setup, we create an settingsPage instance
// that we will use for all the tests
bdd.before(function () {
return scenarioManager.reload('emptyKibana')
.then(function () {
return scenarioManager.loadIfEmpty('makelogs');
});
});
bdd.after(function () {
return scenarioManager.unload('makelogs')
.then(function () {
scenarioManager.unload('emptyKibana');
});
});
initialStateTest(bdd, scenarioManager);
creationChangesTest(bdd, scenarioManager);
indexPatternCreateDeleteTest(bdd, scenarioManager);
indexPatternResultsSortTest(bdd, scenarioManager);
indexPatternPopularityTest(bdd, scenarioManager);
});
});

View file

@ -1,21 +0,0 @@
define(function (require) {
var registerSuite = require('intern!object');
var expect = require('intern/dojo/node!expect.js');
var config = require('intern').config;
var getUrl = require('intern/dojo/node!../utils/getUrl');
registerSuite(function () {
return {
'status': function () {
return this.remote
.get(getUrl(config.kibana, 'status'))
.setFindTimeout(60000)
.findByCssSelector('.plugin_status_breakdown')
.getVisibleText()
.then(function (text) {
expect(text.indexOf('plugin:kibana Ready')).to.be.above(-1);
});
}
};
});
});

View file

@ -0,0 +1,30 @@
define(function (require) {
var bdd = require('intern!bdd');
var expect = require('intern/dojo/node!expect.js');
var config = require('intern').config;
var Common = require('../../support/pages/Common');
bdd.describe('status page', function () {
var common;
bdd.before(function () {
common = new Common(this.remote);
// load the status page
return common.navigateToApp('statusPage', false);
});
bdd.it('should show the kibana plugin as ready', function () {
var self = this;
return common.tryForTime(6000, function () {
return self.remote
.findByCssSelector('.plugin_status_breakdown')
.getVisibleText()
.then(function (text) {
expect(text.indexOf('plugin:kibana Ready')).to.be.above(-1);
});
})
.catch(common.handleError(self));
});
});
});

View file

@ -3,6 +3,7 @@ define(function (require) {
var _ = require('intern/dojo/node!lodash');
return _.assign({
debug: false,
capabilities: {
'selenium-version': '2.47.1',
'idle-timeout': 30
@ -10,8 +11,17 @@ define(function (require) {
environments: [{
browserName: 'firefox'
}],
tunnelOptions: serverConfig.webdriver,
functionalSuites: ['test/functional/status.js'],
excludeInstrumentation: /(fixtures|node_modules)\//
tunnelOptions: serverConfig.servers.webdriver,
functionalSuites: [
'test/functional/status_page/index',
'test/functional/apps/settings/index'
],
excludeInstrumentation: /(fixtures|node_modules)\//,
loaderOptions: {
paths: {
'bluebird': './node_modules/bluebird/js/browser/bluebird.js',
'moment': './node_modules/moment/moment.js'
}
}
}, serverConfig);
});

View file

@ -1,17 +1,42 @@
var kibanaURL = '/app/kibana';
module.exports = {
webdriver: {
protocol: process.env.TEST_UI_WEBDRIVER_PROTOCOL || 'http',
hostname: process.env.TEST_UI_WEBDRIVER_HOSTNAME || 'localhost',
port: parseInt(process.env.TEST_UI_WEBDRIVER_PORT, 10) || 4444
servers: {
webdriver: {
protocol: process.env.TEST_UI_WEBDRIVER_PROTOCOL || 'http',
hostname: process.env.TEST_UI_WEBDRIVER_HOSTNAME || 'localhost',
port: parseInt(process.env.TEST_UI_WEBDRIVER_PORT, 10) || 4444
},
kibana: {
protocol: process.env.TEST_UI_KIBANA_PROTOCOL || 'http',
hostname: process.env.TEST_UI_KIBANA_HOSTNAME || 'localhost',
port: parseInt(process.env.TEST_UI_KIBANA_PORT, 10) || 5620
},
elasticsearch: {
protocol: process.env.TEST_UI_ES_PROTOCOL || 'http',
hostname: process.env.TEST_UI_ES_HOSTNAME || 'localhost',
port: parseInt(process.env.TEST_UI_ES_PORT, 10) || 9220
}
},
kibana: {
protocol: process.env.TEST_UI_KIBANA_PROTOCOL || 'http',
hostname: process.env.TEST_UI_KIBANA_HOSTNAME || 'localhost',
port: parseInt(process.env.TEST_UI_KIBANA_PORT, 10) || 5620
},
elasticsearch: {
protocol: process.env.TEST_UI_ES_PROTOCOL || 'http',
hostname: process.env.TEST_UI_ES_HOSTNAME || 'localhost',
port: parseInt(process.env.TEST_UI_ES_PORT, 10) || 9220
apps: {
statusPage: {
pathname: 'status'
},
discover: {
pathname: kibanaURL,
hash: '/discover',
},
visualize: {
pathname: kibanaURL,
hash: '/visualize',
},
dashboard: {
pathname: kibanaURL,
hash: '/dashboard',
},
settings: {
pathname: kibanaURL,
hash: '/settings'
}
}
};

View file

@ -0,0 +1,198 @@
// in test/support/pages/Common.js
define(function (require) {
var config = require('intern').config;
var Promise = require('bluebird');
var moment = require('moment');
var getUrl = require('intern/dojo/node!../../utils/getUrl');
var fs = require('intern/dojo/node!fs');
var path = require('intern/dojo/node!path');
function Common(remote) {
this.remote = remote;
}
var defaultTimeout = 60000;
Common.prototype = {
constructor: Common,
navigateToApp: function (appName, testStatusPage) {
var self = this;
var appUrl = getUrl(config.servers.kibana, config.apps[appName]);
var doNavigation = function (url) {
return self.tryForTime(defaultTimeout, function () {
// since we're using hash URLs, always reload first to force re-render
return self.remote.get(url)
.then(function () {
return self.remote.refresh();
})
.then(function () {
if (testStatusPage !== false) {
return self.checkForKibanaApp()
.then(function (kibanaLoaded) {
if (!kibanaLoaded) throw new Error('Kibana is not loaded, retrying');
});
}
})
.then(function () {
return self.remote.getCurrentUrl();
})
.then(function (currentUrl) {
var navSuccessful = new RegExp(appUrl).test(currentUrl);
if (!navSuccessful) throw new Error('App failed to load: ' + appName);
});
});
};
return doNavigation(appUrl)
.then(function () {
return self.remote.getCurrentUrl();
})
.then(function (currentUrl) {
var lastUrl = currentUrl;
return self.tryForTime(defaultTimeout, function () {
// give the app time to update the URL
return self.sleep(500)
.then(function () {
return self.remote.getCurrentUrl();
})
.then(function (currentUrl) {
if (lastUrl !== currentUrl) {
lastUrl = currentUrl;
throw new Error('URL changed, waiting for it to settle');
}
});
});
});
},
runScript: function (fn, timeout) {
var self = this;
// by default, give the app 10 seconds to load
timeout = timeout || 10000;
// wait for deps on window before running script
return self.remote
.setExecuteAsyncTimeout(timeout)
.executeAsync(function (done) {
var interval = setInterval(function () {
var ready = (document.readyState === 'complete');
var hasJQuery = !!window.$;
if (ready && hasJQuery) {
console.log('doc ready, jquery loaded');
clearInterval(interval);
done();
}
}, 10);
}).then(function () {
return self.remote.execute(fn);
});
},
getApp: function () {
var self = this;
return self.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('.content > .application')
.then(function () {
return self.runScript(function () {
var $ = window.$;
var $scope = $('.content > .application').scope();
return $scope ? $scope.chrome.getApp() : {};
});
});
},
checkForKibanaApp: function () {
var self = this;
return self.getApp()
.then(function (app) {
var appId = app.id;
self.debug('current application: ' + appId);
return appId === 'kibana';
})
.catch(function (err) {
self.debug('kibana check failed');
self.debug(err);
// not on the kibana app...
return false;
});
},
tryForTime: function (timeout, block) {
var self = this;
var start = Date.now();
var retryDelay = 500;
var lastTry = 0;
function attempt() {
lastTry = Date.now();
if (lastTry - start > timeout) {
throw new Error('timeout');
}
return Promise
.try(block)
.then(function tryForTimeSuccess() {
self.debug('tryForTime success in about ' + (lastTry - start) + ' ms');
return (lastTry - start);
})
.catch(function tryForTimeCatch(err) {
self.debug('tryForTime failure, retry in ' + retryDelay + 'ms - ' + err.message);
return Promise.delay(retryDelay).then(attempt);
});
}
return Promise.try(attempt);
},
log: function (logString) {
console.log(moment().format('HH:mm:ss.SSS') + ': ' + logString);
},
debug: function (logString) {
if (config.debug) this.log(logString);
},
sleep: function (sleepMilliseconds) {
this.debug('sleeping for ' + sleepMilliseconds + 'ms');
return Promise.resolve().delay(sleepMilliseconds);
},
handleError: function (testObj) {
var self = this;
var testName = (testObj.parent) ? [testObj.parent.name, testObj.name].join('_') : testObj.name;
return function (reason) {
var now = Date.now();
var filename = ['failure', now, testName].join('_') + '.png';
return self.saveScreenshot(filename)
.finally(function () {
throw new Error(reason);
});
};
},
saveScreenshot: function (filename) {
var self = this;
var outDir = path.resolve('test', 'output');
return self.remote.takeScreenshot()
.then(function writeScreenshot(data) {
var filepath = path.resolve(outDir, filename);
self.debug('Test Failed, taking screenshot "' + filepath + '"');
fs.writeFileSync(filepath, data);
})
.catch(function (err) {
self.log('SCREENSHOT FAILED: ' + err);
});
}
};
return Common;
});

View file

@ -0,0 +1,54 @@
// in test/support/pages/HeaderPage.js
define(function (require) {
var Common = require('./Common');
var common;
// the page object is created as a constructor
// so we can provide the remote Command object
// at runtime
function HeaderPage(remote) {
this.remote = remote;
common = new Common(this.remote);
}
var defaultTimeout = 5000;
HeaderPage.prototype = {
constructor: HeaderPage,
clickSelector: function (selector) {
var self = this.remote;
return common.tryForTime(5000, function () {
return self.setFindTimeout(defaultTimeout)
.findByCssSelector(selector)
.then(function (tab) {
return tab.click();
});
});
},
clickDiscover: function () {
common.debug('click Discover tab');
this.clickSelector('a[href*=\'discover\']');
},
clickVisualize: function () {
common.debug('click Visualize tab');
this.clickSelector('a[href*=\'visualize\']');
},
clickDashboard: function () {
common.debug('click Dashboard tab');
this.clickSelector('a[href*=\'dashboard\']');
},
clickSettings: function () {
common.debug('click Settings tab');
this.clickSelector('a[href*=\'settings\']');
}
};
return HeaderPage;
});

View file

@ -0,0 +1,299 @@
// in test/support/pages/SettingsPage.js
define(function (require) {
// the page object is created as a constructor
// so we can provide the remote Command object
// at runtime
var Promise = require('bluebird');
var Common = require('./Common');
var defaultTimeout = 60000;
var common;
function SettingsPage(remote) {
this.remote = remote;
common = new Common(this.remote);
}
SettingsPage.prototype = {
constructor: SettingsPage,
navigateTo: function () {
return common.navigateToApp('settings');
},
getTimeBasedEventsCheckbox: function () {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('input[ng-model="index.isTimeBased"]');
},
getTimeBasedIndexPatternCheckbox: function (timeout) {
timeout = timeout || defaultTimeout;
// fail faster since we're sometimes checking that it doesn't exist
return this.remote.setFindTimeout(timeout)
.findByCssSelector('input[ng-model="index.nameIsPattern"]');
},
getIndexPatternField: function () {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('[ng-model="index.name"]');
},
getTimeFieldNameField: function () {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('select[ng-model="index.timeField"]');
},
selectTimeFieldOption: function (selection) {
var self = this;
return self.getTimeFieldNameField().click()
.then(function () {
return self.getTimeFieldNameField().click();
})
.then(function () {
return self.getTimeFieldOption(selection);
});
},
getTimeFieldOption: function (selection) {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('option[label="' + selection + '"]').click();
},
getCreateButton: function () {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('.btn');
},
clickCreateButton: function () {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('.btn').click();
},
clickDefaultIndexButton: function () {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('button.btn.btn-warning.ng-scope').click();
},
clickDeletePattern: function () {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('button.btn.btn-danger.ng-scope').click();
},
getIndexPageHeading: function () {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('h1.title.ng-binding.ng-isolate-scope');
},
getConfigureHeader: function () {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('h1');
},
getTableHeader: function () {
return this.remote.setFindTimeout(defaultTimeout)
.findAllByCssSelector('table.table.table-condensed thead tr th');
},
sortBy: function (columnName) {
return this.remote.setFindTimeout(defaultTimeout)
.findAllByCssSelector('table.table.table-condensed thead tr th span')
.then(function (chartTypes) {
function getChartType(chart) {
return chart.getVisibleText()
.then(function (chartString) {
if (chartString === columnName) {
return chart.click();
}
});
}
var getChartTypesPromises = chartTypes.map(getChartType);
return Promise.all(getChartTypesPromises);
});
},
getTableRow: function (rowNumber, colNumber) {
return this.remote.setFindTimeout(defaultTimeout)
// passing in zero-based index, but adding 1 for css 1-based indexes
.findByCssSelector('div.agg-table-paginated table.table.table-condensed tbody tr:nth-child(' +
(rowNumber + 1) + ') td.ng-scope:nth-child(' +
(colNumber + 1) + ') span.ng-binding'
);
},
getFieldsTabCount: function () {
var self = this;
var selector = 'li.kbn-settings-tab.active a small';
var getText = function () {
return self.remote.setFindTimeout(defaultTimeout)
.findByCssSelector(selector).getVisibleText();
};
return common.tryForTime(defaultTimeout, function () {
return getText();
})
.then(function () {
return getText();
})
.then(function (theText) {
// the value has () around it, remove them
return theText.replace(/\((.*)\)/, '$1');
});
},
getPageSize: function () {
var selectedItemLabel = '';
return this.remote.setFindTimeout(defaultTimeout)
.findAllByCssSelector('select.ng-pristine.ng-valid.ng-untouched option')
.then(function (chartTypes) {
function getChartType(chart) {
var thisChart = chart;
return chart.isSelected()
.then(function (isSelected) {
if (isSelected === true) {
return thisChart.getProperty('label')
.then(function (theLabel) {
selectedItemLabel = theLabel;
});
}
});
}
var getChartTypesPromises = chartTypes.map(getChartType);
return Promise.all(getChartTypesPromises);
})
.then(function () {
return selectedItemLabel;
});
},
getPageFieldCount: function () {
return this.remote.setFindTimeout(defaultTimeout)
.findAllByCssSelector('div.agg-table-paginated table.table.table-condensed tbody tr td.ng-scope:nth-child(1) span.ng-binding');
},
goToPage: function (pageNum) {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('ul.pagination-other-pages-list.pagination-sm.ng-scope li.ng-scope:nth-child(' +
(pageNum + 1) + ') a.ng-binding'
)
.then(function (page) {
return page.click();
});
},
openControlsRow: function (row) {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('table.table.table-condensed tbody tr:nth-child(' +
(row + 1) + ') td.ng-scope div.actions a.btn.btn-xs.btn-default i.fa.fa-pencil'
)
.then(function (page) {
return page.click();
});
},
openControlsByName: function (name) {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('div.actions a.btn.btn-xs.btn-default[href$="/' + name + '"]')
.then(function (button) {
return button.click();
});
},
increasePopularity: function () {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('button.btn.btn-default[aria-label="Plus"]')
.then(function (button) {
return button.click();
});
},
getPopularity: function () {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('input[ng-model="editor.field.count"]')
.then(function (input) {
return input.getProperty('value');
});
},
controlChangeCancel: function () {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('button.btn.btn-primary[aria-label="Cancel"]')
.then(function (button) {
return button.click();
});
},
controlChangeSave: function () {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('button.btn.btn-success.ng-binding[aria-label="Update Field"]')
.then(function (button) {
return button.click();
});
},
setPageSize: function (size) {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('form.form-inline.pagination-size.ng-scope.ng-pristine.ng-valid div.form-group option[label="' + size + '"]')
.then(function (button) {
return button.click();
});
},
createIndexPattern: function () {
var self = this;
return common.tryForTime(defaultTimeout, function () {
return self.selectTimeFieldOption('@timestamp')
.then(function () {
return self.getCreateButton().click();
});
})
.then(function () {
return common.tryForTime(defaultTimeout, function () {
return self.remote.getCurrentUrl()
.then(function (currentUrl) {
if (!currentUrl.match(/indices\/.+\?/)) {
throw new Error('Index pattern not created');
}
});
});
});
},
removeIndexPattern: function () {
var self = this;
var alertText;
return common.tryForTime(defaultTimeout, function () {
return self.clickDeletePattern()
.then(function () {
return self.remote.getAlertText();
})
.then(function (text) {
alertText = text;
})
.then(function () {
return self.remote.acceptAlert();
});
})
.then(function () {
return common.tryForTime(defaultTimeout, function () {
return self.remote.getCurrentUrl()
.then(function (currentUrl) {
if (currentUrl.match(/indices\/.+\?/)) {
throw new Error('Index pattern not removed');
}
});
});
})
.then(function () {
return alertText;
});
}
};
return SettingsPage;
});

View file

@ -2,17 +2,36 @@ var expect = require('expect.js');
var getUrl = require('../getUrl');
describe('getUrl', function () {
it('should be able to convert a config and a path to a url', function () {
expect(getUrl({
it('should convert to a url', function () {
var url = getUrl({
protocol: 'http',
hostname: 'localhost',
}, {
pathname: 'foo'
});
expect(url).to.be('http://localhost/foo');
});
it('should convert to a secure url with port', function () {
var url = getUrl({
protocol: 'http',
hostname: 'localhost',
port: 9220
}, 'foo')).to.be('http://localhost:9220/foo');
}, {
pathname: 'foo'
});
expect(url).to.be('http://localhost:9220/foo');
});
it('should convert to a secure hashed url', function () {
expect(getUrl({
protocol: 'https',
hostname: 'localhost',
}, 'foo')).to.be('https://localhost/foo');
}, {
pathname: 'foo',
hash: 'bar'
})).to.be('https://localhost/foo#bar');
});
});

View file

@ -1,7 +1,6 @@
var _ = require('lodash');
var url = require('url');
/**
* Converts a config and a pathname to a url
* @param {object} config A url config
@ -11,11 +10,14 @@ var url = require('url');
* hostname: 'localhost',
* port: 9220
* }
* @param {string} pathname The requested path
* @param {object} app The params to append
* example:
* {
* pathname: 'app/kibana',
* hash: '/discover'
* }
* @return {string}
*/
module.exports = function getPage(config, pathname) {
return url.format(_.assign(config, {
pathname: pathname
}));
module.exports = function getPage(config, app) {
return url.format(_.assign(config, app));
};