From bf456c864eb08cc01bf2f9d5a92921ba7062e6ec Mon Sep 17 00:00:00 2001 From: Rashid Khan Date: Thu, 23 May 2013 17:28:50 -0700 Subject: [PATCH] Added trend panel, moved index calculation to service, made chart backgrounds transparent --- common/css/bootstrap.dark.min.css | 20 +-- common/css/main.css | 8 ++ config.js | 2 +- js/services.js | 79 ++++++++++++ panels/histogram/editor.html | 4 +- panels/histogram/module.js | 2 +- panels/hits/editor.html | 6 +- panels/hits/module.js | 11 ++ panels/map/module.js | 8 +- panels/pie/module.js | 3 +- panels/timepicker/module.js | 95 ++------------ panels/trends/editor.html | 64 +++++++++ panels/trends/module.html | 10 ++ panels/trends/module.js | 207 ++++++++++++++++++++++++++++++ 14 files changed, 413 insertions(+), 106 deletions(-) create mode 100644 panels/trends/editor.html create mode 100644 panels/trends/module.html create mode 100644 panels/trends/module.js diff --git a/common/css/bootstrap.dark.min.css b/common/css/bootstrap.dark.min.css index 4bc16c742fd..94240b74af3 100644 --- a/common/css/bootstrap.dark.min.css +++ b/common/css/bootstrap.dark.min.css @@ -226,7 +226,7 @@ body { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px; line-height: 21px; - color: #c8c8c8; + color: #ddd; background-color: #272b30; } @@ -1096,7 +1096,7 @@ input[type="color"], margin-bottom: 10.5px; font-size: 14px; line-height: 21px; - color: #c8c8c8; + color: #fff; vertical-align: middle; -webkit-border-radius: 4px; -moz-border-radius: 4px; @@ -1129,8 +1129,8 @@ input[type="search"], input[type="tel"], input[type="color"], .uneditable-input { - background-color: #222; - border: 1px solid #333; + background-color: #666; + border: 1px solid #666; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); @@ -1197,8 +1197,8 @@ input[type="file"] { select { width: 220px; - background-color: #222; - border: 1px solid #333; + background-color: #666; + border: 1px solid #666; } select[multiple], @@ -5201,8 +5201,8 @@ input[type="submit"].btn.btn-mini { top: 10%; left: 50%; z-index: 1050; - width: 560px; - margin-left: -280px; + width: 760px; + margin-left: -380px; background-color: #ffffff; border: 1px solid #999; border: 1px solid rgba(0, 0, 0, 0.3); @@ -5228,7 +5228,7 @@ input[type="submit"].btn.btn-mini { } .modal.fade.in { - top: 10%; + top: 10px; } .modal-header { @@ -5247,7 +5247,7 @@ input[type="submit"].btn.btn-mini { .modal-body { position: relative; - max-height: 400px; + max-height: 500px; padding: 15px; overflow-y: auto; } diff --git a/common/css/main.css b/common/css/main.css index 5395239a293..ed32b431e62 100644 --- a/common/css/main.css +++ b/common/css/main.css @@ -131,6 +131,14 @@ font-weight: bold; } +.normal { + font-weight: normal; +} + +.light { + font-weight: 200; +} + .input-append label { font-size: inherit !important; } diff --git a/config.js b/config.js index f4ffde85bbd..a99a3c0c5fe 100644 --- a/config.js +++ b/config.js @@ -22,6 +22,6 @@ var config = new Settings( kibana_index: "kibana-int", modules: ['histogram','map','pie','table','stringquery','sort', 'timepicker','text','fields','hits','dashcontrol', - 'column','derivequeries'], + 'column','derivequeries','trends'], } ); diff --git a/js/services.js b/js/services.js index 84ec92dba3c..d0716c3dc11 100644 --- a/js/services.js +++ b/js/services.js @@ -83,6 +83,85 @@ angular.module('kibana.services', []) return fields; }) +.service('kbnIndex',function($http) { + // returns a promise containing an array of all indices matching the index + // pattern that exist in a given range + this.indices = function(from,to,pattern,interval) { + var possible = []; + _.each(expand_range(fake_utc(from),fake_utc(to),interval),function(d){ + possible.push(d.format(pattern)); + }); + + return all_indices().then(function(p) { + var indices = _.intersection(possible,p); + indices.reverse(); + return indices + }) + }; + + // returns a promise containing an array of all indices in an elasticsearch + // cluster + function all_indices() { + var something = $http({ + url: config.elasticsearch + "/_aliases", + method: "GET" + }).error(function(data, status, headers, config) { + // Handle error condition somehow? + }); + + return something.then(function(p) { + var indices = []; + _.each(p.data, function(v,k) { + indices.push(k) + }); + return indices; + }); + } + + // this is stupid, but there is otherwise no good way to ensure that when + // I extract the date from an object that I'm get the UTC date. Stupid js. + // I die a little inside every time I call this function. + // Update: I just read this again. I died a little more inside. + // Update2: More death. + function fake_utc(date) { + date = moment(date).clone().toDate() + return moment(new Date(date.getTime() + date.getTimezoneOffset() * 60000)); + } + + // Create an array of date objects by a given interval + function expand_range(start, end, interval) { + if(_.contains(['hour','day','week','month','year'],interval)) { + var range; + start = moment(start).clone(); + range = []; + while (start.isBefore(end)) { + range.push(start.clone()); + switch (interval) { + case 'hour': + start.add('hours',1) + break + case 'day': + start.add('days',1) + break + case 'week': + start.add('weeks',1) + break + case 'month': + start.add('months',1) + break + case 'year': + start.add('years',1) + break + } + } + range.push(moment(end).clone()); + return range; + } else { + return false; + } + } +}) + .service('timer', function($timeout) { // This service really just tracks a list of $timeout promises to give us a // method for cancelling them all when we need to diff --git a/panels/histogram/editor.html b/panels/histogram/editor.html index e7bc9eabd26..37da0ca6af5 100644 --- a/panels/histogram/editor.html +++ b/panels/histogram/editor.html @@ -25,7 +25,7 @@
- +
@@ -34,7 +34,7 @@
- +
diff --git a/panels/histogram/module.js b/panels/histogram/module.js index 8e84e8aae37..fde3cf8e947 100644 --- a/panels/histogram/module.js +++ b/panels/histogram/module.js @@ -315,7 +315,7 @@ angular.module('kibana.histogram', []) color: '#ccc' }, grid: { - backgroundColor: '#272b30', + backgroundColor: null, borderWidth: 0, borderColor: '#eee', color: "#eee", diff --git a/panels/hits/editor.html b/panels/hits/editor.html index 801d7fe041b..9704cbe3f11 100644 --- a/panels/hits/editor.html +++ b/panels/hits/editor.html @@ -38,7 +38,7 @@
- +
@@ -47,12 +47,12 @@
- +
- +
diff --git a/panels/hits/module.js b/panels/hits/module.js index f77730e564e..bb44f1abfda 100644 --- a/panels/hits/module.js +++ b/panels/hits/module.js @@ -141,6 +141,17 @@ angular.module('kibana.hits', []) $scope.get_data(); } + $scope.set_refresh = function (state) { + $scope.refresh = state; + } + + $scope.close_edit = function() { + if($scope.refresh) + $scope.get_data(); + $scope.refresh = false; + $scope.$emit('render'); + } + function set_time(time) { $scope.time = time; $scope.index = _.isUndefined(time.index) ? $scope.index : time.index diff --git a/panels/map/module.js b/panels/map/module.js index 28fe11b5924..0161e2cd461 100644 --- a/panels/map/module.js +++ b/panels/map/module.js @@ -145,7 +145,7 @@ angular.module('kibana.map', []) map: scope.panel.map, regionStyle: {initial: {fill: '#8c8c8c'}}, zoomOnScroll: false, - backgroundColor: '#272b30', + backgroundColor: null, series: { regions: [{ values: scope.data, @@ -157,9 +157,9 @@ angular.module('kibana.map', []) $('.jvectormap-label').css({ "position" : "absolute", "display" : "none", - 'color' : "#c8c8c8", - 'padding' : '10px', - 'font-size': '11pt', + 'color' : "#c8c8c8", + 'padding' : '10px', + 'font-size' : '11pt', 'font-weight' : 200, 'background-color': '#1f1f1f', 'border-radius': '5px' diff --git a/panels/pie/module.js b/panels/pie/module.js index 3ff06940f83..0d43348464a 100644 --- a/panels/pie/module.js +++ b/panels/pie/module.js @@ -246,14 +246,13 @@ angular.module('kibana.pie', []) }, label: label, stroke: { - color: '#272b30', width: 0 } } }, //grid: { hoverable: true, clickable: true }, grid: { - backgroundColor: '#272b30', + backgroundColor: null, hoverable: true, clickable: true }, diff --git a/panels/timepicker/module.js b/panels/timepicker/module.js index 06432a4df04..c4e4ef41c97 100644 --- a/panels/timepicker/module.js +++ b/panels/timepicker/module.js @@ -28,7 +28,7 @@ */ angular.module('kibana.timepicker', []) -.controller('timepicker', function($scope, eventBus, $timeout, timer, $http) { +.controller('timepicker', function($scope, eventBus, $timeout, timer, $http, kbnIndex) { // Set and populate defaults var _d = { @@ -89,7 +89,7 @@ angular.module('kibana.timepicker', []) // request one be sent by broadcasting a 'get_time' with its _id to its group // This panel can handle multiple groups eventBus.register($scope,"get_time", function(event,id) { - eventBus.broadcast($scope.$id,id,'time',unmoment($scope.time)) + eventBus.broadcast($scope.$id,id,'time',compile_time($scope.time)) }); // In case some other panel broadcasts a time, set us to an absolute range @@ -208,13 +208,17 @@ angular.module('kibana.timepicker', []) // Get indices for the time period, then broadcast time range and index list // in a single object. Not sure if I like this. if($scope.panel.index_interval !== 'none') { - indices($scope.time.from,$scope.time.to).then(function (p) { + kbnIndex.indices($scope.time.from, + $scope.time.to, + $scope.panel.index, + $scope.panel.index_interval + ).then(function (p) { $scope.time.index = p; - eventBus.broadcast($scope.$id,$scope.panel.group,'time',unmoment($scope.time)) + eventBus.broadcast($scope.$id,$scope.panel.group,'time',compile_time($scope.time)) }); } else { $scope.time.index = [$scope.panel.index]; - eventBus.broadcast($scope.$id,$scope.panel.group,'time',unmoment($scope.time)) + eventBus.broadcast($scope.$id,$scope.panel.group,'time',compile_time($scope.time)) } // Update panel's string representation of the time object.Don't update if @@ -233,10 +237,12 @@ angular.module('kibana.timepicker', []) // Prefer to pass around Date() objects in the EventBus since interacting with // moment objects in libraries that are expecting Date()s can be tricky - function unmoment(time) { + function compile_time(time) { time = _.clone(time) time.from = time.from.toDate() time.to = time.to.toDate() + time.interval = $scope.panel.index_interval + time.pattern = $scope.panel.index return time; } @@ -254,81 +260,4 @@ angular.module('kibana.timepicker', []) } } - // returns a promise containing an array of all indices matching the index - // pattern that exist in a given range - function indices(from,to) { - var possible = []; - _.each(expand_range(fake_utc(from),fake_utc(to),$scope.panel.index_interval),function(d){ - possible.push(d.format($scope.panel.index)); - }); - - return all_indices().then(function(p) { - var indices = _.intersection(possible,p); - indices.reverse(); - return indices.length == 0 ? [$scope.panel.defaultindex] : indices; - }) - }; - - // returns a promise containing an array of all indices in an elasticsearch - // cluster - function all_indices() { - var something = $http({ - url: config.elasticsearch + "/_aliases", - method: "GET" - }).error(function(data, status, headers, config) { - $scope.error = status; - }); - - return something.then(function(p) { - var indices = []; - _.each(p.data, function(v,k) { - indices.push(k) - }); - return indices; - }); - } - - // this is stupid, but there is otherwise no good way to ensure that when - // I extract the date from an object that I'm get the UTC date. Stupid js. - // I die a little inside every time I call this function. - // Update: I just read this again. I died a little more inside. - // Update2: More death. - function fake_utc(date) { - date = date.clone().toDate() - return moment(new Date(date.getTime() + date.getTimezoneOffset() * 60000)); - } - - // Create an array of date objects by a given interval - function expand_range(start, end, interval) { - if(_.contains(['hour','day','week','month','year'],interval)) { - var range; - start = start.clone(); - range = []; - while (start.isBefore(end)) { - range.push(start.clone()); - switch (interval) { - case 'hour': - start.add('hours',1) - break - case 'day': - start.add('days',1) - break - case 'week': - start.add('weeks',1) - break - case 'month': - start.add('months',1) - break - case 'year': - start.add('years',1) - break - } - } - range.push(end.clone()); - return range; - } else { - return false; - } - } - }) \ No newline at end of file diff --git a/panels/trends/editor.html b/panels/trends/editor.html new file mode 100644 index 00000000000..092f909b20c --- /dev/null +++ b/panels/trends/editor.html @@ -0,0 +1,64 @@ +
+
+
+ The trends panel will give you a percentage representation of how your query + has moved in your current timespan compared a specified amount of time ago. For + example, if the time is 1:10pm, your time picker was set to "Last 10m", and the + "Time Ago" parameter was set to '1h', the panel would show how much the query + results have changed since 12:00-12:10pm +
+
+

Settings

+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
Queries
+
+
+
+ + +
+
+
+
+ + + +
+
+
+
+
+
+
+
+ +
+
+
+
+ + +
+
+
+ +
+
+
diff --git a/panels/trends/module.html b/panels/trends/module.html new file mode 100644 index 00000000000..94f09c42411 --- /dev/null +++ b/panels/trends/module.html @@ -0,0 +1,10 @@ + + +
+ + {{query.percent}}% + + ({{query.label}}) +
+
+
\ No newline at end of file diff --git a/panels/trends/module.js b/panels/trends/module.js new file mode 100644 index 00000000000..83cbb71716d --- /dev/null +++ b/panels/trends/module.js @@ -0,0 +1,207 @@ +/* + + ## Hits + + A variety of representations of the hits a query matches + + ### Parameters + * query :: An array of queries. No labels here, just an array of strings. Maybe + there should be labels. Probably. + * style :: A hash of css styles + * arrangement :: How should I arrange the query results? 'horizontal' or 'vertical' + * ago :: Date math formatted time to look back + ### Group Events + #### Sends + * get_time :: On panel initialization get time range to query + #### Receives + * time :: An object containing the time range to use and the index(es) to query + * query :: An Array of queries, even if its only one + +*/ +angular.module('kibana.trends', []) +.controller('trends', function($scope, eventBus, kbnIndex) { + + // Set and populate defaults + var _d = { + query : ["*"], + group : "default", + style : { "font-size": '14pt'}, + ago : '1d', + arrangement : 'vertical', + } + _.defaults($scope.panel,_d) + + $scope.init = function () { + $scope.hits = 0; + eventBus.register($scope,'time', function(event,time){ + set_time(time) + }); + eventBus.register($scope,'query', function(event, query) { + $scope.panel.query = _.map(query,function(q) { + return {query: q, label: q}; + }) + $scope.get_data(); + }); + // Now that we're all setup, request the time from our group + eventBus.broadcast($scope.$id,$scope.panel.group,'get_time') + } + + $scope.get_data = function(segment,query_id) { + delete $scope.panel.error + $scope.panel.loading = true; + + // Make sure we have everything for the request to complete + if(_.isUndefined($scope.index) || _.isUndefined($scope.time)) + return + + $scope.old_time = { + from : new Date($scope.time.from.getTime() - interval_to_seconds($scope.panel.ago)*1000), + to : new Date($scope.time.to.getTime() - interval_to_seconds($scope.panel.ago)*1000) + } + + var _segment = _.isUndefined(segment) ? 0 : segment + var request = $scope.ejs.Request(); + + // Build the question part of the query + var queries = []; + _.each($scope.panel.query, function(v) { + queries.push($scope.ejs.FilteredQuery( + ejs.QueryStringQuery(v.query || '*'), + ejs.RangeFilter($scope.time.field) + .from($scope.time.from) + .to($scope.time.to)) + ) + }); + + // Build the facet part + _.each(queries, function(v) { + request = request + .facet($scope.ejs.QueryFacet("new"+_.indexOf(queries,v)) + .query(v) + ).size(0) + }) + + var queries = []; + _.each($scope.panel.query, function(v) { + queries.push($scope.ejs.FilteredQuery( + ejs.QueryStringQuery(v.query || '*'), + ejs.RangeFilter($scope.time.field) + .from($scope.old_time.from) + .to($scope.old_time.to)) + ) + }); + + // Build the facet part + _.each(queries, function(v) { + request = request + .facet($scope.ejs.QueryFacet("old"+_.indexOf(queries,v)) + .query(v) + ).size(0) + }) + + // TODO: Spy for hits panel + //$scope.populate_modal(request); + + // If we're on the first segment we need to get our indices + if (_segment == 0) { + kbnIndex.indices( + $scope.old_time.from, + $scope.old_time.to, + $scope.time.pattern, + $scope.time.interval + ).then(function (p) { + $scope.index = _.union(p,$scope.index); + process_results(request.indices($scope.index[_segment]).doSearch()); + }); + } else { + process_results(request.indices($scope.index[_segment]).doSearch()); + } + + // Populate scope when we have results + function process_results(results) { + results.then(function(results) { + + $scope.panel.loading = false; + if(_segment == 0) { + $scope.hits = {}; + $scope.data = []; + query_id = $scope.query_id = new Date().getTime(); + } + + // Check for error and abort if found + if(!(_.isUndefined(results.error))) { + $scope.panel.error = $scope.parse_error(results.error); + return; + } + if($scope.query_id === query_id) { + var i = 0; + _.each($scope.panel.query, function(k) { + var n = results.facets['new'+i].count + var o = results.facets['old'+i].count + + var hits = { + new : _.isUndefined($scope.data[i]) || _segment == 0 ? n : $scope.data[i].hits.new+n, + old : _.isUndefined($scope.data[i]) || _segment == 0 ? o : $scope.data[i].hits.old+o + } + + $scope.hits.new += n; + $scope.hits.old += o; + + var percent = Math.round(percentage(hits.old,hits.new)*100)/100 + // Create series + $scope.data[i] = { + label: $scope.panel.query[i].label || "query"+(parseInt(i)+1), + hits: { + new : hits.new, + old : hits.old + }, + percent: _.isNull(percent) ? 0 : percent + }; + + i++; + }); + $scope.$emit('render'); + if(_segment < $scope.index.length-1) + $scope.get_data(_segment+1,query_id) + + } + }); + } + + } + + function percentage(x,y) { + return 100*(y-x)/x + } + + $scope.remove_query = function(q) { + $scope.panel.query = _.without($scope.panel.query,q); + $scope.get_data(); + } + + $scope.add_query = function(label,query) { + $scope.panel.query.unshift({ + query: query, + label: label, + }); + $scope.get_data(); + } + + $scope.set_refresh = function (state) { + $scope.refresh = state; + } + + $scope.close_edit = function() { + if($scope.refresh) + $scope.get_data(); + $scope.refresh = false; + $scope.$emit('render'); + } + + function set_time(time) { + $scope.time = time; + $scope.index = time.index || $scope.index + $scope.get_data(); + } + +}) \ No newline at end of file