diff --git a/FAQ.md b/FAQ.md index 7f7a76eef46f..fd45eddc12aa 100644 --- a/FAQ.md +++ b/FAQ.md @@ -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. + diff --git a/docs/production.asciidoc b/docs/production.asciidoc index 59c2c2de84a2..ddc0c6d57bbf 100644 --- a/docs/production.asciidoc +++ b/docs/production.asciidoc @@ -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: +* <> +* <> +* <> +* <> -* 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 +<>. +[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]. \ No newline at end of file +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" +-------- + + + + + diff --git a/package.json b/package.json index 396fe5a26639..78f6bafd99ed 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/kibana/components/doc_viewer/doc_viewer.html b/src/kibana/components/doc_viewer/doc_viewer.html index 35ac32123b36..b196bfe01b86 100644 --- a/src/kibana/components/doc_viewer/doc_viewer.html +++ b/src/kibana/components/doc_viewer/doc_viewer.html @@ -25,7 +25,11 @@ - + @@ -43,4 +47,4 @@
{{hit | json}}
- \ No newline at end of file + diff --git a/src/kibana/components/doc_viewer/doc_viewer.js b/src/kibana/components/doc_viewer/doc_viewer.js index a32395b7431e..fdd65b759b9a 100644 --- a/src/kibana/components/doc_viewer/doc_viewer.js +++ b/src/kibana/components/doc_viewer/doc_viewer.js @@ -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) { } }; }); -}); \ No newline at end of file +}); diff --git a/src/kibana/components/timepicker/timepicker.html b/src/kibana/components/timepicker/timepicker.html index b9562a743ba0..e363107ecd6d 100644 --- a/src/kibana/components/timepicker/timepicker.html +++ b/src/kibana/components/timepicker/timepicker.html @@ -91,12 +91,14 @@
- - round to the {{units[relative.unit]}} +
@@ -169,7 +171,11 @@
diff --git a/src/kibana/components/vislib/lib/dispatch.js b/src/kibana/components/vislib/lib/dispatch.js index 63c03a0f1773..87c911440311 100644 --- a/src/kibana/components/vislib/lib/dispatch.js +++ b/src/kibana/components/vislib/lib/dispatch.js @@ -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 * diff --git a/src/kibana/components/vislib/lib/legend.js b/src/kibana/components/vislib/lib/legend.js index 046f320a42ac..c597efbcf31d 100644 --- a/src/kibana/components/vislib/lib/legend.js +++ b/src/kibana/components/vislib/lib/legend.js @@ -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); }); }; diff --git a/src/kibana/components/vislib/visualizations/_chart.js b/src/kibana/components/vislib/visualizations/_chart.js index ad419953ef18..6da7b7b9ce5c 100644 --- a/src/kibana/components/vislib/visualizations/_chart.js +++ b/src/kibana/components/vislib/visualizations/_chart.js @@ -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); }; /** diff --git a/src/kibana/components/vislib/visualizations/area_chart.js b/src/kibana/components/vislib/visualizations/area_chart.js index 540c524c145c..8a200a112983 100644 --- a/src/kibana/components/vislib/visualizations/area_chart.js +++ b/src/kibana/components/vislib/visualizations/area_chart.js @@ -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); diff --git a/src/kibana/components/vislib/visualizations/column_chart.js b/src/kibana/components/vislib/visualizations/column_chart.js index e4ee69c99208..55ed6fb7619f 100644 --- a/src/kibana/components/vislib/visualizations/column_chart.js +++ b/src/kibana/components/vislib/visualizations/column_chart.js @@ -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); diff --git a/src/kibana/components/vislib/visualizations/line_chart.js b/src/kibana/components/vislib/visualizations/line_chart.js index 932e4441fd9e..7c15c4add13c 100644 --- a/src/kibana/components/vislib/visualizations/line_chart.js +++ b/src/kibana/components/vislib/visualizations/line_chart.js @@ -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; }); }; diff --git a/src/kibana/components/vislib/visualizations/pie_chart.js b/src/kibana/components/vislib/visualizations/pie_chart.js index debfa5d48bc6..ac06b1c6bc17 100644 --- a/src/kibana/components/vislib/visualizations/pie_chart.js +++ b/src/kibana/components/vislib/visualizations/pie_chart.js @@ -41,6 +41,7 @@ define(function (require) { return element .call(events.addHoverEvent()) + .call(events.addMouseoutEvent()) .call(events.addClickEvent()); }; diff --git a/src/kibana/directives/css_truncate.js b/src/kibana/directives/css_truncate.js index d9cfe54b20a4..778a4a2ff631 100644 --- a/src/kibana/directives/css_truncate.js +++ b/src/kibana/directives/css_truncate.js @@ -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 () { diff --git a/src/kibana/directives/input_focus.js b/src/kibana/directives/input_focus.js index d52c299da9a6..6d3bfa7471d2 100644 --- a/src/kibana/directives/input_focus.js +++ b/src/kibana/directives/input_focus.js @@ -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(); }); } }; diff --git a/src/kibana/plugins/dashboard/index.html b/src/kibana/plugins/dashboard/index.html index 60bc4993586d..4964a9c4852a 100644 --- a/src/kibana/plugins/dashboard/index.html +++ b/src/kibana/plugins/dashboard/index.html @@ -10,10 +10,11 @@
- \ No newline at end of file diff --git a/src/kibana/plugins/discover/components/field_chooser/discover_field_details.html b/src/kibana/plugins/discover/components/field_chooser/discover_field_details.html index 4c83364f221d..f5712b1e8cfb 100644 --- a/src/kibana/plugins/discover/components/field_chooser/discover_field_details.html +++ b/src/kibana/plugins/discover/components/field_chooser/discover_field_details.html @@ -38,11 +38,11 @@ class="sidebar-item-button primary"> Visualize - ( {{warnings.length}} warnings ) + ( {{warnings.length}} )
\ No newline at end of file + class="sidebar-item-button primary">Not Indexed diff --git a/src/kibana/plugins/discover/components/field_chooser/field_chooser.js b/src/kibana/plugins/discover/components/field_chooser/field_chooser.js index 8139aaca817b..a9102e709894 100644 --- a/src/kibana/plugins/discover/components/field_chooser/field_chooser.js +++ b/src/kibana/plugins/discover/components/field_chooser/field_chooser.js @@ -37,7 +37,8 @@ define(function (require) { 'type', 'indexed', 'analyzed', - 'missing' + 'missing', + 'name' ], defaults: { missing: true diff --git a/src/kibana/plugins/discover/partials/save_search.html b/src/kibana/plugins/discover/partials/save_search.html index 4c02837eb5ff..793bda71cf54 100644 --- a/src/kibana/plugins/discover/partials/save_search.html +++ b/src/kibana/plugins/discover/partials/save_search.html @@ -3,7 +3,7 @@
- +
\ No newline at end of file +
diff --git a/src/kibana/plugins/settings/sections/about/index.html b/src/kibana/plugins/settings/sections/about/index.html index f94012e60ba0..37d991d50da6 100644 --- a/src/kibana/plugins/settings/sections/about/index.html +++ b/src/kibana/plugins/settings/sections/about/index.html @@ -23,7 +23,7 @@

- © 2014 All Rights Reserved - Elasticsearch + © 2015 All Rights Reserved - Elasticsearch diff --git a/src/kibana/plugins/settings/sections/indices/scripted_fields/index.html b/src/kibana/plugins/settings/sections/indices/scripted_fields/index.html index b10c69b5c3d7..50ceb81b281d 100644 --- a/src/kibana/plugins/settings/sections/indices/scripted_fields/index.html +++ b/src/kibana/plugins/settings/sections/indices/scripted_fields/index.html @@ -49,7 +49,7 @@ - +
diff --git a/src/kibana/plugins/settings/styles/main.less b/src/kibana/plugins/settings/styles/main.less index b6f76a79f67c..655235271dc1 100644 --- a/src/kibana/plugins/settings/styles/main.less +++ b/src/kibana/plugins/settings/styles/main.less @@ -149,6 +149,10 @@ kbn-settings-objects-view { .flex(4, 0, auto); } } + + .scripted-field-script { + font-family: @font-family-monospace; + } } kbn-settings-indices .fields { diff --git a/src/kibana/plugins/vis_types/vislib/editors/line.html b/src/kibana/plugins/vis_types/vislib/editors/line.html new file mode 100644 index 000000000000..3974262f5b82 --- /dev/null +++ b/src/kibana/plugins/vis_types/vislib/editors/line.html @@ -0,0 +1,8 @@ + + +
+ +
diff --git a/src/kibana/plugins/vis_types/vislib/line.js b/src/kibana/plugins/vis_types/vislib/line.js index 1506222290c7..4cac2830a1fb 100644 --- a/src/kibana/plugins/vis_types/vislib/line.js +++ b/src/kibana/plugins/vis_types/vislib/line.js @@ -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([ { diff --git a/src/kibana/plugins/visualize/editor/editor.html b/src/kibana/plugins/visualize/editor/editor.html index 56702e7813f2..630e964b0957 100644 --- a/src/kibana/plugins/visualize/editor/editor.html +++ b/src/kibana/plugins/visualize/editor/editor.html @@ -9,7 +9,7 @@   This visualization is linked to a saved search: - {{ savedVis.savedSearchId | json}} + {{ savedVis.savedSearch.title }}
- +
\ No newline at end of file diff --git a/src/kibana/plugins/visualize/editor/vis_options.html b/src/kibana/plugins/visualize/editor/vis_options.html index 74f7e4551bee..c97fe5aff321 100644 --- a/src/kibana/plugins/visualize/editor/vis_options.html +++ b/src/kibana/plugins/visualize/editor/vis_options.html @@ -1,12 +1,12 @@ diff --git a/src/kibana/styles/main.less b/src/kibana/styles/main.less index 317b80ffb3a5..5808e7e7eb74 100644 --- a/src/kibana/styles/main.less +++ b/src/kibana/styles/main.less @@ -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 { diff --git a/src/server/bin/kibana.js b/src/server/bin/kibana.js index 617d0ed29622..ee3381abf707 100755 --- a/src/server/bin/kibana.js +++ b/src/server/bin/kibana.js @@ -18,6 +18,7 @@ program.option('-c, --config ', 'Path to the config file'); program.option('-p, --port ', 'The port to bind to', parseInt); program.option('-q, --quiet', 'Turns off logging'); program.option('-H, --host ', 'The host to bind to'); +program.option('-l, --log-file ', 'The file to log to'); program.option('--plugins ', '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; } diff --git a/src/server/config/index.js b/src/server/config/index.js index 846862db4560..9d9c83354155 100644 --- a/src/server/config/index.js +++ b/src/server/config/index.js @@ -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); diff --git a/src/server/config/kibana.yml b/src/server/config/kibana.yml index 3bc7102bf164..5ce9630ec8ae 100644 --- a/src/server/config/kibana.yml +++ b/src/server/config/kibana.yml @@ -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 \ No newline at end of file diff --git a/src/server/index.js b/src/server/index.js index 81413f72f3e3..116b571075f6 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -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); diff --git a/src/server/lib/JSONStream.js b/src/server/lib/createJSONStream.js similarity index 59% rename from src/server/lib/JSONStream.js rename to src/server/lib/createJSONStream.js index f059691ea3c8..042b6dc0a16a 100644 --- a/src/server/lib/JSONStream.js +++ b/src/server/lib/createJSONStream.js @@ -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); +}; \ No newline at end of file diff --git a/src/server/lib/logger.js b/src/server/lib/logger.js index befa814b51fc..78210738be63 100644 --- a/src/server/lib/logger.js +++ b/src/server/lib/logger.js @@ -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({ diff --git a/src/server/lib/migrateConfig.js b/src/server/lib/migrateConfig.js index 7000d9d3c6c0..393796cf7b3a 100644 --- a/src/server/lib/migrateConfig.js +++ b/src/server/lib/migrateConfig.js @@ -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; }); diff --git a/src/server/lib/serverInitialization.js b/src/server/lib/serverInitialization.js index 8f3b803443cf..94aebc348be4 100644 --- a/src/server/lib/serverInitialization.js +++ b/src/server/lib/serverInitialization.js @@ -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); }); }; diff --git a/src/server/lib/upgradeConfig.js b/src/server/lib/upgradeConfig.js index 830105bfc651..d3d95d7eab0e 100644 --- a/src/server/lib/upgradeConfig.js +++ b/src/server/lib/upgradeConfig.js @@ -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 + }); + }; }; diff --git a/test/unit/fixtures/config_upgrade_from_4.0.0.json b/test/unit/fixtures/config_upgrade_from_4.0.0.json new file mode 100644 index 000000000000..522de78648c9 --- /dev/null +++ b/test/unit/fixtures/config_upgrade_from_4.0.0.json @@ -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-*" + } + } + ] + } +} diff --git a/test/unit/fixtures/config_upgrade_from_4.0.0_to_4.0.1-snapshot.json b/test/unit/fixtures/config_upgrade_from_4.0.0_to_4.0.1-snapshot.json new file mode 100644 index 000000000000..30990d109cad --- /dev/null +++ b/test/unit/fixtures/config_upgrade_from_4.0.0_to_4.0.1-snapshot.json @@ -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-*" + } + } + ] + } +} diff --git a/test/unit/fixtures/config_upgrade_from_4.0.0_to_4.0.1.json b/test/unit/fixtures/config_upgrade_from_4.0.0_to_4.0.1.json new file mode 100644 index 000000000000..57b486491b39 --- /dev/null +++ b/test/unit/fixtures/config_upgrade_from_4.0.0_to_4.0.1.json @@ -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-*" + } + } + ] + } +} diff --git a/test/unit/server/lib/upgradeConfig.js b/test/unit/server/lib/upgradeConfig.js new file mode 100644 index 000000000000..d0bbd17c8d69 --- /dev/null +++ b/test/unit/server/lib/upgradeConfig.js @@ -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-*' + } + }); + }); + + }); + +}); diff --git a/test/unit/specs/components/doc_viewer/doc_viewer.js b/test/unit/specs/components/doc_viewer/doc_viewer.js index aa3b312bc35f..a16792786a2b 100644 --- a/test/unit/specs/components/doc_viewer/doc_viewer.js +++ b/test/unit/specs/components/doc_viewer/doc_viewer.js @@ -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); }); diff --git a/test/unit/specs/directives/input_focus.js b/test/unit/specs/directives/input_focus.js new file mode 100644 index 000000000000..4efb776b808d --- /dev/null +++ b/test/unit/specs/directives/input_focus.js @@ -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 = $('
'); + $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(''); + expect(selectedEl).to.equal(element[0]); + expect(selectedText.length).to.equal(0); + }); + + it('should select the text in the input', function () { + renderEl(''); + expect(selectedEl).to.equal(element[0]); + expect(selectedText.length).to.equal(inputValue.length); + expect(selectedText).to.equal(inputValue); + }); + }); +}); \ No newline at end of file diff --git a/test/unit/specs/directives/timepicker.js b/test/unit/specs/directives/timepicker.js index d72ee753909d..0dba510628f9 100644 --- a/test/unit/specs/directives/timepicker.js +++ b/test/unit/specs/directives/timepicker.js @@ -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'); diff --git a/test/unit/specs/vislib/lib/legend.js b/test/unit/specs/vislib/lib/legend.js index a09e47c5f099..ccbd90694dd4 100644 --- a/test/unit/specs/vislib/lib/legend.js +++ b/test/unit/specs/vislib/lib/legend.js @@ -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) {