Merge branch 'master' of github.com:elasticsearch/kibana into feature/config-migration

Conflicts:
	package.json
	src/server/routes/proxy.js
	tasks/test.js
This commit is contained in:
Chris Cowan 2015-01-28 13:17:40 -07:00
commit 22bfd9e079
36 changed files with 384 additions and 107 deletions

2
docs/access.asciidoc Normal file
View file

@ -0,0 +1,2 @@
[[access]]
== Accessing Kibana

2
docs/dashboard.asciidoc Normal file
View file

@ -0,0 +1,2 @@
[[dashboard]]
== Working with Dashboards

2
docs/discover.asciidoc Normal file
View file

@ -0,0 +1,2 @@
[[discover]]
== Discovering your Data

22
docs/index.asciidoc Normal file
View file

@ -0,0 +1,22 @@
[[kibana-guide]]
= Kibana User Guide
include::introduction.asciidoc[]
include::setup.asciidoc[]
include::access.asciidoc[]
include::discover.asciidoc[]
include::visualize.asciidoc[]
include::dashboard.asciidoc[]
include::settings.asciidoc[]

View file

@ -0,0 +1,2 @@
[[introduction]]
== Introduction

2
docs/settings.asciidoc Normal file
View file

@ -0,0 +1,2 @@
[[settings]]
== Configuring Kibana

2
docs/setup.asciidoc Normal file
View file

@ -0,0 +1,2 @@
[[setup]]
== Getting Kibana Up and Running

2
docs/visualize.asciidoc Normal file
View file

@ -0,0 +1,2 @@
[[visualize]]
== Visualizing your Data

View file

@ -47,14 +47,15 @@
"elasticsearch": "^3.1.1",
"express": "~4.10.6",
"glob": "^4.3.2",
"http-proxy": "^1.8.1",
"jade": "~1.8.2",
"js-yaml": "^3.2.5",
"less-middleware": "1.0.x",
"lodash": "^2.4.1",
"morgan": "~1.5.1",
"semver": "^4.2.0",
"serve-favicon": "~2.2.0"
"request": "^2.40.0",
"serve-favicon": "~2.2.0",
"ssl-root-cas": "^1.1.7"
},
"devDependencies": {
"connect": "~2.19.5",
@ -91,7 +92,6 @@
"opn": "~1.0.0",
"path-browserify": "0.0.0",
"progress": "^1.1.8",
"request": "^2.40.0",
"requirejs": "~2.1.14",
"rjs-build-analysis": "0.0.3",
"simple-git": "^0.11.0",

View file

@ -17,5 +17,6 @@
class="form-control"
name="interval"
min="0"
input-whole-number
>
</div>

View file

@ -19,7 +19,6 @@ define(function (require) {
if (updater.fired) return;
updater.fired = true;
var method;
var body;
var updated = [];
@ -44,6 +43,9 @@ define(function (require) {
queue.forEach(function (q) { q.resolve(resp); });
}, function (err) {
queue.forEach(function (q) { q.reject(err); });
})
.finally(function () {
$rootScope.$emit('change:config', updated.concat(deleted));
});
};
@ -68,7 +70,7 @@ define(function (require) {
var defer = Promise.defer();
queue.push(defer);
notify.log('config change: ' + key + ': ' + oldVal + ' -> ' + newVal);
$rootScope.$broadcast('change:config.' + key, newVal, oldVal);
$rootScope.$emit('change:config.' + key, newVal, oldVal);
// reset the fire timer
clearTimeout(timer);

View file

@ -25,7 +25,15 @@ define(function (require) {
{ from: 'now-12h', to: 'now', display: 'Last 12 hours', section: 2 },
{ from: 'now-24h', to: 'now', display: 'Last 24 hours', section: 2 },
{ from: 'now-7d', to: 'now', display: 'Last 7 days', section: 2 },
{ from: 'now-30d', to: 'now', display: 'Last 30 days', section: 2 },
{ from: 'now-30d', to: 'now', display: 'Last 30 days', section: 3 },
{ from: 'now-60d', to: 'now', display: 'Last 60 days', section: 3 },
{ from: 'now-90d', to: 'now', display: 'Last 90 days', section: 3 },
{ from: 'now-6M', to: 'now', display: 'Last 6 months', section: 3 },
{ from: 'now-1y', to: 'now', display: 'Last 1 year', section: 3 },
{ from: 'now-2y', to: 'now', display: 'Last 2 years', section: 3 },
{ from: 'now-5y', to: 'now', display: 'Last 5 years', section: 3 },
]);
});

View file

@ -91,7 +91,7 @@ define(function (require) {
}
if (obj.splits) {
d3.select(this.el).select('.' + obj.class).call(obj.splits);
d3.select(this.el).select('.' + obj.class).call(obj.splits, obj.parent);
}
if (obj.children) {

View file

@ -9,7 +9,7 @@ define(function () {
* if not data.rows or data.columns, return no chart titles
*/
return function (selection) {
return function (selection, parent) {
selection.each(function (data) {
var div = d3.select(this);
@ -24,9 +24,9 @@ define(function () {
.attr('class', 'chart-title');
if (data.rows) {
d3.select('.x-axis-chart-title').remove();
d3.select(parent).select('.x-axis-chart-title').remove();
} else {
d3.select('.y-axis-chart-title').remove();
d3.select(parent).select('.y-axis-chart-title').remove();
}
return div;

View file

@ -208,10 +208,10 @@ define(function (require) {
selection.each(function () {
axis = d3.select(this);
labels = axis.selectAll('.tick text');
if (!ordered || ordered === undefined) {
axis.call(self.rotateAxisLabels());
} else {
if (ordered && ordered.date) {
axis.call(self.filterAxisLabels());
} else {
axis.call(self.rotateAxisLabels());
}
});

View file

@ -164,6 +164,7 @@
min-height: 15px;
max-height: 15px;
min-width: 20px;
overflow: hidden;
}
.x-axis-div {

View file

@ -187,8 +187,10 @@ define(function (require) {
// Append the bars
circles = layer
.selectAll('rect')
.data(function appendData(d) {
return d;
.data(function appendData(data) {
return data.filter(function isNotZero(d) {
return d.y !== 0;
});
});
// exit

View file

@ -0,0 +1,19 @@
define(function (require) {
var module = require('modules').get('kibana');
module.directive('inputWholeNumber', function () {
return {
restrict: 'A',
require: 'ngModel',
link: function ($scope, $elem, attrs, ngModel) {
ngModel.$parsers.push(checkWholeNumber);
ngModel.$formatters.push(checkWholeNumber);
function checkWholeNumber(value) {
ngModel.$setValidity('whole', value % 1 === 0);
return value;
}
}
};
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -7,6 +7,22 @@
<meta name="viewport" content="width=device-width">
<link rel="shortcut icon" href="styles/theme/elk.ico">
<title>Kibana 4</title>
<link rel="stylesheet" href="styles/main.css?_b=@@buildNum">
</head>
<body kibana ng-class="'application-' + activeApp.id">
<div class="row">
<div class="col-md-offset-4 col-md-4 page-header initial-load">
<center>
<img width="128" src="images/initial_load.gif">
<h1>
<strong>Kibana</strong>
<small>is loading. Give me a moment here. I'm loading a whole bunch of code. Don't worry, all this good stuff will be cached up for next time!</small>
</h1>
</center>
</div>
</div>
<script>
window.KIBANA_VERSION='@@version';
@ -14,7 +30,6 @@
window.KIBANA_COMMIT_SHA='@@commitSha';
</script>
<link rel="stylesheet" href="styles/main.css?_b=@@buildNum">
<script src="bower_components/requirejs/require.js?_b=@@buildNum"></script>
<script src="require.config.js?_b=@@buildNum"></script>
<script>
@ -25,6 +40,6 @@
require(['kibana'], function (kibana) { kibana.init(); });
</script>
</head>
<body kibana ng-class="'application-' + activeApp.id"></body>
</body>
</html>

View file

@ -5,6 +5,7 @@
</div>
<div class="clearfix visible-xs-block"></div>
<input
input-focus
ng-model="filter"
ng-attr-placeholder="{{noun}} Filter"
class="form-control"

View file

@ -10,59 +10,52 @@ define(function (require) {
});
require('modules').get('apps/settings')
.directive('kbnSettingsAdvanced', function (config, Notifier, Private) {
.directive('kbnSettingsAdvanced', function (config, Notifier, Private, $rootScope) {
return {
restrict: 'E',
link: function ($scope) {
var notify = new Notifier();
var configVals = config._vals();
var keyCodes = {
ESC: 27
};
// determine if a value is too complex to be edditted (at this time)
var tooComplex = function (conf) {
// get the type of the current value or the default
switch (conf.type) {
case 'string':
case 'number':
case 'null':
case 'undefined':
conf.normal = true;
break;
case 'json':
conf.json = true;
break;
default:
if (_.isArray(config.get(conf.name))) {
conf.array = true;
} else if (typeof config.get(conf.name) === 'boolean') {
conf.bool = true;
} else {
conf.tooComplex = true;
}
}
};
var NAMED_EDITORS = ['json', 'array', 'boolean'];
var NORMAL_EDITOR = ['number', 'string', 'null', 'undefined'];
$scope.configs = _.map(configDefaults, function (def, name) {
var conf = {
name: name,
defVal: def.value,
type: (def.type || typeof config.get(name)),
description: def.description,
value: configVals[name]
};
function getEditorType(conf) {
if (_.contains(NORMAL_EDITOR, conf.type)) return 'normal';
if (_.contains(NAMED_EDITORS, conf.type)) return conf.type;
}
tooComplex(conf);
function isTypeComplex(conf) {
return !(conf.json || conf.array || conf.bool || conf.normal);
}
$scope.$on('change:config.' + name, function () {
configVals = config._vals();
conf.value = configVals[name];
tooComplex(conf);
function readConfigVals() {
var configVals = config._vals();
$scope.configs = _.map(configDefaults, function (def, name) {
var val = configVals[name];
var conf = {
name: name,
defVal: def.value,
type: (def.type || _.isArray(val) || typeof val),
description: def.description,
value: val,
};
var editor = getEditorType(conf);
conf.json = editor === 'json';
conf.bool = editor === 'bool';
conf.array = editor === 'array';
conf.normal = editor === 'normal';
conf.tooComplex = !editor;
return conf;
});
}
return conf;
});
readConfigVals();
$rootScope.$on('change:config', readConfigVals);
}
};
});

View file

@ -15,7 +15,7 @@ define(function (require) {
// wrapper directive, which sets some global stuff up like the left nav
require('modules').get('apps/settings')
.directive('kbnSettingsIndices', function ($route, config, kbnUrl) {
.directive('kbnSettingsIndices', function ($route, config, kbnUrl, $rootScope) {
return {
restrict: 'E',
transclude: true,
@ -23,7 +23,7 @@ define(function (require) {
link: function ($scope) {
$scope.edittingId = $route.current.params.id;
$scope.defaultIndex = config.get('defaultIndex');
$scope.$on('change:config.defaultIndex', function () {
$rootScope.$on('change:config.defaultIndex', function () {
$scope.defaultIndex = config.get('defaultIndex');
});

View file

@ -1,5 +1,5 @@
<div ng-controller="KbnTableVisController" class="table-vis">
<div ng-if="!hasSomeRows" class="table-vis-error">
<div ng-if="!hasSomeRows && hasSomeRows !== null" class="table-vis-error">
<h2><i class="fa fa-meh-o"></i></h2>
<h4>No results found</h4>
</div>

View file

@ -434,6 +434,10 @@ textarea {
resize: vertical;
}
.initial-load {
margin-top: 60px;
}
.field-collapse-toggle {
color: #999;
margin-left: 10px !important;

View file

@ -3,6 +3,8 @@ define(function (require) {
var moment = require('moment');
var datemath = require('utils/datemath');
require('directives/input_whole_number');
/**
* Calculate a graph interval
*

View file

@ -34,3 +34,7 @@ shard_timeout: 0
# certificate.
verify_ssl: true
# If you need to provide a CA certificate for your Elasticsarech instance, put
# the path of the pem file here.
# ca: /path/to/your/CA.pem

View file

@ -1,52 +1,104 @@
var logger = require('../lib/logger');
var express = require('express');
var router = module.exports = express.Router();
var httpProxy = require('http-proxy');
var config = require('../config');
var request = require('request');
var buffer = require('buffer');
var querystring = require('querystring');
var express = require('express');
var _ = require('lodash');
var fs = require('fs');
var url = require('url');
var target = url.parse(config.elasticsearch);
var proxy = new httpProxy.createProxyServer({});
var buffer = require('buffer');
var join = require('path').join;
proxy.on('proxyReq', function (proxyReq, req, res, options) {
// To support the elasticsearch_preserve_host feature we need to change the
// host header to the target host header.
if (config.kibana.elasticsearch_preserve_host) {
proxyReq.setHeader('host', target.host);
// If the target is backed by an SSL and a CA is provided via the config
// then we need to inject the CA
var hasCustomCA = false;
if (/^https/.test(target.protocol) && config.kibana.ca) {
var sslRootCAs = require('ssl-root-cas/latest');
sslRootCAs.inject();
var ca = fs.readFileSync(config.kibana.ca, 'utf8');
var https = require('https');
https.globalAgent.options.ca.push(ca);
hasCustomCA = true;
}
// Create the router
var router = module.exports = express.Router();
// We need to capture the raw body before moving on
router.use(function (req, res, next) {
req.rawBody = '';
req.setEncoding('utf8');
req.on('data', function (chunk) {
req.rawBody += chunk;
});
req.on('end', next);
});
function getPort(req) {
var matches = req.headers.host.match(/:(\d+)/);
if (matches[1]) return matches[1];
return req.connection.pair ? '443' : '80';
}
// Create the proxy middleware
router.use(function (req, res, next) {
var uri = _.defaults({}, target);
var options = {
url: uri.protocol + '//' + uri.host + req.url,
method: req.method,
headers: _.defaults({ host: target.hostname }, req.headers),
strictSSL: config.kibana.verify_ssl,
timeout: config.kibana.request_timeout
};
options.headers['x-forward-for'] = req.connection.remoteAddress || req.socket.remoteAddress;
options.headers['x-forward-port'] = getPort(req);
options.headers['x-forward-proto'] = req.connection.pair ? 'https' : 'http';
// If the server has a custom CA we need to add it to the agent options
if (hasCustomCA) {
options.agentOptions = { ca: https.globalAgent.options.ca };
}
// Only send the body if it's a PATCH, PUT, or POST
if (req.rawBody) {
options.body = req.rawBody;
}
// Support for handling basic auth
if (config.kibana.elasticsearch_username && config.kibana.elasticsearch_password) {
var code = new buffer.Buffer(config.kibana.elasticsearch_username + ':' + config.kibana.elasticsearch_password);
var auth = 'Basic ' + code.toString('base64');
proxyReq.setHeader('authorization', auth);
}
});
// Error handling for the proxy
proxy.on('error', function (err, req, res) {
var code = 502;
var body = { message: 'Bad Gateway' };
if (err.code === 'ECONNREFUSED') {
body.message = 'Unable to connect to Elasticsearch';
options.headers.authorization = auth;
}
if (err.message === 'DEPTH_ZERO_SELF_SIGNED_CERT') {
body.message = 'SSL handshake with Elasticsearch failed';
// To support the elasticsearch_preserve_host feature we need to change the
// host header to the target host header. I don't quite understand the value
// of this... but it's a feature we had before so I guess we are keeping it.
if (config.kibana.elasticsearch_preserve_host) {
options.headers.host = target.host;
}
res.writeHead(502, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(body));
});
router.use(function (req, res, next) {
var options = {
target: config.elasticsearch,
secure: config.kibana.verify_ssl,
xfwd: true,
timeout: (config.kibana.request_timeout)
};
proxy.web(req, res, options);
// Create the request and pipe the response
var esRequest = request(options);
esRequest.on('error', function (err) {
var code = 502;
var body = { message: 'Bad Gateway' };
if (err.code === 'ECONNREFUSED') {
body.message = 'Unable to connect to Elasticsearch';
}
if (err.message === 'DEPTH_ZERO_SELF_SIGNED_CERT') {
body.message = 'SSL handshake with Elasticsearch failed';
}
body.err = err.message;
res.status(code).json(body);
});
esRequest.pipe(res);
});

View file

@ -4,7 +4,7 @@ module.exports = function (grunt) {
grunt.registerTask('npm_install_kibana', 'NPM isntall kibana server into dist', function () {
var done = this.async();
var cwd = join(grunt.config.get('build'), 'dist', 'kibana', 'src');
var command = 'npm install --production';
var command = 'npm install --production --no-optional';
var options = { cwd: cwd };
child_process.exec(command, options, function (err, stdout, stderr) {
if (err) {

View file

@ -32,7 +32,6 @@ module.exports = function (grunt) {
];
addTestTask(tasks);
if (process.env.TRAVIS) tasks.unshift('esvm:dev');
addTestTask(tasks);
grunt.task.run(tasks);
});

View file

@ -0,0 +1,48 @@
define(function (require) {
var angular = require('angular');
require('directives/input_whole_number');
describe('Whole number input directive', function () {
var $compile, $rootScope;
var html = '<input type="text" ng-model="value" input-whole-number />';
beforeEach(module('kibana'));
beforeEach(inject(function (_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
it('should allow whole numbers', function () {
var element = $compile(html)($rootScope);
$rootScope.value = '123';
$rootScope.$digest();
expect(element.hasClass('ng-valid')).to.be.ok();
$rootScope.value = '1.0';
$rootScope.$digest();
expect(element.hasClass('ng-valid')).to.be.ok();
$rootScope.value = '-5.0';
$rootScope.$digest();
expect(element.hasClass('ng-valid')).to.be.ok();
});
it('should disallow numbers with decimals', function () {
var element = $compile(html)($rootScope);
$rootScope.value = '123.0';
$rootScope.$digest();
expect(element.hasClass('ng-valid')).to.be.ok();
$rootScope.value = '1.2';
$rootScope.$digest();
expect(element.hasClass('ng-invalid')).to.be.ok();
$rootScope.value = '-5.5';
$rootScope.$digest();
expect(element.hasClass('ng-invalid')).to.be.ok();
});
});
});

View file

@ -153,8 +153,8 @@ define(function (require) {
$scope.$digest();
});
it('should contain 3 lists of options', function (done) {
expect($elem.find('.kbn-timepicker-section .list-unstyled').length).to.be(3);
it('should contain 4 lists of options', function (done) {
expect($elem.find('.kbn-timepicker-section .list-unstyled').length).to.be(4);
done();
});

View file

@ -7,7 +7,7 @@ define(function (require) {
$('body').append('<div class=visualize-chart></div>');
var $el = $('.visualize-chart');
var $el = $('.visualize-chart:last');
$el.width(1024);
$el.height(300);

View file

@ -183,6 +183,23 @@ define(function (require) {
expect($(chart.chartEl).find('circle').length).to.be.greaterThan(0);
});
});
it('should not draw circles where d.y === 0', function () {
vis.handler.charts.forEach(function (chart) {
var series = chart.chartData.series;
var isZero = series.some(function (d) {
return d.y === 0;
});
var circles = $.makeArray($(chart.chartEl).find('circle'));
var isNotDrawn = circles.some(function (d) {
return d.__data__.y === 0;
});
if (isZero) {
expect(isNotDrawn).to.be(false);
}
});
});
});
// Cannot seem to get these tests to work on the box

View file

@ -7,24 +7,24 @@ define(function (require) {
// Data
var series = require('vislib_fixtures/mock_data/date_histogram/_series');
var termsColumns = require('vislib_fixtures/mock_data/terms/_columns');
var histogramRows = require('vislib_fixtures/mock_data/histogram/_rows');
//var histogramRows = require('vislib_fixtures/mock_data/histogram/_rows');
var stackedSeries = require('vislib_fixtures/mock_data/date_histogram/_stacked_series');
var dataArray = [
series,
termsColumns,
histogramRows,
//histogramRows,
stackedSeries
];
var names = [
'series',
'terms columns',
'histogram rows',
//'histogram rows',
'stackedSeries'
];
var modes = [
'stacked',
'grouped',
'percentage',
//'percentage',
'stacked'
];

View file

@ -47,6 +47,79 @@ define(function (require) {
120
];
describe('No global chart settings', function () {
var visLibParams1 = {
el: '<div class=chart1></div>',
type: 'pie',
addLegend: true,
addTooltip: true
};
var visLibParams2 = {
el: '<div class=chart2></div>',
type: 'pie',
addLegend: true,
addTooltip: true
};
var chart1;
var chart2;
var Vis;
var indexPattern;
var buildHierarchicalData;
var data1;
var data2;
beforeEach(function () {
module('PieChartFactory');
});
beforeEach(function () {
inject(function (d3, Private) {
chart1 = Private(require('vislib_fixtures/_vis_fixture'))(visLibParams1);
chart2 = Private(require('vislib_fixtures/_vis_fixture'))(visLibParams2);
Vis = Private(require('components/vis/vis'));
indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern'));
buildHierarchicalData = Private(require('components/agg_response/hierarchical/build_hierarchical_data'));
require('css!components/vislib/styles/main');
var id_1 = 1;
var id_2 = 1;
var stubVis1 = new Vis(indexPattern, {
type: 'pie',
aggs: rowAgg
});
var stubVis2 = new Vis(indexPattern, {
type: 'pie',
aggs: colAgg
});
// We need to set the aggs to a known value.
_.each(stubVis1.aggs, function (agg) {
agg.id = 'agg_' + id_1++;
});
_.each(stubVis2.aggs, function (agg) {
agg.id = 'agg_' + id_2++;
});
data1 = buildHierarchicalData(stubVis1, fixtures.threeTermBuckets);
data2 = buildHierarchicalData(stubVis2, fixtures.threeTermBuckets);
chart1.render(data1);
chart2.render(data2);
});
});
afterEach(function () {
$('.visualize-chart').remove();
chart1 = null;
chart2 = null;
});
it('should render chart titles for all charts', function () {
expect($(chart1.el).find('.y-axis-chart-title').length).to.be(1);
expect($(chart2.el).find('.x-axis-chart-title').length).to.be(1);
});
});
aggArray.forEach(function (dataAgg, i) {
describe('Vislib PieChart Class Test Suite for ' + names[i] + ' data', function () {
var visLibParams = {