diff --git a/config.js b/config.js index 30d70cd988c..8d694bd8802 100644 --- a/config.js +++ b/config.js @@ -22,6 +22,7 @@ var config = new Settings( kibana_index: "kibana-int", modules: ['histogram','map','pie','table','filtering', 'timepicker','text','fields','hits','dashcontrol', - 'column','derivequeries','trends','bettermap','query'], + 'column','derivequeries','trends','bettermap','query', + 'terms'], } ); diff --git a/panels/pie/module.js b/panels/pie/module.js index 467fe9a4faa..e2d660c3282 100644 --- a/panels/pie/module.js +++ b/panels/pie/module.js @@ -25,10 +25,10 @@ angular.module('kibana.pie', []) .controller('pie', function($scope, $rootScope, querySrv, dashboard, filterSrv) { $scope.panelMeta = { - status : "Deprecating Soon", + status : "Deprecated", description : "Uses an Elasticsearch terms facet to create a pie chart. You should really only"+ - " point this at not_analyzed fields for that reason. This panel is going away soon, to be"+ - " replaced with a panel that can represent a terms facet in a variety of ways." + " point this at not_analyzed fields for that reason. This panel is going away soon, it has"+ + " been replaced by the terms panel. Please use that one instead." }; // Set and populate defaults @@ -247,11 +247,10 @@ angular.module('kibana.pie', []) colors: querySrv.colors }; - // Populate element + // Populate legend if(elem.is(":visible")){ scripts.wait(function(){ - scope.plot = $.plot(elem, scope.data, pie); - scope.legend = scope.plot.getData(); + scope.legend = $.plot(elem, scope.data, pie).getData(); if(!scope.$$phase) { scope.$apply(); } diff --git a/panels/terms/editor.html b/panels/terms/editor.html new file mode 100644 index 00000000000..3568e529fc3 --- /dev/null +++ b/panels/terms/editor.html @@ -0,0 +1,50 @@ +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
diff --git a/panels/terms/module.html b/panels/terms/module.html new file mode 100644 index 00000000000..9cb6848dc44 --- /dev/null +++ b/panels/terms/module.html @@ -0,0 +1,56 @@ + + + +
+ + + + + +
{{term.label}}{{term.data[0][1]}}
+ + +
+ {{term.label}} ({{term.data[0][1]}}) +

+ +
+ + +
+ +
+ +
+ + + + + +
{{term.label}}{{term.data[0][1]}}
+ + +
+ {{term.label}} ({{term.data[0][1]}}) +

+ +
+ + + + + + + + + + + +
Term Count Action
{{term.label}}{{term.data[0][1]}} + + + + +
+ +
\ No newline at end of file diff --git a/panels/terms/module.js b/panels/terms/module.js new file mode 100644 index 00000000000..2d791866e65 --- /dev/null +++ b/panels/terms/module.js @@ -0,0 +1,302 @@ +/*jshint globalstrict:true */ +/*global angular:true */ + +/* + + ## Terms + + ### Parameters + * style :: A hash of css styles + * size :: top N + * arrangement :: How should I arrange the query results? 'horizontal' or 'vertical' + * chart :: Show a chart? 'none', 'bar', 'pie' + * donut :: Only applies to 'pie' charts. Punches a hole in the chart for some reason + * tilt :: Only 'pie' charts. Janky 3D effect. Looks terrible 90% of the time. + * lables :: Only 'pie' charts. Labels on the pie? + +*/ + +'use strict'; + +angular.module('kibana.terms', []) +.controller('terms', function($scope, querySrv, dashboard, filterSrv) { + + $scope.panelMeta = { + status : "Beta", + description : "Displays the results of an elasticsearch facet as a pie chart, bar chart, or a "+ + "table" + }; + + // Set and populate defaults + var _d = { + queries : { + mode : 'all', + ids : [] + }, + field : '_type', + exclude : [], + missing : true, + other : true, + size : 10, + style : { "font-size": '10pt'}, + donut : false, + tilt : false, + labels : true, + arrangement : 'horizontal', + chart : 'bar', + counter_pos : 'above' + }; + _.defaults($scope.panel,_d); + + $scope.init = function () { + $scope.hits = 0; + + $scope.$on('refresh',function(){ + $scope.get_data(); + }); + $scope.get_data(); + + }; + + $scope.get_data = function(segment,query_id) { + // Make sure we have everything for the request to complete + if(dashboard.indices.length === 0) { + return; + } + + $scope.panel.loading = true; + var request, + results, + boolQuery; + + request = $scope.ejs.Request().indices(dashboard.indices); + + $scope.panel.queries.ids = querySrv.idsByMode($scope.panel.queries); + // This could probably be changed to a BoolFilter + boolQuery = $scope.ejs.BoolQuery(); + _.each($scope.panel.queries.ids,function(id) { + boolQuery = boolQuery.should(querySrv.getEjsObj(id)); + }); + + // Terms mode + request = request + .facet($scope.ejs.TermsFacet('terms') + .field($scope.panel.field) + .size($scope.panel.size) + .exclude($scope.panel.exclude) + .facetFilter($scope.ejs.QueryFilter( + $scope.ejs.FilteredQuery( + boolQuery, + filterSrv.getBoolFilter(filterSrv.ids) + )))).size(0); + + //$scope.populate_modal(request); + + results = request.doSearch(); + + // Populate scope when we have results + results.then(function(results) { + var k = 0; + $scope.panel.loading = false; + $scope.hits = results.hits.total; + $scope.data = []; + _.each(results.facets.terms.terms, function(v) { + var slice = { label : v.term, data : [[k,v.count]], actions: true}; + $scope.data.push(slice); + k = k + 1; + }); + + $scope.data.push({label:'Missing field', + data:[[k,results.facets.terms.missing]],meta:"missing",color:'#aaa',opacity:0}); + $scope.data.push({label:'Other values', + data:[[k+1,results.facets.terms.other]],meta:"other",color:'#444'}); + + $scope.$emit('render'); + }); + }; + + $scope.build_search = function(term,negate) { + if(_.isUndefined(term.meta)) { + filterSrv.set({type:'terms',field:$scope.panel.field,value:term.label, + mandate:(negate ? 'mustNot':'must')}); + } else if(term.meta === 'missing') { + filterSrv.set({type:'exists',field:$scope.panel.field, + mandate:(negate ? 'must':'mustNot')}); + } else { + return; + } + dashboard.refresh(); + }; + + $scope.set_refresh = function (state) { + $scope.refresh = state; + }; + + $scope.close_edit = function() { + if($scope.refresh) { + $scope.get_data(); + } + $scope.refresh = false; + $scope.$emit('render'); + }; + + $scope.showMeta = function(term) { + if(_.isUndefined(term.meta)) { + return true; + } + if(term.meta === 'other' && !$scope.panel.other) { + return false; + } + if(term.meta === 'missing' && !$scope.panel.missing) { + return false; + } + return true; + }; + +}).directive('termsChart', function(querySrv, filterSrv, dashboard) { + return { + restrict: 'A', + link: function(scope, elem, attrs, ctrl) { + + // Receive render events + scope.$on('render',function(){ + render_panel(); + }); + + // Re-render if the window is resized + angular.element(window).bind('resize', function(){ + render_panel(); + }); + + // Function for rendering panel + function render_panel() { + var plot, chartData; + var scripts = $LAB.script("common/lib/panels/jquery.flot.js").wait() + .script("common/lib/panels/jquery.flot.pie.js"); + + // IE doesn't work without this + elem.css({height:scope.panel.height||scope.row.height}); + + // Make a clone we can operate on. + chartData = _.clone(scope.data); + chartData = scope.panel.missing ? chartData : + _.without(chartData,_.findWhere(chartData,{meta:'missing'})); + chartData = scope.panel.other ? chartData : + _.without(chartData,_.findWhere(chartData,{meta:'other'})); + + // Populate element. + scripts.wait(function(){ + // Populate element + try { + // Add plot to scope so we can build out own legend + if(scope.panel.chart === 'bar') { + plot = $.plot(elem, chartData, { + legend: { show: false }, + series: { + lines: { show: false, }, + bars: { show: true, fill: 1, barWidth: 0.8, horizontal: false }, + shadowSize: 1 + }, + yaxis: { show: true, min: 0, color: "#c8c8c8" }, + xaxis: { show: false }, + grid: { + borderWidth: 0, + borderColor: '#eee', + color: "#eee", + hoverable: true, + clickable: true + }, + colors: querySrv.colors + }); + } + if(scope.panel.chart === 'pie') { + var labelFormat = function(label, series){ + return '
'+ + label+'
'+Math.round(series.percent)+'%
'; + }; + + plot = $.plot(elem, chartData, { + legend: { show: false }, + series: { + pie: { + innerRadius: scope.panel.donut ? 0.4 : 0, + tilt: scope.panel.tilt ? 0.45 : 1, + radius: 1, + show: true, + combine: { + color: '#999', + label: 'The Rest' + }, + stroke: { + width: 0 + }, + label: { + show: scope.panel.labels, + radius: 2/3, + formatter: labelFormat, + threshold: 0.1 + } + } + }, + //grid: { hoverable: true, clickable: true }, + grid: { hoverable: true, clickable: true }, + colors: querySrv.colors + }); + } + + // Populate legend + if(elem.is(":visible")){ + scripts.wait(function(){ + scope.legend = plot.getData(); + if(!scope.$$phase) { + scope.$apply(); + } + }); + } + + } catch(e) { + elem.text(e); + } + }); + } + + function tt(x, y, contents) { + var tooltip = $('#pie-tooltip').length ? + $('#pie-tooltip') : $('
'); + //var tooltip = $('#pie-tooltip') + tooltip.html(contents).css({ + position: 'absolute', + top : y + 5, + left : x + 5, + color : "#c8c8c8", + padding : '10px', + 'font-size': '11pt', + 'font-weight' : 200, + 'background-color': '#1f1f1f', + 'border-radius': '5px', + }).appendTo("body"); + } + + elem.bind("plotclick", function (event, pos, object) { + if(object) { + scope.build_search(scope.data[object.seriesIndex]); + } + }); + + elem.bind("plothover", function (event, pos, item) { + if (item) { + var value = scope.panel.chart === 'bar' ? + item.datapoint[1] : item.datapoint[1][0][1]; + tt(pos.pageX, pos.pageY, + "
"+item.series.label+ + " ("+value.toFixed(0)+")"); + } else { + $("#pie-tooltip").remove(); + } + }); + + } + }; +});