Merge branch 'master' of github.com:elasticsearch/kibana into fixitfriday/auto-select-first

This commit is contained in:
Rashid Khan 2015-03-06 15:21:09 -07:00
commit 706aa3c3f1
48 changed files with 605 additions and 152 deletions

4
FAQ.md
View file

@ -11,3 +11,7 @@
**Q:** What happened to templated/scripted dashboards?
**A:** Check out the URL. The state of each app is stored there, including any filters, queries or columns. This should be a lot easier than constructing scripted dashboards. The encoding of the URL is RISON.
**Q:** I'm getting `bin/node/bin/node: not found` but I can see the node binary in the package?
**A:** Kibana 4 packages are architecture specific. Ensure you are using the correct package for your architecture.

View file

@ -1,13 +1,10 @@
[[production]]
== Using Kibana in a Production Environment
When you set up Kibana in a production environment, rather than on your local
machine, you need to consider:
* <<configuring-kibana-shield, Configuring Kibana to Work with Shield>>
* <<enabling-ssl, Enabling SSL>>
* <<controlling-access, Controlling Access>>
* <<load-balancing, Load Balancing Across Multiple Elasticsearch Nodes>>
* Where you are going to run Kibana.
* Whether you need to encrypt communications to and from Kibana.
* If you need to control access to your data.
=== Deployment Considerations
How you deploy Kibana largely depends on your use case. If you are the only user,
you can run Kibana on your local machine and configure it to point to whatever
Elasticsearch instance you want to interact with. Conversely, if you have a large
@ -15,35 +12,19 @@ number of heavy Kibana users, you might need to load balance across multiple
Kibana instances that are all connected to the same Elasticsearch instance.
While Kibana isn't terribly resource intensive, we still recommend running Kibana
on its own node, rather than on one of your Elasticsearch nodes.
separate from your Elasticsearch data or master nodes. To distribute Kibana
traffic across the nodes in your Elasticsearch cluster, you can run Kibana
and an Elasticsearch client node on the same machine. For more information, see
<<load-balancing, Load Balancing Across Multiple Elasticsearch Nodes>>.
[float]
[[configuring-kibana-shield]]
=== Configuring Kibana to Work with Shield
If you are using Shield to authenticate Elasticsearch users, you need to provide
Kibana with user credentials so it can access the `.kibana` index. The Kibana user
needs permission to perform the following actions on the `.kibana` index:
the Kibana server with credentials so it can access the `.kibana` index and monitor
the cluster.
----
'.kibana':
- indices:admin/create
- indices:admin/exists
- indices:admin/mapping/put
- indices:admin/mappings/fields/get
- indices:admin/refresh
- indices:admin/validate/query
- indices:data/read/get
- indices:data/read/mget
- indices:data/read/search
- indices:data/write/delete
- indices:data/write/index
- indices:data/write/update
- indices:admin/create
----
For more information about configuring access in Shield,
see https://www.elasticsearch.org/guide/en/shield/current/_shield_with_kibana_4.html[Shield with Kibana 4]
in the Shield documentation.
To configure credentials for Kibana, set the `kibana_elasticsearch_username` and
To configure credentials the Kibana server, set the `kibana_elasticsearch_username` and
`kibana_elasticsearch_password` properties in `kibana.yml`:
----
@ -51,6 +32,13 @@ To configure credentials for Kibana, set the `kibana_elasticsearch_username` and
kibana_elasticsearch_username: kibana4
kibana_elasticsearch_password: kibana4
----
For information about assigning the Kibana server the necessary permissions in Shield,
see https://www.elasticsearch.org/guide/en/shield/current/_shield_with_kibana_4.html[Shield with Kibana 4]
in the Shield documentation.
[float]
[[enabling-ssl]]
=== Enabling SSL
Kibana supports SSL encryption for both client requests and the requests the Kibana server
sends to Elasticsearch.
@ -82,6 +70,8 @@ If you are using a self-signed certificate for Elasticsearch, set the `ca` prope
ca: /path/to/your/ca/cacert.pem
----
[float]
[[controlling-access]]
=== Controlling access
You can use http://www.elasticsearch.org/overview/shield/[Elasticsearch Shield]
(Shield) to control what Elasticsearch data users can access through Kibana.
@ -89,6 +79,47 @@ Shield provides index-level access control. If a user isn't authorized to run
the query that populates a Kibana visualization, the user just sees an empty
visualization.
To configure access to Kibana using Shield, you create one or more Shield roles
To configure access to Kibana using Shield, you create Shield roles
for Kibana using the `kibana4` default role as a starting point. For more
information, see http://www.elasticsearch.org/guide/en/shield/current/_shield_with_kibana_4.html[Using Shield with Kibana 4].
information, see http://www.elasticsearch.org/guide/en/shield/current/_shield_with_kibana_4.html[Using Shield with Kibana 4].
[float]
[[load-balancing]]
=== Load Balancing Across Multiple Elasticsearch Nodes
If you have multiple nodes in your Elasticsearch cluster, the easiest way to distribute Kibana requests
across the nodes is to run an Elasticsearch _client_ node on the same machine as Kibana.
Elasticsearch client nodes are essentially smart load balancers that are part of the cluster. They
process incoming HTTP requests, redirect operations to the other nodes in the cluster as needed, and
gather and return the results. For more information, see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/modules-node.html[Node] in the Elasticsearch reference.
To use a local client node to load balance Kibana requests:
. Install Elasticsearch on the same machine as Kibana.
. Configure the node as a client node. In `elasticsearch.yml`, set both `node.data` and `node.master` to `false`:
+
--------
# 3. You want this node to be neither master nor data node, but
# to act as a "search load balancer" (fetching data from nodes,
# aggregating results, etc.)
#
node.master: false
node.data: false
--------
. Configure the client node to join your Elasticsearch cluster. In `elasticsearch.yml`, set the `cluster.name` to the
name of your cluster.
+
--------
cluster.name: "my_cluster"
--------
. Make sure Kibana is configured to point to your local client node. In `kibana.yml`, the `elasticsearch_url` should be set to
`localhost:9200`.
+
--------
# The Elasticsearch instance to use for all your queries.
elasticsearch_url: "http://localhost:9200"
--------

View file

@ -56,7 +56,8 @@
"request": "^2.40.0",
"requirefrom": "^0.2.0",
"semver": "^4.2.0",
"serve-favicon": "~2.2.0"
"serve-favicon": "~2.2.0",
"through": "^2.3.6"
},
"devDependencies": {
"connect": "~2.19.5",
@ -96,6 +97,8 @@
"requirejs": "~2.1.14",
"rjs-build-analysis": "0.0.3",
"simple-git": "^0.11.0",
"sinon": "^1.12.2",
"sinon-as-promised": "^2.0.3",
"tar": "^1.0.1"
}
}

View file

@ -25,7 +25,11 @@
</td>
<td>
<i bo-if="!mapping[field] && !showArrayInObjectsWarning(doc, field)"
<i bo-if="!mapping[field] && field[0] === '_'"
tooltip-placement="top"
tooltip="Field names beginning with _ are not supported"
class="fa fa-warning text-color-warning ng-scope doc-viewer-underscore"></i>
<i bo-if="!mapping[field] && field[0] !== '_' && !showArrayInObjectsWarning(doc, field)"
tooltip-placement="top"
tooltip="No cached mapping for this field. Refresh your mapping from the Settings > Indices page"
class="fa fa-warning text-color-warning ng-scope doc-viewer-no-mapping"></i>
@ -43,4 +47,4 @@
<pre ng-show="mode == 'json'">{{hit | json}}</pre>
</div>
</div>
</div>

View file

@ -27,6 +27,9 @@ define(function (require) {
$scope.formatted = _.mapValues($scope.flattened, function (value, name) {
var mapping = $scope.mapping[name];
var formatter = (mapping && mapping.format) ? mapping.format : defaultFormat;
if (_.isArray(value) && typeof value[0] === 'object') {
value = JSON.stringify(value, null, ' ');
}
return formatter.convert(value);
});
$scope.fields = _.keys($scope.flattened).sort();
@ -39,4 +42,4 @@ define(function (require) {
}
};
});
});
});

View file

@ -91,12 +91,14 @@
</div>
<br>
<div class="small">
<input
ng-model="relative.round"
ng-checked="relative.round"
ng-change="formatRelative()"
type="checkbox">
round to the {{units[relative.unit]}}
<label>
<input
ng-model="relative.round"
ng-checked="relative.round"
ng-change="formatRelative()"
type="checkbox">
round to the {{units[relative.unit]}}
</label>
</div>
</div>
@ -169,7 +171,11 @@
<br>
<div ng-repeat="list in refreshLists" class="kbn-refresh-section">
<ul class="list-unstyled">
<li ng-repeat="interval in list"><a ng-click="setRefreshInterval(interval)">{{interval.display}}</a></li>
<li ng-repeat="inter in list">
<a class="refresh-interval" ng-class="{ 'refresh-interval-active': interval.value == inter.value }" ng-click="setRefreshInterval(inter)">
{{inter.display}}
</a>
</li>
</ul>
</div>
</div>

View file

@ -19,7 +19,7 @@ define(function (require) {
this.handler = handler;
this.dispatch = d3.dispatch('brush', 'click', 'hover', 'mouseup',
'mousedown', 'mouseover');
'mousedown', 'mouseover', 'mouseout');
}
/**
@ -92,6 +92,7 @@ define(function (require) {
};
};
/**
*
* @method addHoverEvent
@ -101,6 +102,7 @@ define(function (require) {
var self = this;
var isClickable = (this.dispatch.on('click'));
var addEvent = this.addEvent;
var $el = this.handler.el;
function hover(d, i) {
d3.event.stopPropagation();
@ -110,12 +112,32 @@ define(function (require) {
self.addMousePointer.call(this, arguments);
}
self.highlightLegend.call(this, $el);
self.dispatch.hover.call(this, self.eventResponse(d, i));
}
return addEvent('mouseover', hover);
};
/**
*
* @method addMouseoutEvent
* @returns {Function}
*/
Dispatch.prototype.addMouseoutEvent = function () {
var self = this;
var addEvent = this.addEvent;
var $el = this.handler.el;
function mouseout() {
d3.event.stopPropagation();
self.unHighlightLegend.call(this, $el);
}
return addEvent('mouseout', mouseout);
};
/**
*
* @method addClickEvent
@ -189,7 +211,7 @@ define(function (require) {
/**
* Mouse over Behavior
* Mouseover Behavior
*
* @method addMousePointer
* @returns {D3.Selection}
@ -198,6 +220,38 @@ define(function (require) {
return d3.select(this).style('cursor', 'pointer');
};
/**
* Mouseover Behavior
*
* @param element {D3.Selection}
* @method highlightLegend
*/
Dispatch.prototype.highlightLegend = function (element) {
var classList = d3.select(this).node().classList;
var liClass = d3.select(this).node().classList[1];
d3.select(element)
.select('.legend-ul')
.selectAll('li.color')
.filter(function (d, i) {
return d3.select(this).node().classList[1] !== liClass;
})
.classed('blur_shape', true);
};
/**
* Mouseout Behavior
*
* @param element {D3.Selection}
* @method unHighlightLegend
*/
Dispatch.prototype.unHighlightLegend = function (element) {
d3.select(element)
.select('.legend-ul')
.selectAll('li.color')
.classed('blur_shape', false);
};
/**
* Adds D3 brush to SVG and returns the brush function
*

View file

@ -85,14 +85,16 @@ define(function (require) {
};
/**
* Creates a class name based on the colors assigned to each label
* Creates a class name based on the hexColor assigned to each label
*
* @method colorToClass
* @param name {String} Label
* @param hexColor {String} Label
* @returns {string} CSS class name
*/
Legend.prototype.colorToClass = function (name) {
return 'c' + name.replace(/[#]/g, '');
Legend.prototype.colorToClass = function (hexColor) {
if (hexColor) {
return 'c' + hexColor.replace(/[#]/g, '');
}
};
/**
@ -131,35 +133,70 @@ define(function (require) {
}
});
visEl.selectAll('.color')
legendDiv.select('.legend-ul').selectAll('li')
.on('mouseover', function (d) {
var liClass = '.' + self.colorToClass(self.color(d));
visEl.selectAll('.color').classed('blur_shape', true);
var liClass = self.colorToClass(self.color(d));
var charts = visEl.selectAll('.chart');
// legend
legendDiv.selectAll('li')
.filter(function (d) {
return d3.select(this).node().classList[1] !== liClass;
})
.classed('blur_shape', true);
// lines/area
charts.selectAll('.color')
.filter(function (d) {
return d3.select(this).node().classList[1] !== liClass;
})
.classed('blur_shape', true);
// circles
charts.selectAll('.line circle')
.filter(function (d) {
return d3.select(this).node().classList[1] !== liClass;
})
.classed('blur_shape', true);
// pie slices
charts.selectAll('.slice')
.filter(function (d) {
return d3.select(this).node().classList[1] !== liClass;
})
.classed('blur_shape', true);
var eventEl = d3.select(this);
eventEl.style('white-space', 'inherit');
eventEl.style('word-break', 'break-all');
// select series on chart
visEl.selectAll(liClass).classed('blur_shape', false);
})
.on('mouseout', function () {
/*
* The default opacity of elements in charts may be modified by the
* chart constructor, and so may differ from that of the legend
*/
visEl.selectAll('.chart')
.selectAll('.color')
var charts = visEl.selectAll('.chart');
// legend
legendDiv.selectAll('li')
.classed('blur_shape', false);
// lines/areas
charts.selectAll('.color')
.classed('blur_shape', false);
// circles
charts.selectAll('.line circle')
.classed('blur_shape', false);
// pie slices
charts.selectAll('.slice')
.classed('blur_shape', false);
var eventEl = d3.select(this);
eventEl.style('white-space', 'nowrap');
eventEl.style('word-break', 'inherit');
// Legend values should always return to their default opacity of 1
visEl.select('.legend-ul')
.selectAll('.color')
.classed('blur_shape', false);
});
};

View file

@ -55,7 +55,7 @@ define(function (require) {
* @returns {String} CSS class name
*/
Chart.prototype.colorToClass = function (label) {
return 'color ' + Legend.prototype.colorToClass.call(null, label);
return Legend.prototype.colorToClass.call(null, label);
};
/**

View file

@ -96,7 +96,7 @@ define(function (require) {
// Append path
path = layer.append('path')
.attr('class', function (d) {
return self.colorToClass(color(d[0].label));
return 'color ' + self.colorToClass(color(d[0].label));
})
.style('fill', function (d) {
return color(d[0].label);
@ -125,8 +125,9 @@ define(function (require) {
var isBrushable = events.isBrushable();
var brush = isBrushable ? events.addBrushEvent(svg) : undefined;
var hover = events.addHoverEvent();
var mouseout = events.addMouseoutEvent();
var click = events.addClickEvent();
var attachedEvents = element.call(hover).call(click);
var attachedEvents = element.call(hover).call(mouseout).call(click);
if (isBrushable) {
attachedEvents.call(brush);
@ -144,6 +145,7 @@ define(function (require) {
* @returns {D3.UpdateSelection} SVG with circles added
*/
AreaChart.prototype.addCircles = function (svg, data) {
var self = this;
var color = this.handler.data.getColorFunc();
var xScale = this.handler.xAxis.xScale;
var yScale = this.handler.yAxis.yScale;
@ -162,7 +164,7 @@ define(function (require) {
.append('g')
.attr('class', 'points area');
// Append the bars
// append the bars
circles = layer
.selectAll('rect')
.data(function appendData(data) {
@ -179,7 +181,7 @@ define(function (require) {
.enter()
.append('circle')
.attr('class', function circleClass(d) {
return d.label;
return d.label + ' ' + self.colorToClass(color(d.label));
})
.attr('stroke', function strokeColor(d) {
return color(d.label);

View file

@ -69,7 +69,7 @@ define(function (require) {
.enter()
.append('rect')
.attr('class', function (d) {
return self.colorToClass(color(d.label));
return 'color ' + self.colorToClass(color(d.label));
})
.attr('fill', function (d) {
return color(d.label);
@ -246,8 +246,9 @@ define(function (require) {
var isBrushable = events.isBrushable();
var brush = isBrushable ? events.addBrushEvent(svg) : undefined;
var hover = events.addHoverEvent();
var mouseout = events.addMouseoutEvent();
var click = events.addClickEvent();
var attachedEvents = element.call(hover).call(click);
var attachedEvents = element.call(hover).call(mouseout).call(click);
if (isBrushable) {
attachedEvents.call(brush);

View file

@ -45,8 +45,9 @@ define(function (require) {
var isBrushable = events.isBrushable();
var brush = isBrushable ? events.addBrushEvent(svg) : undefined;
var hover = events.addHoverEvent();
var mouseout = events.addMouseoutEvent();
var click = events.addClickEvent();
var attachedEvents = element.call(hover).call(click);
var attachedEvents = element.call(hover).call(mouseout).call(click);
if (isBrushable) {
attachedEvents.call(brush);
@ -65,6 +66,7 @@ define(function (require) {
*/
LineChart.prototype.addCircles = function (svg, data) {
var self = this;
var showCircles = this._attr.showCircles;
var color = this.handler.data.getColorFunc();
var xScale = this.handler.xAxis.xScale;
var yScale = this.handler.yAxis.yScale;
@ -81,7 +83,7 @@ define(function (require) {
.attr('class', 'points line');
var circles = layer
.selectAll('rect')
.selectAll('circle')
.data(function appendData(d) {
return d;
});
@ -105,14 +107,26 @@ define(function (require) {
return color(d.label);
}
function colorCircle(d) {
var parent = d3.select(this).node().parentNode;
var lengthOfParent = d3.select(parent).data()[0].length;
var isVisible = (lengthOfParent === 1);
// If only 1 point exists, show circle
if (!showCircles && !isVisible) return 'none';
return cColor(d);
}
circles
.enter()
.append('circle')
.attr('r', visibleRadius)
.attr('cx', cx)
.attr('cy', cy)
.attr('fill', cColor)
.attr('class', 'circle-decoration');
.attr('fill', colorCircle)
.attr('class', function circleClass(d) {
return 'circle-decoration ' + self.colorToClass(color(d.label));
});
circles
.enter()
@ -172,7 +186,7 @@ define(function (require) {
lines.append('path')
.attr('class', function lineClass(d) {
return self.colorToClass(color(d.label));
return 'color ' + self.colorToClass(color(d.label));
})
.attr('d', function lineD(d) {
return line(d.values);
@ -299,7 +313,6 @@ define(function (require) {
.style('stroke', '#ddd')
.style('stroke-width', lineStrokeWidth);
return svg;
});
};

View file

@ -41,6 +41,7 @@ define(function (require) {
return element
.call(events.addHoverEvent())
.call(events.addMouseoutEvent())
.call(events.addClickEvent());
};

View file

@ -3,7 +3,7 @@ define(function (require) {
var $ = require('jquery');
var _ = require('lodash');
module.directive('cssTruncate', function ($compile) {
module.directive('cssTruncate', function ($timeout) {
return {
restrict: 'A',
scope: {},
@ -16,11 +16,18 @@ define(function (require) {
'word-break': 'break-all',
});
if (!_.isUndefined(attrs.cssTruncateExpandable)) {
$elem.css({'cursor': 'pointer'});
$elem.bind('click', function () {
$scope.toggle();
});
if (attrs.cssTruncateExpandable != null) {
$scope.$watch(
function () { return $elem.html(); },
function () {
if ($elem[0].offsetWidth < $elem[0].scrollWidth) {
$elem.css({'cursor': 'pointer'});
$elem.bind('click', function () {
$scope.toggle();
});
}
}
);
}
$scope.toggle = function () {

View file

@ -6,7 +6,8 @@ define(function (require) {
restrict: 'A',
link: function ($scope, $elem, attrs) {
$timeout(function () {
$elem[0].focus();
$elem.focus();
if (attrs.inputFocus === 'select') $elem.select();
});
}
};

View file

@ -10,10 +10,11 @@
<div class="input-group"
ng-class="queryInput.$invalid ? 'has-error' : ''">
<input type="text" input-focus
<input type="text"
placeholder="Filter..."
class="form-control"
ng-model="state.query"
input-focus
kbn-typeahead-input
validate-query>
<button type="submit" class="btn btn-default" ng-disabled="queryInput.$invalid">

View file

@ -1,7 +1,7 @@
<form ng-submit="opts.save()">
<div class="form-group">
<label for="exampleInputEmail1">Save As</label>
<input type="text" ng-model="opts.dashboard.title" class="form-control" placeholder="Dashboard title" input-focus>
<input type="text" ng-model="opts.dashboard.title" class="form-control" placeholder="Dashboard title" input-focus="select">
</div>
<button type="submit" ng-disabled="!opts.dashboard.title" class="btn btn-primary">Save</button>
</form>

View file

@ -38,11 +38,11 @@
class="sidebar-item-button primary">
Visualize
<span class="discover-field-vis-warning" ng-show="warnings.length" tooltip="{{warnings.join(' ')}}">
( {{warnings.length}} warnings <i class="fa fa-warning"></i> )
( {{warnings.length}} <ng-pluralize count="warnings.length" when="{'1':'warning', 'other':'warnings'}"></ng-pluralize> <i class="fa fa-warning"></i> )
</span>
</div>
<div ng-show="!field.indexed && !field.scripted"
disabled="disabled"
tooltip="This field is not indexed thus unavailable for visualization and search"
class="sidebar-item-button primary">Not Indexed</div>
class="sidebar-item-button primary">Not Indexed</div>

View file

@ -37,7 +37,8 @@ define(function (require) {
'type',
'indexed',
'analyzed',
'missing'
'missing',
'name'
],
defaults: {
missing: true

View file

@ -3,7 +3,7 @@
<div class="col-md-12">
<div class="form-group">
<label class="control-label">Save Search</label>
<input ng-model="opts.savedSearch.title" input-focus class="form-control" placeholder="Name this search...">
<input ng-model="opts.savedSearch.title" input-focus="select" class="form-control" placeholder="Name this search...">
</div>
<div class="form-group">
<button ng-click="opts.saveDataSource()" ng-disabled="!opts.savedSearch.title" type="submit" class="btn btn-primary">

View file

@ -4,7 +4,7 @@ define(function (require) {
$rootScope.globalState = globalState;
// and some local values
$scope.appEmbedded = $location.search().embed;
$scope.appEmbedded = $location.search().embed || false;
$scope.httpActive = $http.pendingRequests;
$scope.notifList = notify._notifs;
@ -13,4 +13,4 @@ define(function (require) {
courier.start();
});
};
});
});

View file

@ -1,6 +1,6 @@
<kbn-notifications list="notifList"></kbn-notifications>
<div class="content" style="display: none;">
<nav ng-hide="appEmbedded" bindonce class="navbar navbar-inverse navbar-static-top">
<nav ng-class="{show: appEmbedded === false}" bindonce class="hide navbar navbar-inverse navbar-static-top">
<div class="navbar-header">
<button ng-click="showCollapsed = !showCollapsed" type="button" class="navbar-toggle">
<span class="sr-only">Toggle navigation</span>
@ -51,4 +51,4 @@
</config>
<div class="application" ng-view></div>
</div>
</div>

View file

@ -23,7 +23,7 @@
</table>
</p>
<small>© 2014 All Rights Reserved - Elasticsearch</small>
<small>© 2015 All Rights Reserved - Elasticsearch</small>
</center>
</div>
</div>

View file

@ -49,7 +49,7 @@
<label>Script <small>Please familiarize yourself with <a target="_window" href="http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-script-fields.html#search-request-script-fields">script fields <i class="fa-link fa"></i></a> and with
<a target="_window" href="http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html#search-aggregations-bucket-terms-aggregation-script">scripts in aggregations <i class="fa-link fa"></i></a>
before using scripted fields.</small></label>
<textarea required class="form-control span12" ng-model="scriptedField.script"></textarea>
<textarea required class="scripted-field-script form-control span12" ng-model="scriptedField.script"></textarea>
</div>
</form>
<div ng-if="namingConflict" class="alert alert-danger">

View file

@ -149,6 +149,10 @@ kbn-settings-objects-view {
.flex(4, 0, auto);
}
}
.scripted-field-script {
font-family: @font-family-monospace;
}
}
kbn-settings-indices .fields {

View file

@ -0,0 +1,8 @@
<!-- vis type specific options -->
<vislib-basic-options></vislib-basic-options>
<div class="vis-option-item form-group">
<label>
<input type="checkbox" value="{{showCircles}}" ng-model="vis.params.showCircles" name="showCircles" ng-checked="vis.params.showCircles">
Show Circles
</label>
</div>

View file

@ -14,9 +14,10 @@ define(function (require) {
shareYAxis: true,
addTooltip: true,
addLegend: true,
showCircles: true,
defaultYExtents: false
},
editor: require('text!plugins/vis_types/vislib/editors/basic.html')
editor: require('text!plugins/vis_types/vislib/editors/line.html')
},
schemas: new Schemas([
{

View file

@ -9,7 +9,7 @@
<i class="fa fa-link"></i>
&nbsp;
This visualization is linked to a saved search:
<b>{{ savedVis.savedSearchId | json}}</b>
<b>{{ savedVis.savedSearch.title }}</b>
</div>
<div

View file

@ -1,7 +1,7 @@
<form role="form" ng-submit="conf.doSave()">
<div class="form-group">
<label for="visTitle">Title</label>
<input class="form-control" input-focus type="text" name="visTitle" ng-model="conf.savedVis.title" required>
<input class="form-control" input-focus="select" type="text" name="visTitle" ng-model="conf.savedVis.title" required>
</div>
<button type="submit" class="btn btn-primary">Save</button>
</form>

View file

@ -1,12 +1,12 @@
<li class="sidebar-item" ng-show="vis.type.params.editor">
<div class="sidebar-item-title" ng-click="showVisOptions = !showVisOptions">
<div ng-hide="alwaysShowOptions" class="sidebar-item-title" ng-click="showVisOptions = !showVisOptions">
view options
<i
class="fa fa-caret-down"
ng-if="!alwaysShowOptions"
ng-class="{'fa-caret-down': showVisOptions, 'fa-caret-right': !showVisOptions}">
</i>
</div>
<div ng-show="alwaysShowOptions" class="sidebar-item-title">view options</div>
<div class="visualization-options" ng-show="alwaysShowOptions || showVisOptions"></div>
</li>

View file

@ -276,6 +276,16 @@ notifications {
text-shadow: none;
}
.kbn-timepicker .refresh-interval {
padding: 0.2em 0.4em;
border-radius: @border-radius-small;
}
.kbn-timepicker .refresh-interval-active {
background-color: @btn-info-bg;
color: @btn-info-color;
}
//== Table
kbn-table, .kbn-table {

View file

@ -18,6 +18,7 @@ program.option('-c, --config <path>', 'Path to the config file');
program.option('-p, --port <port>', 'The port to bind to', parseInt);
program.option('-q, --quiet', 'Turns off logging');
program.option('-H, --host <host>', 'The host to bind to');
program.option('-l, --log-file <path>', 'The file to log to');
program.option('--plugins <path>', 'Path to scan for plugins');
program.parse(process.argv);
@ -49,6 +50,10 @@ if (program.quiet) {
config.quiet = program.quiet;
}
if (program.logFile) {
config.log_file = program.logFile;
}
if (program.host) {
config.host = program.host;
}

View file

@ -45,7 +45,8 @@ var config = module.exports = {
kibana : kibana,
package : require(packagePath),
htpasswd : htpasswdPath,
buildNum : '@@buildNum'
buildNum : '@@buildNum',
log_file : kibana.log_file || null
};
config.plugins = listPlugins(config);

View file

@ -49,3 +49,6 @@ verify_ssl: true
# Set the path to where you would like the process id file to be created.
# pid_file: /var/run/kibana.pid
# If you would like to send the log output to a file you can set the path below.
# This will also turn off the STDOUT log output.
# log_file: ./kibana.log

View file

@ -69,8 +69,8 @@ function onListening() {
}
function start() {
var port = parseInt(process.env.PORT, 10) || config.port || 3000;
var host = process.env.HOST || config.host || '127.0.0.1';
var port = config.port || 3000;
var host = config.host || '127.0.0.1';
var listen = Promise.promisify(server.listen.bind(server));
app.set('port', port);
return listen(port, host);

View file

@ -1,6 +1,5 @@
var _ = require('lodash');
var Writable = require('stream').Writable;
var util = require('util');
var through = require('through');
var levels = {
10: 'trace',
@ -11,14 +10,7 @@ var levels = {
60: 'fatal'
};
function JSONStream(options) {
options = options || {};
Writable.call(this, options);
}
util.inherits(JSONStream, Writable);
JSONStream.prototype._write = function (entry, encoding, callback) {
function write(entry) {
entry = JSON.parse(entry.toString('utf8'));
var env = process.env.NODE_ENV || 'development';
@ -36,8 +28,13 @@ JSONStream.prototype._write = function (entry, encoding, callback) {
if (!output.message) output.message = output.error.message;
}
process.stdout.write(JSON.stringify(output) + "\n");
callback();
};
this.queue(JSON.stringify(output) + '\n');
}
module.exports = JSONStream;
function end() {
this.queue(null);
}
module.exports = function () {
return through(write, end);
};

View file

@ -2,18 +2,33 @@ var _ = require('lodash');
var morgan = require('morgan');
var env = process.env.NODE_ENV || 'development';
var bunyan = require('bunyan');
var fs = require('fs');
var StdOutStream = require('./StdOutStream');
var JSONStream = require('./JSONStream');
var createJSONStream = require('./createJSONStream');
var config = require('../config');
var stream = { stream: new JSONStream() };
var streams = [];
// Set the default stream based on the enviroment. If we are on development then
// then we are going to create a pretty stream. Everytyhing else will get the
// JSON stream to stdout.
var defaultStream;
if (env === 'development') {
stream.stream = new StdOutStream();
defaultStream = new StdOutStream();
} else {
defaultStream = createJSONStream()
.pipe(process.stdout);
}
if (!config.quiet) {
streams.push(stream);
// If we are not being oppressed and we are not sending the output to a log file
// push the default stream to the list of streams
if (!config.quiet && !config.log_file) {
streams.push({ stream: defaultStream });
}
// Send the stream to a file using the json format.
if (config.log_file) {
var fileStream = fs.createWriteStream(config.log_file);
streams.push({ stream: createJSONStream().pipe(fileStream) });
}
var logger = module.exports = bunyan.createLogger({

View file

@ -1,8 +1,7 @@
var config = require('../config');
var upgrade = require('./upgradeConfig');
var client = require('./elasticsearch_client');
module.exports = function () {
module.exports = function (client) {
var options = {
index: config.kibana.kibana_index,
type: 'config',
@ -22,7 +21,7 @@ module.exports = function () {
};
return client.search(options)
.then(upgrade)
.then(upgrade(client))
.catch(function (err) {
if (!/SearchParseException.+mapping.+\[buildNum\]|^IndexMissingException/.test(err.message)) throw err;
});

View file

@ -1,9 +1,9 @@
var Promise = require('bluebird');
var waitForEs = require('./waitForEs');
var migrateConfig = require('./migrateConfig');
var client = require('./elasticsearch_client');
module.exports = function () {
return waitForEs().then(function () {
return migrateConfig();
return migrateConfig(client);
});
};

View file

@ -3,29 +3,31 @@ var isUpgradeable = require('./isUpgradeable');
var config = require('../config');
var _ = require('lodash');
var client = require('./elasticsearch_client');
module.exports = function (response) {
var newConfig = {};
// Check to see if there are any doc. If not then we can assume
// nothing needs to be done
if (response.hits.hits.length === 0) return Promise.resolve();
module.exports = function (client) {
return function (response) {
var newConfig = {};
// Check to see if there are any doc. If not then we can assume
// nothing needs to be done
if (response.hits.hits.length === 0) return Promise.resolve();
// Look for upgradeable configs. If none of them are upgradeable
// then resolve with null.
var body = _.find(response.hits.hits, isUpgradeable);
if (!body) return Promise.resolve();
// if we already have a the current version in the index then we need to stop
if (_.find(response.hits.hits, { _id: config.package.version })) return Promise.resolve();
// if the build number is still the template string (which it wil be in development)
// then we need to set it to the max interger. Otherwise we will set it to the build num
body._source.buildNum = (/^@@/.test(config.buildNum)) ? Math.pow(2, 53) - 1 : parseInt(config.buildNum, 10);
// Look for upgradeable configs. If none of them are upgradeable
// then resolve with null.
var body = _.find(response.hits.hits, isUpgradeable);
if (!body) return Promise.resolve();
return client.create({
index: config.kibana.kibana_index,
type: 'config',
body: body._source,
id: config.package.version
}).catch(function (err) {
// Ignore document already exists exceptions for beta and snapshot upgrades.
if (/DocumentAlreadyExistsException/.test(err.message) && /beta|snapshot/.test(config.package.version)) return;
throw err;
});
// if the build number is still the template string (which it wil be in development)
// then we need to set it to the max interger. Otherwise we will set it to the build num
body._source.buildNum = (/^@@/.test(config.buildNum)) ? Math.pow(2, 53) - 1 : parseInt(config.buildNum, 10);
return client.create({
index: config.kibana.kibana_index,
type: 'config',
body: body._source,
id: config.package.version
});
};
};

View file

@ -0,0 +1,25 @@
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"failed": 0
},
"hits": {
"total": 1,
"max_score": 1,
"hits": [
{
"_index": ".kibana",
"_type": "config",
"_id": "4.0.0",
"_score": 1,
"_source": {
"buildNum": 5888,
"defaultIndex": "logstash-*"
}
}
]
}
}

View file

@ -0,0 +1,35 @@
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"failed": 0
},
"hits": {
"total": 2,
"max_score": 1,
"hits": [
{
"_index": ".kibana",
"_type": "config",
"_id": "4.0.1-snapshot",
"_score": 1,
"_source": {
"buildNum": 5921,
"defaultIndex": "logstash-*"
}
},
{
"_index": ".kibana",
"_type": "config",
"_id": "4.0.0",
"_score": 1,
"_source": {
"buildNum": 5888,
"defaultIndex": "logstash-*"
}
}
]
}
}

View file

@ -0,0 +1,35 @@
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"failed": 0
},
"hits": {
"total": 2,
"max_score": 1,
"hits": [
{
"_index": ".kibana",
"_type": "config",
"_id": "4.0.1",
"_score": 1,
"_source": {
"buildNum": 5921,
"defaultIndex": "logstash-*"
}
},
{
"_index": ".kibana",
"_type": "config",
"_id": "4.0.0",
"_score": 1,
"_source": {
"buildNum": 5888,
"defaultIndex": "logstash-*"
}
}
]
}
}

View file

@ -0,0 +1,75 @@
var root = require('requirefrom')('');
var upgradeConfig = root('src/server/lib/upgradeConfig');
var expect = require('expect.js');
var sinon = require('sinon');
var sinonAsPromised = require('sinon-as-promised')(require('bluebird'));
var util = require('util');
var package = root('package.json');
var config = root('src/server/config');
var upgradeFrom4_0_0_to_4_0_1 = root('test/unit/fixtures/config_upgrade_from_4.0.0_to_4.0.1.json');
var upgradeFrom4_0_0_to_4_0_1_snapshot = root('test/unit/fixtures/config_upgrade_from_4.0.0_to_4.0.1-snapshot.json');
var upgradeFrom4_0_0 = root('test/unit/fixtures/config_upgrade_from_4.0.0.json');
describe('lib/upgradeConfig', function () {
var client, oldPackageVersion, oldBuildNum;
beforeEach(function () {
oldPackageVersion = config.package.version;
oldBuildNum = config.buildNum;
client = { create: sinon.stub() };
});
afterEach(function () {
config.package.version = oldPackageVersion;
config.buildNum = oldBuildNum;
});
it('should not upgrade if the current version of the config exits', function () {
config.package.version = '4.0.1';
var fn = upgradeConfig(client);
client.create.rejects(new Error('DocumentAlreadyExistsException'));
return fn(upgradeFrom4_0_0_to_4_0_1).finally(function () {
sinon.assert.notCalled(client.create);
});
});
it('should not upgrade if there are no hits', function () {
config.package.version = '4.0.1';
var fn = upgradeConfig(client);
return fn({ hits: { hits: [] } }).finally(function () {
sinon.assert.notCalled(client.create);
});
});
it('should not upgrade even if a snapshot exists', function () {
config.package.version = '4.0.1-snapshot';
client.create.rejects(new Error('DocumentAlreadyExistsException'));
var fn = upgradeConfig(client);
return fn(upgradeFrom4_0_0_to_4_0_1_snapshot).finally(function () {
sinon.assert.notCalled(client.create);
});
});
it('should upgrade from 4.0.0 to 4.0.1', function () {
config.package.version = '4.0.1';
config.buildNum = 5921;
var fn = upgradeConfig(client);
client.create.resolves({ _index: '.kibana', _type: 'config', _id: '4.0.1', _version: 1, created: true });
return fn(upgradeFrom4_0_0).finally(function () {
sinon.assert.calledOnce(client.create);
var body = client.create.args[0][0];
expect(body).to.eql({
index: '.kibana',
type: 'config',
id: '4.0.1',
body: {
'buildNum': 5921,
'defaultIndex': 'logstash-*'
}
});
});
});
});

View file

@ -13,7 +13,8 @@ define(function (require) {
'bytes': 100,
'area': [{lat: 7, lon: 7}],
'noMapping': 'hasNoMapping',
'objectArray': [{foo: true}, {bar: false}]
'objectArray': [{foo: true}, {bar: false}],
'_underscore': 1
}
};
@ -103,14 +104,23 @@ define(function (require) {
});
describe('warnings', function () {
it('displays a warning about field name starting with underscore', function () {
var cells = $elem.find('td[title="_underscore"]').siblings();
expect(cells.find('.doc-viewer-underscore').length).to.be(1);
expect(cells.find('.doc-viewer-no-mapping').length).to.be(0);
expect(cells.find('.doc-viewer-object-array').length).to.be(0);
});
it('displays a warning about missing mappings', function () {
var cells = $elem.find('td[title="noMapping"]').siblings();
expect(cells.find('.doc-viewer-underscore').length).to.be(0);
expect(cells.find('.doc-viewer-no-mapping').length).to.be(1);
expect(cells.find('.doc-viewer-object-array').length).to.be(0);
});
it('displays a warning about objects in arrays', function () {
var cells = $elem.find('td[title="objectArray"]').siblings();
expect(cells.find('.doc-viewer-underscore').length).to.be(0);
expect(cells.find('.doc-viewer-no-mapping').length).to.be(0);
expect(cells.find('.doc-viewer-object-array').length).to.be(1);
});

View file

@ -0,0 +1,51 @@
define(function (require) {
var angular = require('angular');
var $ = require('jquery');
require('directives/input_focus');
describe('Input focus directive', function () {
var $compile, $rootScope, $timeout, element;
var $el, selectedEl, selectedText;
var inputValue = 'Input Text Value';
beforeEach(module('kibana'));
beforeEach(inject(function (_$compile_, _$rootScope_, _$timeout_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
$timeout = _$timeout_;
$el = $('<div>');
$el.appendTo('body');
}));
afterEach(function () {
$el.remove();
$el = null;
});
function renderEl(html) {
$rootScope.value = inputValue;
element = $compile(html)($rootScope);
element.appendTo($el);
$rootScope.$digest();
$timeout.flush();
selectedEl = document.activeElement;
selectedText = window.getSelection().toString();
}
it('should focus the input', function () {
renderEl('<input type="text" ng-model="value" input-focus />');
expect(selectedEl).to.equal(element[0]);
expect(selectedText.length).to.equal(0);
});
it('should select the text in the input', function () {
renderEl('<input type="text" ng-model="value" input-focus="select" />');
expect(selectedEl).to.equal(element[0]);
expect(selectedText.length).to.equal(inputValue.length);
expect(selectedText).to.equal(inputValue);
});
});
});

View file

@ -110,6 +110,14 @@ define(function (require) {
done();
});
it('should highlight the current active interval', function (done) {
$scope.setRefreshInterval({ value: 300000 });
$elem.scope().$digest();
expect($elem.find('.refresh-interval-active').length).to.be(1);
expect($elem.find('.refresh-interval-active').text().trim()).to.be('5 minutes');
done();
});
it('should default the interval on the courier with incorrect values', function (done) {
// Change refresh interval and digest
$scope.setRefreshInterval('undefined');

View file

@ -64,7 +64,7 @@ define(function (require) {
if (path.__data__.name === undefined) return false;
return path.__data__.name.toString() === label;
}).map(function (path) {
return $(path).attr('class').split(/\s+/)[2].replace('c', '#');
return $(path).attr('class').split(/\s+/)[1].replace('c', '#');
});
slices.forEach(function (hex) {