From 85f241a4d89dc236dc54032de4a47c3cbf64859a Mon Sep 17 00:00:00 2001 From: Spencer Alger Date: Fri, 23 Aug 2013 13:20:31 -0700 Subject: [PATCH 01/23] Restored the "fill all the holes" strategy to the ZeroFill class, and introduced the fill_style option ("minimal" and "all" are possible values). Also allowed a set of required times to be specified when asking the times series for it's data. This way, we can ensure in the stacked bar chart that each data point in use has a value preventing the bars from stacking incorrectly. --- panels/histogram/module.js | 188 +++++++++++++++++++++++++++---------- 1 file changed, 137 insertions(+), 51 deletions(-) diff --git a/panels/histogram/module.js b/panels/histogram/module.js index 5593e6d9e90..ee184fdc3e5 100644 --- a/panels/histogram/module.js +++ b/panels/histogram/module.js @@ -197,12 +197,12 @@ angular.module('kibana.histogram', []) // we need to initialize the data variable on the first run, // and when we are working on the first segment of the data. if(_.isUndefined($scope.data[i]) || segment === 0) { - time_series = new timeSeries.ZeroFilled( - _interval, - // range may be false - _range && _range.from, - _range && _range.to - ); + time_series = new timeSeries.ZeroFilled({ + interval: _interval, + start_date: _range && _range.from, + end_date: _range && _range.to, + fillStyle: 'minimal' + }); hits = 0; } else { time_series = $scope.data[i].time_series; @@ -216,9 +216,8 @@ angular.module('kibana.histogram', []) $scope.hits += entry.count; // Entire dataset level hits counter }); $scope.data[i] = { - time_series: time_series, info: querySrv.list[id], - data: time_series.getFlotPairs(), + time_series: time_series, hits: hits }; @@ -310,7 +309,7 @@ angular.module('kibana.histogram', []) // Populate from the query service try { - _.each(scope.data,function(series) { + _.each(scope.data, function(series) { series.label = series.info.alias; series.color = series.info.color; }); @@ -383,6 +382,19 @@ angular.module('kibana.histogram', []) options.selection = { mode: "x", color: '#666' }; } + // when rendering stacked bars, we need to ensure each point that has data is zero-filled + // so that the stacking happens in the proper order + var required_times = []; + if (scope.panel.bars && stack) { + required_times = Array.prototype.concat.apply([], _.map(scope.data, function (series) { + return series.time_series.getOrderedTimes(); + })); + } + + for (var i = 0; i < scope.data.length; i++) { + scope.data[i].data = scope.data[i].time_series.getFlotPairs(required_times); + } + scope.plot = $.plot(elem, scope.data, options); } catch(e) { @@ -448,36 +460,53 @@ angular.module('kibana.histogram', []) }; }) .service('timeSeries', function () { + // map compatable parseInt + function base10Int(val) { + return parseInt(val, 10); + } + /** * Certain graphs require 0 entries to be specified for them to render * properly (like the line graph). So with this we will caluclate all of * the expected time measurements, and fill the missing ones in with 0 - * @param date start The start time for the result set - * @param date end The end time for the result set - * @param integer interval The length between measurements, in es interval - * notation (1m, 30s, 1h, 15d) + * @param object opts An object specifying some/all of the options + * + * OPTIONS: + * @opt string interval The interval notion describing the expected spacing between + * each data point. + * @opt date start_date (optional) The start point for the time series, setting this and the + * end_date will ensure that the series streches to resemble the entire + * expected result + * @opt date end_date (optional) The end point for the time series, see start_date + * @opt string fill_style Either "minimal", or "all" describing the strategy used to zero-fill + * the series. */ - var undef; - function base10Int(val) { - return parseInt(val, 10); - } - this.ZeroFilled = function (interval, start, end) { + this.ZeroFilled = function (opts) { + this.opts = _.defaults(opts, { + interval: '10m', + start_date: null, + end_date: null, + fill_style: 'minimal' + }); + // the expected differenece between readings. - this.interval_ms = base10Int(kbn.interval_to_seconds(interval)) * 1000; + this.interval_ms = base10Int(kbn.interval_to_seconds(opts.interval)) * 1000; + // will keep all values here, keyed by their time this._data = {}; - if (start) { - this.addValue(start, null); + if (opts.start_date) { + this.addValue(opts.start_date, null); } - if (end) { - this.addValue(end, null); + if (opts.end_date) { + this.addValue(opts.end_date, null); } }; + /** * Add a row - * @param int time The time for the value, in - * @param any value The value at this time + * @param {int} time The time for the value, in + * @param {any} value The value at this time */ this.ZeroFilled.prototype.addValue = function (time, value) { if (time instanceof Date) { @@ -486,44 +515,101 @@ angular.module('kibana.histogram', []) time = base10Int(time); } if (!isNaN(time)) { - this._data[time] = (value === undef ? 0 : value); + this._data[time] = (_.isUndefined(value) ? 0 : value); } + this._cached_times = null; }; + + /** + * Get an array of the times that have been explicitly set in the series + * @param {array} include (optional) list of timestamps to include in the response + * @return {array} An array of integer times. + */ + this.ZeroFilled.prototype.getOrderedTimes = function (include) { + var times = _.map(_.keys(this._data), base10Int).sort(); + if (_.isArray(include)) { + times = times.concat(include); + } + return times; + }; + /** * return the rows in the format: * [ [time, value], [time, value], ... ] - * @return array + * + * Heavy lifting is done by _get(Min|All)FlotPairs() + * @param {array} required_times An array of timestamps that must be in the resulting pairs + * @return {array} */ - this.ZeroFilled.prototype.getFlotPairs = function () { - // var startTime = performance.now(); - var times = _.map(_.keys(this._data), base10Int).sort(), - result = []; - _.each(times, function (time, i, times) { - var next, expected_next, prev, expected_prev; + this.ZeroFilled.prototype.getFlotPairs = function (required_times) { + var times = this.getOrderedTimes(required_times), + strategy, + pairs; - // check for previous measurement - if (i > 0) { - prev = times[i - 1]; - expected_prev = time - this.interval_ms; - if (prev < expected_prev) { - result.push([expected_prev, 0]); - } + if(this.opts.fill_style === 'all') { + strategy = this._getAllFlotPairs; + } else { + strategy = this._getMinFlotPairs; + } + + return _.reduce( + times, // what + strategy, // how + [], // where + this // context + ); + }; + + /** + * ** called as a reduce stragegy in getFlotPairs() ** + * Fill zero's on either side of the current time, unless there is already a measurement there or + * we are looking at an edge. + * @return {array} An array of points to plot with flot + */ + this.ZeroFilled.prototype._getMinFlotPairs = function (result, time, i, times) { + var next, expected_next, prev, expected_prev; + + // check for previous measurement + if (i > 0) { + prev = times[i - 1]; + expected_prev = time - this.interval_ms; + if (prev < expected_prev) { + result.push([expected_prev, 0]); } + } - // add the current time - result.push([ time, this._data[time] ]); + // add the current time + result.push([ time, this._data[time] || 0 ]); - // check for next measurement - if (times.length > i) { - next = times[i + 1]; - expected_next = time + this.interval_ms; - if (next > expected_next) { - result.push([expected_next, 0]); - } + // check for next measurement + if (times.length > i) { + next = times[i + 1]; + expected_next = time + this.interval_ms; + if (next > expected_next) { + result.push([expected_next, 0]); } + } - }, this); - // console.log(Math.round((performance.now() - startTime)*100)/100, 'ms to get', result.length, 'pairs'); return result; }; + + /** + * ** called as a reduce stragegy in getFlotPairs() ** + * Fill zero's to the right of each time, until the next measurement is reached or we are at the + * last measurement + * @return {array} An array of points to plot with flot + */ + this.ZeroFilled.prototype._getAllFlotPairs = function (result, time, i, times) { + var next, expected_next; + + result.push([ times[i], this._data[times[i]] || 0 ]); + next = times[i + 1]; + expected_next = times[i] + this.interval_ms; + for(; times.length > i && next > expected_next; expected_next+= this.interval_ms) { + result.push([expected_next, 0]); + } + + return result; + }; + }); \ No newline at end of file From 9b62617ede066d1b8c011248f8a1c72c76e0ed00 Mon Sep 17 00:00:00 2001 From: Spencer Alger Date: Fri, 23 Aug 2013 14:31:59 -0700 Subject: [PATCH 02/23] cleaned up the documentation a bit --- panels/histogram/module.js | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/panels/histogram/module.js b/panels/histogram/module.js index ee184fdc3e5..ed1eec0fcbc 100644 --- a/panels/histogram/module.js +++ b/panels/histogram/module.js @@ -108,11 +108,17 @@ angular.module('kibana.histogram', []) $scope.panel.interval = interval || '10m'; return $scope.panel.interval; }; + /** * Fetch the data for a chunk of a queries results. Multiple segments occur when several indicies * need to be consulted (like timestamped logstash indicies) - * @param number segment The segment count, (0 based) - * @param number query_id The id of the query, generated on the first run and passed back when + * + * The results of this function are stored on the scope's data property. This property will be an + * array of objects with the properties info, time_series, and hits. These objects are used in the + * render_panel function to create the historgram. + * + * @param {number} segment The segment count, (0 based) + * @param {number} query_id The id of the query, generated on the first run and passed back when * this call is made recursively for more segments */ $scope.get_data = function(segment, query_id) { @@ -469,17 +475,17 @@ angular.module('kibana.histogram', []) * Certain graphs require 0 entries to be specified for them to render * properly (like the line graph). So with this we will caluclate all of * the expected time measurements, and fill the missing ones in with 0 - * @param object opts An object specifying some/all of the options + * @param {object} opts An object specifying some/all of the options * * OPTIONS: - * @opt string interval The interval notion describing the expected spacing between - * each data point. - * @opt date start_date (optional) The start point for the time series, setting this and the - * end_date will ensure that the series streches to resemble the entire - * expected result - * @opt date end_date (optional) The end point for the time series, see start_date - * @opt string fill_style Either "minimal", or "all" describing the strategy used to zero-fill - * the series. + * @opt {string} interval The interval notion describing the expected spacing between + * each data point. + * @opt {date} start_date (optional) The start point for the time series, setting this and the + * end_date will ensure that the series streches to resemble the entire + * expected result + * @opt {date} end_date (optional) The end point for the time series, see start_date + * @opt {string} fill_style Either "minimal", or "all" describing the strategy used to zero-fill + * the series. */ this.ZeroFilled = function (opts) { this.opts = _.defaults(opts, { @@ -522,7 +528,7 @@ angular.module('kibana.histogram', []) /** * Get an array of the times that have been explicitly set in the series - * @param {array} include (optional) list of timestamps to include in the response + * @param {array} include (optional) list of timestamps to include in the response * @return {array} An array of integer times. */ this.ZeroFilled.prototype.getOrderedTimes = function (include) { @@ -538,7 +544,7 @@ angular.module('kibana.histogram', []) * [ [time, value], [time, value], ... ] * * Heavy lifting is done by _get(Min|All)FlotPairs() - * @param {array} required_times An array of timestamps that must be in the resulting pairs + * @param {array} required_times An array of timestamps that must be in the resulting pairs * @return {array} */ this.ZeroFilled.prototype.getFlotPairs = function (required_times) { From 252b2fd651e949e33d558c9bb83551e442c661dc Mon Sep 17 00:00:00 2001 From: Spencer Alger Date: Sat, 24 Aug 2013 14:24:54 -0700 Subject: [PATCH 03/23] fixed a property typo --- panels/histogram/module.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panels/histogram/module.js b/panels/histogram/module.js index ed1eec0fcbc..fe7de099773 100644 --- a/panels/histogram/module.js +++ b/panels/histogram/module.js @@ -207,7 +207,7 @@ angular.module('kibana.histogram', []) interval: _interval, start_date: _range && _range.from, end_date: _range && _range.to, - fillStyle: 'minimal' + fill_style: 'minimal' }); hits = 0; } else { From 556925f840f4680a3be61db51577ceada022d4f3 Mon Sep 17 00:00:00 2001 From: Spencer Alger Date: Mon, 26 Aug 2013 08:51:51 -0700 Subject: [PATCH 04/23] added check for 404 response from the index_save call, assumes that auto_create_index is disabled and attempts to create the index before saving a second time --- js/services.js | 4 +++ panels/dashcontrol/module.js | 59 +++++++++++++++++++++++++----------- 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/js/services.js b/js/services.js index 101579b4de7..4ad364f744c 100644 --- a/js/services.js +++ b/js/services.js @@ -800,6 +800,10 @@ angular.module('kibana.services', []) ); }; + this.elasticsearch_create_index = function () { + return ejs.client.post('/'+config.kibana_index); + }; + this.elasticsearch_delete = function(id) { return ejs.Document(config.kibana_index,'dashboard',id).doDelete( // Success diff --git a/panels/dashcontrol/module.js b/panels/dashcontrol/module.js index 1119943b34b..015898e048a 100644 --- a/panels/dashcontrol/module.js +++ b/panels/dashcontrol/module.js @@ -17,7 +17,7 @@ * hide_control :: Upon save, hide this panel * elasticsearch_size :: show this many dashboards under the ES section in the load drop down * temp :: Allow saving of temp dashboards - * ttl :: Enable setting ttl. + * ttl :: Enable setting ttl. * temp_ttl :: How long should temp dashboards persist */ @@ -61,6 +61,10 @@ angular.module('kibana.dashcontrol', []) services: {} }; + function notice(type, title, message) { + alertSrv.set(title, message, type, 5000); + } + $scope.init = function() { $scope.gist_pattern = /(^\d{5,}$)|(^[a-z0-9]{10,}$)|(gist.github.com(\/*.*)\/[a-z0-9]{5,}\/*$)/; $scope.gist = {}; @@ -69,17 +73,17 @@ angular.module('kibana.dashcontrol', []) $scope.set_default = function() { if(dashboard.set_default()) { - alertSrv.set('Local Default Set',dashboard.current.title+' has been set as your local default','success',5000); + notice('success', 'Local Default Set', dashboard.current.title+' has been set as your local default'); } else { - alertSrv.set('Incompatible Browser','Sorry, your browser is too old for this feature','error',5000); + notice('error', 'Incompatible Browser', 'Sorry, your browser is too old for this feature'); } }; $scope.purge_default = function() { if(dashboard.purge_default()) { - alertSrv.set('Local Default Clear','Your local default dashboard has been cleared','success',5000); + notice('success', 'Local Default Clear', 'Your local default dashboard has been cleared'); } else { - alertSrv.set('Incompatible Browser','Sorry, your browser is too old for this feature','error',5000); + notice('error', 'Incompatible Browser', 'Sorry, your browser is too old for this feature'); } }; @@ -91,13 +95,30 @@ angular.module('kibana.dashcontrol', []) ).then( function(result) { if(!_.isUndefined(result._id)) { - alertSrv.set('Dashboard Saved','This dashboard has been saved to Elasticsearch as "' + - result._id + '"','success',5000); + notice( + 'success', + 'Dashboard Saved', + 'This dashboard has been saved to Elasticsearch as "'+result._id + '"' + ); if(type === 'temp') { - $scope.share = dashboard.share_link(dashboard.current.title,'temp',result._id); + $scope.share = dashboard.share_link(dashboard.current.title, 'temp', result._id); } } else { - alertSrv.set('Save failed','Dashboard could not be saved to Elasticsearch','error',5000); + if (result.status === 404) { + // auto create must be disabled and the index doesn't exist, create it! + return dashboard.elasticsearch_create_index().then(function () { + return $scope.elasticsearch_save(type, ttl); + }, function () { + notice( + 'error', + 'Save failed', + 'Dashboard could not be saved because the "'+config.kibana_index+'" '+ + 'index does not exist and could not be created.' + ); + }); + } else { + notice('error', 'Save failed', 'Dashboard could not be saved to Elasticsearch'); + } } }); }; @@ -107,15 +128,15 @@ angular.module('kibana.dashcontrol', []) function(result) { if(!_.isUndefined(result)) { if(result.found) { - alertSrv.set('Dashboard Deleted',id+' has been deleted','success',5000); + notice('success', 'Dashboard Deleted', id+' has been deleted'); // Find the deleted dashboard in the cached list and remove it var toDelete = _.where($scope.elasticsearch.dashboards,{_id:id})[0]; $scope.elasticsearch.dashboards = _.without($scope.elasticsearch.dashboards,toDelete); } else { - alertSrv.set('Dashboard Not Found','Could not find '+id+' in Elasticsearch','warning',5000); + notice('warning', 'Dashboard Not Found', 'Could not find '+id+' in Elasticsearch'); } } else { - alertSrv.set('Dashboard Not Deleted','An error occurred deleting the dashboard','error',5000); + notice('error', 'Dashboard Not Deleted', 'An error occurred deleting the dashboard'); } } ); @@ -137,10 +158,14 @@ angular.module('kibana.dashcontrol', []) function(link) { if(!_.isUndefined(link)) { $scope.gist.last = link; - alertSrv.set('Gist saved','You will be able to access your exported dashboard file at '+ - ''+link+' in a moment','success'); + notice( + 'success', + 'Gist saved', + 'You will be able to access your exported dashboard file at '+ + ''+link+' in a moment' + ); } else { - alertSrv.set('Save failed','Gist could not be saved','error',5000); + notice('error', 'Save failed', 'Gist could not be saved'); } }); }; @@ -151,7 +176,7 @@ angular.module('kibana.dashcontrol', []) if(files && files.length > 0) { $scope.gist.files = files; } else { - alertSrv.set('Gist Failed','Could not retrieve dashboard list from gist','error',5000); + notice('error', 'Gist Failed', 'Could not retrieve dashboard list from gist',5000); } }); }; @@ -183,7 +208,7 @@ angular.module('kibana.dashcontrol', []) // Something document.getElementById('dashupload').addEventListener('change', file_selected, false); } else { - alertSrv.set('Oops','Sorry, the HTML5 File APIs are not fully supported in this browser.','error'); + alertSrv.set('Oops', 'Sorry, the HTML5 File APIs are not fully supported in this browser.', 'error', 5000); } } }; From e12a78514bff7cf36a095b8e621d44bb3337a456 Mon Sep 17 00:00:00 2001 From: Rashid Khan Date: Mon, 26 Aug 2013 13:38:44 -0700 Subject: [PATCH 05/23] Revert "added check for 404 response from the index_save call, assumes that auto_create_index is disabled and attempts to create the index before saving a second time" This reverts commit 556925f840f4680a3be61db51577ceada022d4f3. --- js/services.js | 4 --- panels/dashcontrol/module.js | 59 +++++++++++------------------------- 2 files changed, 17 insertions(+), 46 deletions(-) diff --git a/js/services.js b/js/services.js index 4ad364f744c..101579b4de7 100644 --- a/js/services.js +++ b/js/services.js @@ -800,10 +800,6 @@ angular.module('kibana.services', []) ); }; - this.elasticsearch_create_index = function () { - return ejs.client.post('/'+config.kibana_index); - }; - this.elasticsearch_delete = function(id) { return ejs.Document(config.kibana_index,'dashboard',id).doDelete( // Success diff --git a/panels/dashcontrol/module.js b/panels/dashcontrol/module.js index 015898e048a..1119943b34b 100644 --- a/panels/dashcontrol/module.js +++ b/panels/dashcontrol/module.js @@ -17,7 +17,7 @@ * hide_control :: Upon save, hide this panel * elasticsearch_size :: show this many dashboards under the ES section in the load drop down * temp :: Allow saving of temp dashboards - * ttl :: Enable setting ttl. + * ttl :: Enable setting ttl. * temp_ttl :: How long should temp dashboards persist */ @@ -61,10 +61,6 @@ angular.module('kibana.dashcontrol', []) services: {} }; - function notice(type, title, message) { - alertSrv.set(title, message, type, 5000); - } - $scope.init = function() { $scope.gist_pattern = /(^\d{5,}$)|(^[a-z0-9]{10,}$)|(gist.github.com(\/*.*)\/[a-z0-9]{5,}\/*$)/; $scope.gist = {}; @@ -73,17 +69,17 @@ angular.module('kibana.dashcontrol', []) $scope.set_default = function() { if(dashboard.set_default()) { - notice('success', 'Local Default Set', dashboard.current.title+' has been set as your local default'); + alertSrv.set('Local Default Set',dashboard.current.title+' has been set as your local default','success',5000); } else { - notice('error', 'Incompatible Browser', 'Sorry, your browser is too old for this feature'); + alertSrv.set('Incompatible Browser','Sorry, your browser is too old for this feature','error',5000); } }; $scope.purge_default = function() { if(dashboard.purge_default()) { - notice('success', 'Local Default Clear', 'Your local default dashboard has been cleared'); + alertSrv.set('Local Default Clear','Your local default dashboard has been cleared','success',5000); } else { - notice('error', 'Incompatible Browser', 'Sorry, your browser is too old for this feature'); + alertSrv.set('Incompatible Browser','Sorry, your browser is too old for this feature','error',5000); } }; @@ -95,30 +91,13 @@ angular.module('kibana.dashcontrol', []) ).then( function(result) { if(!_.isUndefined(result._id)) { - notice( - 'success', - 'Dashboard Saved', - 'This dashboard has been saved to Elasticsearch as "'+result._id + '"' - ); + alertSrv.set('Dashboard Saved','This dashboard has been saved to Elasticsearch as "' + + result._id + '"','success',5000); if(type === 'temp') { - $scope.share = dashboard.share_link(dashboard.current.title, 'temp', result._id); + $scope.share = dashboard.share_link(dashboard.current.title,'temp',result._id); } } else { - if (result.status === 404) { - // auto create must be disabled and the index doesn't exist, create it! - return dashboard.elasticsearch_create_index().then(function () { - return $scope.elasticsearch_save(type, ttl); - }, function () { - notice( - 'error', - 'Save failed', - 'Dashboard could not be saved because the "'+config.kibana_index+'" '+ - 'index does not exist and could not be created.' - ); - }); - } else { - notice('error', 'Save failed', 'Dashboard could not be saved to Elasticsearch'); - } + alertSrv.set('Save failed','Dashboard could not be saved to Elasticsearch','error',5000); } }); }; @@ -128,15 +107,15 @@ angular.module('kibana.dashcontrol', []) function(result) { if(!_.isUndefined(result)) { if(result.found) { - notice('success', 'Dashboard Deleted', id+' has been deleted'); + alertSrv.set('Dashboard Deleted',id+' has been deleted','success',5000); // Find the deleted dashboard in the cached list and remove it var toDelete = _.where($scope.elasticsearch.dashboards,{_id:id})[0]; $scope.elasticsearch.dashboards = _.without($scope.elasticsearch.dashboards,toDelete); } else { - notice('warning', 'Dashboard Not Found', 'Could not find '+id+' in Elasticsearch'); + alertSrv.set('Dashboard Not Found','Could not find '+id+' in Elasticsearch','warning',5000); } } else { - notice('error', 'Dashboard Not Deleted', 'An error occurred deleting the dashboard'); + alertSrv.set('Dashboard Not Deleted','An error occurred deleting the dashboard','error',5000); } } ); @@ -158,14 +137,10 @@ angular.module('kibana.dashcontrol', []) function(link) { if(!_.isUndefined(link)) { $scope.gist.last = link; - notice( - 'success', - 'Gist saved', - 'You will be able to access your exported dashboard file at '+ - ''+link+' in a moment' - ); + alertSrv.set('Gist saved','You will be able to access your exported dashboard file at '+ + ''+link+' in a moment','success'); } else { - notice('error', 'Save failed', 'Gist could not be saved'); + alertSrv.set('Save failed','Gist could not be saved','error',5000); } }); }; @@ -176,7 +151,7 @@ angular.module('kibana.dashcontrol', []) if(files && files.length > 0) { $scope.gist.files = files; } else { - notice('error', 'Gist Failed', 'Could not retrieve dashboard list from gist',5000); + alertSrv.set('Gist Failed','Could not retrieve dashboard list from gist','error',5000); } }); }; @@ -208,7 +183,7 @@ angular.module('kibana.dashcontrol', []) // Something document.getElementById('dashupload').addEventListener('change', file_selected, false); } else { - alertSrv.set('Oops', 'Sorry, the HTML5 File APIs are not fully supported in this browser.', 'error', 5000); + alertSrv.set('Oops','Sorry, the HTML5 File APIs are not fully supported in this browser.','error'); } } }; From 9ee5618335f9a2f7007876774b6d24a50533880c Mon Sep 17 00:00:00 2001 From: Rashid Khan Date: Tue, 20 Aug 2013 16:10:09 -0700 Subject: [PATCH 06/23] Removed useless placement, added pointer class to field list --- panels/table/micropanel.html | 2 +- panels/table/module.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/panels/table/micropanel.html b/panels/table/micropanel.html index 1ad11ddd78f..b4a000a6c5b 100644 --- a/panels/table/micropanel.html +++ b/panels/table/micropanel.html @@ -31,6 +31,6 @@
-
+
{{field}} ({{Math.round((count / micropanel.count) * 100)}}%), \ No newline at end of file diff --git a/panels/table/module.html b/panels/table/module.html index eb8e9e04e56..564cbc17008 100644 --- a/panels/table/module.html +++ b/panels/table/module.html @@ -18,7 +18,7 @@ From c5bc350d4f89490bafa7ca29124bc15435ff598d Mon Sep 17 00:00:00 2001 From: Rashid Khan Date: Wed, 21 Aug 2013 09:53:12 -0700 Subject: [PATCH 07/23] Adding panel should have all options --- partials/paneladd.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/partials/paneladd.html b/partials/paneladd.html index b41f253c3d1..067ae1ae467 100644 --- a/partials/paneladd.html +++ b/partials/paneladd.html @@ -6,7 +6,7 @@
-
+

{{tab.title}}

\ No newline at end of file From 809b0a34e7e493be01736f2d74b2e9341a9bdc2c Mon Sep 17 00:00:00 2001 From: Rashid Khan Date: Wed, 21 Aug 2013 15:56:20 -0700 Subject: [PATCH 08/23] Improved error handling via alertSrv service --- index.html | 4 +- js/controllers.js | 27 ++----------- js/services.js | 75 +++++++++++++++++++++++++++++++----- panels/dashcontrol/module.js | 30 +++++++-------- partials/dasheditor.html | 10 +++-- 5 files changed, 93 insertions(+), 53 deletions(-) diff --git a/index.html b/index.html index 93498a2c5c4..e0831bebedd 100644 --- a/index.html +++ b/index.html @@ -32,8 +32,8 @@ -
- +
+ {{alert.title}}
{{$index + 1}} alert(s)
-
+
Timestamping
-
+
Index pattern Absolutes in []
-
+
+
Failover
+ +
+
Default Index If index not found
From 4cdac5e709548888b1b280896ec174443e175e1a Mon Sep 17 00:00:00 2001 From: Rashid Khan Date: Thu, 22 Aug 2013 15:46:38 -0700 Subject: [PATCH 09/23] More error handling, changed es loading to use get instead of search --- js/services.js | 64 ++++++++++++++++++++++++++------------------------ 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/js/services.js b/js/services.js index bd7f98b2eea..0cc4f825fb0 100644 --- a/js/services.js +++ b/js/services.js @@ -30,7 +30,7 @@ angular.module('kibana.services', []) this.clearAll = function() { self.list = []; - }; + }; }) .service('fields', function(dashboard, $rootScope, $http, alertSrv) { @@ -53,6 +53,7 @@ angular.module('kibana.services', []) self.map(indices).then(function(result) { self.mapping = _.extend(self.mapping,result); self.list = mapFields(self.mapping); + console.log(self.mapping); }); // Otherwise just use the cached mapping } else { @@ -116,7 +117,7 @@ angular.module('kibana.services', []) ret[propName] = obj; } } - return ret; + return ret; }; }) @@ -242,11 +243,11 @@ angular.module('kibana.services', []) ids : [], }); - // For convenience - var ejs = ejsResource(config.elasticsearch); + // For convenience + var ejs = ejsResource(config.elasticsearch); var _q = dashboard.current.services.query; - this.colors = [ + this.colors = [ "#7EB26D","#EAB839","#6ED0E0","#EF843C","#E24D42","#1F78C1","#BA43A9","#705DA0", //1 "#508642","#CCA300","#447EBC","#C15C17","#890F02","#0A437C","#6D1F62","#584477", //2 "#B7DBAB","#F4D598","#70DBED","#F9BA8F","#F29191","#82B5D8","#E5A8E2","#AEA2E0", //3 @@ -264,7 +265,7 @@ angular.module('kibana.services', []) _q = dashboard.current.services.query; self.list = dashboard.current.services.query.list; self.ids = dashboard.current.services.query.ids; - + if (self.ids.length === 0) { self.set({}); } @@ -330,7 +331,7 @@ angular.module('kibana.services', []) }; this.idsByMode = function(config) { - switch(config.mode) + switch(config.mode) { case 'all': return self.ids; @@ -370,7 +371,7 @@ angular.module('kibana.services', []) }); // For convenience - var ejs = ejsResource(config.elasticsearch); + var ejs = ejsResource(config.elasticsearch); var _f = dashboard.current.services.filter; // Save a reference to this @@ -390,7 +391,7 @@ angular.module('kibana.services', []) }; - // This is used both for adding filters and modifying them. + // This is used both for adding filters and modifying them. // If an id is passed, the filter at that id is updated this.set = function(filter,id) { _.defaults(filter,{mandate:'must'}); @@ -425,7 +426,7 @@ angular.module('kibana.services', []) var either_bool = ejs.BoolFilter().must(ejs.MatchAllFilter()); _.each(ids,function(id) { if(self.list[id].active) { - switch(self.list[id].mandate) + switch(self.list[id].mandate) { case 'mustNot': bool = bool.mustNot(self.getEjsObj(id)); @@ -563,7 +564,7 @@ angular.module('kibana.services', []) }; // An elasticJS client to use - var ejs = ejsResource(config.elasticsearch); + var ejs = ejsResource(config.elasticsearch); var gist_pattern = /(^\d{5,}$)|(^[a-z0-9]{10,}$)|(gist.github.com(\/*.*)\/[a-z0-9]{5,}\/*$)/; // Store a reference to this @@ -602,8 +603,8 @@ angular.module('kibana.services', []) // No dashboard in the URL } else { - // Check if browser supports localstorage, and if there's a dashboard - if (window.Modernizr.localstorage && + // Check if browser supports localstorage, and if there's a dashboard + if (window.Modernizr.localstorage && !(_.isUndefined(window.localStorage['dashboard'])) && window.localStorage['dashboard'] !== '' ) { @@ -612,11 +613,11 @@ angular.module('kibana.services', []) // No? Ok, grab default.json, its all we have now } else { self.file_load('default.json'); - } + } } }; - // Since the dashboard is responsible for index computation, we can compute and assign the indices + // Since the dashboard is responsible for index computation, we can compute and assign the indices // here before telling the panels to refresh this.refresh = function() { if(self.current.index.interval !== 'none') { @@ -626,7 +627,7 @@ angular.module('kibana.services', []) self.current.index.pattern,self.current.index.interval ).then(function (p) { if(p.length > 0) { - self.indices = p; + self.indices = p; } else { //TODO: Option to not failover if(self.current.failover) { @@ -711,7 +712,7 @@ angular.module('kibana.services', []) return true; } else { return false; - } + } }; this.purge_default = function() { @@ -749,20 +750,22 @@ angular.module('kibana.services', []) }); }; + this.elasticsearch_load = function(type,id) { - var request = ejs.Request().indices(config.kibana_index).types(type); - return request.query( - ejs.IdsQuery(id) - ).doSearch(function(results) { - if(_.isUndefined(results)) { - return false; + return $http({ + url: config.elasticsearch + "/" + config.kibana_index + "/"+type+"/"+id, + method: "GET" + }).error(function(data, status, headers, conf) { + if(status === 0) { + alertSrv.set('Error',"Could not contact Elasticsearch at "+config.elasticsearch+ + ". Please ensure that Elasticsearch is reachable from your system." ,'error'); } else { - self.dash_load(angular.fromJson(results.hits.hits[0]['_source']['dashboard'])); - return true; + alertSrv.set('Error',"Could not find "+id+". If you"+ + " are using a proxy, ensure it is configured correctly",'error'); } - }, - function(data,status) { - alertSrv.set('Error','Could not load '+config.elasticsearch+"/"+config.kibana_index+"/"+type+"/"+id,'error'); + return false; + }).success(function(data, status, headers) { + self.dash_load(angular.fromJson(data['_source']['dashboard'])); }); }; @@ -770,7 +773,7 @@ angular.module('kibana.services', []) // Clone object so we can modify it without influencing the existing obejct var save = _.clone(self.current); var id; - + // Change title on object clone if (type === 'dashboard') { id = save.title = _.isUndefined(title) ? self.current.title : title; @@ -783,10 +786,9 @@ angular.module('kibana.services', []) title: save.title, dashboard: angular.toJson(save) }); - + request = type === 'temp' && ttl ? request.ttl(ttl) : request; - // TOFIX: Implement error handling here return request.doIndex( // Success function(result) { From 1274a718dcdb28170bcf61bb202cfe158a0eaed5 Mon Sep 17 00:00:00 2001 From: Spencer Alger Date: Thu, 22 Aug 2013 13:53:31 -0700 Subject: [PATCH 10/23] created timeSeries service with a ZeroFilled class to manage the rows for the histogram --- js/services.js | 74 +++++++++++++++++ panels/histogram/module.js | 157 +++++++++++++++++++++---------------- 2 files changed, 165 insertions(+), 66 deletions(-) diff --git a/js/services.js b/js/services.js index 0cc4f825fb0..2615002a6b8 100644 --- a/js/services.js +++ b/js/services.js @@ -870,5 +870,79 @@ angular.module('kibana.services', []) return false; }); }; +}) +.service('timeSeries', function () { + /** + * Certain graphs require 0 entries to be specified for them to render + * properly (like the line graph). So with this we will caluclate all of + * the expected time measurements, and fill the missing ones in with 0 + * @param date start The start time for the result set + * @param date end The end time for the result set + * @param integer interval The length between measurements, in es interval + * notation (1m, 30s, 1h, 15d) + */ + var undef; + function dateToSecondsWithBlankMs(date) { + // return the date as millis since epoch, with 0 millis + return Math.floor(date.getTime() / 1000)*1000; + } + function base10Int(val) { + return parseInt(val, 10); + } + this.ZeroFilled = function (interval, start, end) { + // the expected differenece between readings. + this.interval_ms = parseInt(kbn.interval_to_seconds(interval), 10) * 1000; + // will keep all values here, keyed by their time + this._data = {}; + if (start) { + this.addValue(start, null); + } + if (end) { + this.addValue(end, null); + } + } + /** + * Add a row + * @param int time The time for the value, in + * @param any value The value at this time + */ + this.ZeroFilled.prototype.addValue = function (time, value) { + if (time instanceof Date) { + time = dateToSecondsWithBlankMs(time); + } else { + time = parseInt(time, 10); + } + if (!isNaN(time)) { + this._data[time] = (value === undef ? 0 : value); + } + }; + /** + * return the rows in the format: + * [ [time, value], [time, value], ... ] + * @return array + */ + this.ZeroFilled.prototype.getFlotPairs = function () { + // var startTime = performance.now(); + var times = _.map(_.keys(this._data), base10Int).sort() + , result = [] + , i + , next + , expected_next; + for(i = 0; i < times.length; i++) { + result.push([ times[i], this._data[times[i]] ]); + next = times[i + 1]; + expected_next = times[i] + this.interval_ms; + for(; times.length > i && next > expected_next; expected_next+= this.interval_ms) { + /** + * since we don't know how the server will round subsequent segments + * we have to recheck for blanks each time. + */ + // this._data[expected_next] = 0; + result.push([expected_next, 0]); + } + } + // console.log(Math.round((performance.now() - startTime)*100)/100, 'ms to get', result.length, 'pairs'); + return result; + }; }); \ No newline at end of file diff --git a/panels/histogram/module.js b/panels/histogram/module.js index 78630e3d373..83a88566cb1 100644 --- a/panels/histogram/module.js +++ b/panels/histogram/module.js @@ -12,10 +12,10 @@ * interval :: Datapoint interval in elasticsearch date math format (eg 1d, 1w, 1y, 5y) * fill :: Only applies to line charts. Level of area shading from 0-10 * linewidth :: Only applies to line charts. How thick the line should be in pixels - While the editor only exposes 0-10, this can be any numeric value. + While the editor only exposes 0-10, this can be any numeric value. Set to 0 and you'll get something like a scatter plot * timezone :: This isn't totally functional yet. Currently only supports browser and utc. - browser will adjust the x-axis labels to match the timezone of the user's + browser will adjust the x-axis labels to match the timezone of the user's browser * spyable :: Dislay the 'eye' icon that show the last elasticsearch query * zoomlinks :: Show the zoom links? @@ -34,7 +34,7 @@ 'use strict'; angular.module('kibana.histogram', []) -.controller('histogram', function($scope, querySrv, dashboard, filterSrv) { +.controller('histogram', function($scope, querySrv, dashboard, filterSrv, timeSeries) { $scope.panelMeta = { editorTabs : [ @@ -56,7 +56,7 @@ angular.module('kibana.histogram', []) }, value_field : null, auto_int : true, - resolution : 100, + resolution : 100, interval : '5m', fill : 0, linewidth : 3, @@ -85,7 +85,39 @@ angular.module('kibana.histogram', []) }; - $scope.get_data = function(segment,query_id) { + /** + * The time range effecting the panel + * @return {[type]} [description] + */ + $scope.get_time_range = function () { + var range = $scope.range = filterSrv.timeRange('min'); + return range; + } + $scope.get_interval = function () { + var interval = $scope.panel.interval + , range; + if ($scope.panel.auto_int) { + range = $scope.get_time_range() + if (range) { + interval = kbn.secondsToHms( + kbn.calculate_interval(range.from, range.to, $scope.panel.resolution, 0) / 1000 + ); + } + } + $scope.panel.interval = interval || '10m'; + return $scope.panel.interval + } + /** + * Fetch the data for a chunk of a queries results. Multiple segments occur when several indicies + * need to be consulted (like timestamped logstash indicies) + * @param number segment The segment count, (0 based) + * @param number query_id The id of the query, generated on the first run and passed back when + * this call is made recursively for more segments + */ + $scope.get_data = function(segment, query_id) { + if (_.isUndefined(segment)) { + segment = 0 + } delete $scope.panel.error; // Make sure we have everything for the request to complete @@ -94,16 +126,16 @@ angular.module('kibana.histogram', []) } - var _range = $scope.range = filterSrv.timeRange('min'); - + var _range = $scope.get_time_range() + var _interval = $scope.get_interval(_range); + if ($scope.panel.auto_int) { $scope.panel.interval = kbn.secondsToHms( kbn.calculate_interval(_range.from,_range.to,$scope.panel.resolution,0)/1000); } $scope.panelMeta.loading = true; - var _segment = _.isUndefined(segment) ? 0 : segment; - var request = $scope.ejs.Request().indices(dashboard.indices[_segment]); + var request = $scope.ejs.Request().indices(dashboard.indices[segment]); $scope.panel.queries.ids = querySrv.idsByMode($scope.panel.queries); // Build the query @@ -114,7 +146,7 @@ angular.module('kibana.histogram', []) ); var facet = $scope.ejs.DateHistogramFacet(id); - + if($scope.panel.mode === 'count') { facet = facet.field($scope.panel.time_field); } else { @@ -124,7 +156,7 @@ angular.module('kibana.histogram', []) } facet = facet.keyField($scope.panel.time_field).valueField($scope.panel.value_field); } - facet = facet.interval($scope.panel.interval).facetFilter($scope.ejs.QueryFilter(query)); + facet = facet.interval(_interval).facetFilter($scope.ejs.QueryFilter(query)); request = request.facet(facet).size(0); }); @@ -137,12 +169,12 @@ angular.module('kibana.histogram', []) // Populate scope when we have results results.then(function(results) { $scope.panelMeta.loading = false; - if(_segment === 0) { + if(segment === 0) { $scope.hits = 0; $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); @@ -153,49 +185,42 @@ angular.module('kibana.histogram', []) var facetIds = _.map(_.keys(results.facets),function(k){return parseInt(k, 10);}); // Make sure we're still on the same query/queries - if($scope.query_id === query_id && - _.intersection(facetIds,$scope.panel.queries.ids).length === $scope.panel.queries.ids.length - ) { + if($scope.query_id === query_id && _.difference(facetIds, $scope.panel.queries.ids).length === 0) { - var i = 0; - var data, hits; + var i = 0 + , time_series + , hits; _.each($scope.panel.queries.ids, function(id) { - var v = results.facets[id]; - - // Null values at each end of the time range ensure we see entire range - if(_.isUndefined($scope.data[i]) || _segment === 0) { - data = []; - if(filterSrv.idsByType('time').length > 0) { - data = [[_range.from.getTime(), null],[_range.to.getTime(), null]]; - //data = []; - } + var query_results = results.facets[id]; + // we need to initialize the data variable on the first run, + // and when we are working on the first segment of the data. + if(_.isUndefined($scope.data[i]) || segment === 0) { + time_series = new timeSeries.ZeroFilled( + _interval, + // range may be false + _range && _range.from, + _range && _range.to + ); hits = 0; } else { - data = $scope.data[i].data; + time_series = $scope.data[i].time_series; hits = $scope.data[i].hits; } - // Assemble segments - var segment_data = []; - _.each(v.entries, function(v, k) { - segment_data.push([v.time,v[$scope.panel.mode]]); - hits += v.count; // The series level hits counter - $scope.hits += v.count; // Entire dataset level hits counter + // push each entry into the time series, while incrementing counters + _.each(query_results.entries, function(entry) { + time_series.addValue(entry.time, entry[$scope.panel.mode]); + hits += entry.count; // The series level hits counter + $scope.hits += entry.count; // Entire dataset level hits counter }); - data.splice.apply(data,[1,0].concat(segment_data)); // Join histogram data - - // Create the flot series object - var series = { - data: { - info: querySrv.list[id], - data: data, - hits: hits - }, + $scope.data[i] = { + time_series: time_series, + info: querySrv.list[id], + data: time_series.getFlotPairs(), + hits: hits }; - $scope.data[i] = series.data; - i++; }); @@ -203,10 +228,9 @@ angular.module('kibana.histogram', []) $scope.$emit('render'); // If we still have segments left, get them - if(_segment < dashboard.indices.length-1) { - $scope.get_data(_segment+1,query_id); + if(segment < dashboard.indices.length-1) { + $scope.get_data(segment+1,query_id); } - } }); }; @@ -238,7 +262,7 @@ angular.module('kibana.histogram', []) to:moment.utc(_to), field:$scope.panel.time_field }); - + dashboard.refresh(); }; @@ -248,8 +272,8 @@ angular.module('kibana.histogram', []) $scope.inspector = angular.toJson(JSON.parse(request.toString()),true); }; - $scope.set_refresh = function (state) { - $scope.refresh = state; + $scope.set_refresh = function (state) { + $scope.refresh = state; }; $scope.close_edit = function() { @@ -271,7 +295,7 @@ angular.module('kibana.histogram', []) scope.$on('render',function(){ render_panel(); }); - + // Re-render if the window is resized angular.element(window).bind('resize', function(){ render_panel(); @@ -282,7 +306,7 @@ angular.module('kibana.histogram', []) // IE doesn't work without this elem.css({height:scope.panel.height||scope.row.height}); - + // Populate from the query service try { _.each(scope.data,function(series) { @@ -299,21 +323,21 @@ angular.module('kibana.histogram', []) .script("common/lib/panels/jquery.flot.stack.js") .script("common/lib/panels/jquery.flot.selection.js") .script("common/lib/panels/timezone.js"); - + // Populate element. Note that jvectormap appends, does not replace. scripts.wait(function(){ var stack = scope.panel.stack ? true : null; // Populate element - try { + try { var options = { legend: { show: false }, series: { //stackpercent: scope.panel.stack ? scope.panel.percentage : false, stack: scope.panel.percentage ? null : stack, - lines: { - show: scope.panel.lines, - fill: scope.panel.fill/10, + lines: { + show: scope.panel.lines, + fill: scope.panel.fill/10, lineWidth: scope.panel.linewidth, steps: false }, @@ -321,10 +345,10 @@ angular.module('kibana.histogram', []) points: { show: scope.panel.points, fill: 1, fillColor: false, radius: 5}, shadowSize: 1 }, - yaxis: { - show: scope.panel['y-axis'], - min: 0, - max: scope.panel.percentage && scope.panel.stack ? 100 : null, + yaxis: { + show: scope.panel['y-axis'], + min: 0, + max: scope.panel.percentage && scope.panel.stack ? 100 : null, }, xaxis: { timezone: scope.panel.timezone, @@ -366,14 +390,15 @@ angular.module('kibana.histogram', []) if(_int >= 60) { return "%H:%M
%m/%d"; } - + return "%H:%M:%S"; } function tt(x, y, contents) { // If the tool tip already exists, don't recreate it, just update it - var tooltip = $('#pie-tooltip').length ? - $('#pie-tooltip') : $('
'); + var tooltip = $('#pie-tooltip').length + ? $('#pie-tooltip') + : $('
'); tooltip.html(contents).css({ position: 'absolute', @@ -393,7 +418,7 @@ angular.module('kibana.histogram', []) tt(pos.pageX, pos.pageY, "
"+ - item.datapoint[1].toFixed(0) + " @ " + + item.datapoint[1].toFixed(0) + " @ " + moment(item.datapoint[0]).format('MM/DD HH:mm:ss')); } else { $("#pie-tooltip").remove(); From e858d3cb62051caf9fae496133b2b889f84ad27a Mon Sep 17 00:00:00 2001 From: Spencer Alger Date: Thu, 22 Aug 2013 14:58:10 -0700 Subject: [PATCH 11/23] moved the timeSeries service into the histogram module. --- js/services.js | 75 -------------------------------------- panels/histogram/module.js | 71 ++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 75 deletions(-) diff --git a/js/services.js b/js/services.js index 2615002a6b8..eceb1f8ac5e 100644 --- a/js/services.js +++ b/js/services.js @@ -870,79 +870,4 @@ angular.module('kibana.services', []) return false; }); }; -}) -.service('timeSeries', function () { - /** - * Certain graphs require 0 entries to be specified for them to render - * properly (like the line graph). So with this we will caluclate all of - * the expected time measurements, and fill the missing ones in with 0 - * @param date start The start time for the result set - * @param date end The end time for the result set - * @param integer interval The length between measurements, in es interval - * notation (1m, 30s, 1h, 15d) - */ - var undef; - function dateToSecondsWithBlankMs(date) { - // return the date as millis since epoch, with 0 millis - return Math.floor(date.getTime() / 1000)*1000; - } - function base10Int(val) { - return parseInt(val, 10); - } - this.ZeroFilled = function (interval, start, end) { - // the expected differenece between readings. - this.interval_ms = parseInt(kbn.interval_to_seconds(interval), 10) * 1000; - // will keep all values here, keyed by their time - this._data = {}; - - if (start) { - this.addValue(start, null); - } - if (end) { - this.addValue(end, null); - } - } - /** - * Add a row - * @param int time The time for the value, in - * @param any value The value at this time - */ - this.ZeroFilled.prototype.addValue = function (time, value) { - if (time instanceof Date) { - time = dateToSecondsWithBlankMs(time); - } else { - time = parseInt(time, 10); - } - if (!isNaN(time)) { - this._data[time] = (value === undef ? 0 : value); - } - }; - /** - * return the rows in the format: - * [ [time, value], [time, value], ... ] - * @return array - */ - this.ZeroFilled.prototype.getFlotPairs = function () { - // var startTime = performance.now(); - var times = _.map(_.keys(this._data), base10Int).sort() - , result = [] - , i - , next - , expected_next; - for(i = 0; i < times.length; i++) { - result.push([ times[i], this._data[times[i]] ]); - next = times[i + 1]; - expected_next = times[i] + this.interval_ms; - for(; times.length > i && next > expected_next; expected_next+= this.interval_ms) { - /** - * since we don't know how the server will round subsequent segments - * we have to recheck for blanks each time. - */ - // this._data[expected_next] = 0; - result.push([expected_next, 0]); - } - } - // console.log(Math.round((performance.now() - startTime)*100)/100, 'ms to get', result.length, 'pairs'); - return result; - }; }); \ No newline at end of file diff --git a/panels/histogram/module.js b/panels/histogram/module.js index 83a88566cb1..90de0372336 100644 --- a/panels/histogram/module.js +++ b/panels/histogram/module.js @@ -436,4 +436,75 @@ angular.module('kibana.histogram', []) }); } }; +}) +.service('timeSeries', function () { + /** + * Certain graphs require 0 entries to be specified for them to render + * properly (like the line graph). So with this we will caluclate all of + * the expected time measurements, and fill the missing ones in with 0 + * @param date start The start time for the result set + * @param date end The end time for the result set + * @param integer interval The length between measurements, in es interval + * notation (1m, 30s, 1h, 15d) + */ + var undef; + function base10Int(val) { + return parseInt(val, 10); + } + this.ZeroFilled = function (interval, start, end) { + // the expected differenece between readings. + this.interval_ms = base10Int(kbn.interval_to_seconds(interval)) * 1000; + // will keep all values here, keyed by their time + this._data = {}; + + if (start) { + this.addValue(start, null); + } + if (end) { + this.addValue(end, null); + } + } + /** + * Add a row + * @param int time The time for the value, in + * @param any value The value at this time + */ + this.ZeroFilled.prototype.addValue = function (time, value) { + if (time instanceof Date) { + time = Math.floor(time.getTime() / 1000)*1000; + } else { + time = base10Int(time); + } + if (!isNaN(time)) { + this._data[time] = (value === undef ? 0 : value); + } + }; + /** + * return the rows in the format: + * [ [time, value], [time, value], ... ] + * @return array + */ + this.ZeroFilled.prototype.getFlotPairs = function () { + // var startTime = performance.now(); + var times = _.map(_.keys(this._data), base10Int).sort() + , result = [] + , i + , next + , expected_next; + for(i = 0; i < times.length; i++) { + result.push([ times[i], this._data[times[i]] ]); + next = times[i + 1]; + expected_next = times[i] + this.interval_ms; + for(; times.length > i && next > expected_next; expected_next+= this.interval_ms) { + /** + * since we don't know how the server will round subsequent segments + * we have to recheck for blanks each time. + */ + // this._data[expected_next] = 0; + result.push([expected_next, 0]); + } + } + // console.log(Math.round((performance.now() - startTime)*100)/100, 'ms to get', result.length, 'pairs'); + return result; + }; }); \ No newline at end of file From 206e634c24268192b3b8e477430c69e5f2fcf5f0 Mon Sep 17 00:00:00 2001 From: Rashid Khan Date: Thu, 22 Aug 2013 15:47:31 -0700 Subject: [PATCH 12/23] spacing --- panels/dashcontrol/module.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panels/dashcontrol/module.js b/panels/dashcontrol/module.js index b7b9957ea0f..1119943b34b 100644 --- a/panels/dashcontrol/module.js +++ b/panels/dashcontrol/module.js @@ -88,7 +88,7 @@ angular.module('kibana.dashcontrol', []) type, ($scope.elasticsearch.title || dashboard.current.title), ($scope.panel.ttl_enable ? ttl : false) - ).then( + ).then( function(result) { if(!_.isUndefined(result._id)) { alertSrv.set('Dashboard Saved','This dashboard has been saved to Elasticsearch as "' + From 7a11d35f8f393ee010586f498582374d663b93d1 Mon Sep 17 00:00:00 2001 From: Rashid Khan Date: Fri, 23 Aug 2013 07:37:33 -0700 Subject: [PATCH 13/23] Brought code back into line with existing jshint --- panels/histogram/module.js | 39 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/panels/histogram/module.js b/panels/histogram/module.js index 90de0372336..d5621dea871 100644 --- a/panels/histogram/module.js +++ b/panels/histogram/module.js @@ -92,12 +92,13 @@ angular.module('kibana.histogram', []) $scope.get_time_range = function () { var range = $scope.range = filterSrv.timeRange('min'); return range; - } + }; + $scope.get_interval = function () { - var interval = $scope.panel.interval - , range; + var interval = $scope.panel.interval, + range; if ($scope.panel.auto_int) { - range = $scope.get_time_range() + range = $scope.get_time_range(); if (range) { interval = kbn.secondsToHms( kbn.calculate_interval(range.from, range.to, $scope.panel.resolution, 0) / 1000 @@ -105,8 +106,8 @@ angular.module('kibana.histogram', []) } } $scope.panel.interval = interval || '10m'; - return $scope.panel.interval - } + return $scope.panel.interval; + }; /** * Fetch the data for a chunk of a queries results. Multiple segments occur when several indicies * need to be consulted (like timestamped logstash indicies) @@ -116,7 +117,7 @@ angular.module('kibana.histogram', []) */ $scope.get_data = function(segment, query_id) { if (_.isUndefined(segment)) { - segment = 0 + segment = 0; } delete $scope.panel.error; @@ -126,7 +127,7 @@ angular.module('kibana.histogram', []) } - var _range = $scope.get_time_range() + var _range = $scope.get_time_range(); var _interval = $scope.get_interval(_range); if ($scope.panel.auto_int) { @@ -187,9 +188,9 @@ angular.module('kibana.histogram', []) // Make sure we're still on the same query/queries if($scope.query_id === query_id && _.difference(facetIds, $scope.panel.queries.ids).length === 0) { - var i = 0 - , time_series - , hits; + var i = 0, + time_series, + hits; _.each($scope.panel.queries.ids, function(id) { var query_results = results.facets[id]; @@ -396,9 +397,7 @@ angular.module('kibana.histogram', []) function tt(x, y, contents) { // If the tool tip already exists, don't recreate it, just update it - var tooltip = $('#pie-tooltip').length - ? $('#pie-tooltip') - : $('
'); + var tooltip = $('#pie-tooltip').length ? $('#pie-tooltip') : $('
'); tooltip.html(contents).css({ position: 'absolute', @@ -463,7 +462,7 @@ angular.module('kibana.histogram', []) if (end) { this.addValue(end, null); } - } + }; /** * Add a row * @param int time The time for the value, in @@ -486,11 +485,11 @@ angular.module('kibana.histogram', []) */ this.ZeroFilled.prototype.getFlotPairs = function () { // var startTime = performance.now(); - var times = _.map(_.keys(this._data), base10Int).sort() - , result = [] - , i - , next - , expected_next; + var times = _.map(_.keys(this._data), base10Int).sort(), + result = [], + i, + next, + expected_next; for(i = 0; i < times.length; i++) { result.push([ times[i], this._data[times[i]] ]); next = times[i + 1]; From 2d6803cdbd7a0df9efd6713a3f21ab6650c3cb90 Mon Sep 17 00:00:00 2001 From: Rashid Khan Date: Fri, 23 Aug 2013 07:40:09 -0700 Subject: [PATCH 14/23] fixed 0 height bars --- js/services.js | 1 - panels/histogram/module.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/js/services.js b/js/services.js index eceb1f8ac5e..101579b4de7 100644 --- a/js/services.js +++ b/js/services.js @@ -53,7 +53,6 @@ angular.module('kibana.services', []) self.map(indices).then(function(result) { self.mapping = _.extend(self.mapping,result); self.list = mapFields(self.mapping); - console.log(self.mapping); }); // Otherwise just use the cached mapping } else { diff --git a/panels/histogram/module.js b/panels/histogram/module.js index d5621dea871..60b823f8185 100644 --- a/panels/histogram/module.js +++ b/panels/histogram/module.js @@ -342,7 +342,7 @@ angular.module('kibana.histogram', []) lineWidth: scope.panel.linewidth, steps: false }, - bars: { show: scope.panel.bars, fill: 1, barWidth: barwidth/1.8, zero: false }, + bars: { show: scope.panel.bars, fill: 1, lineWidth:0, barWidth: barwidth/1.7, zero: false }, points: { show: scope.panel.points, fill: 1, fillColor: false, radius: 5}, shadowSize: 1 }, From c5867779d16df128db48a5c77dea9ad32775bdd0 Mon Sep 17 00:00:00 2001 From: Rashid Khan Date: Fri, 23 Aug 2013 08:53:27 -0700 Subject: [PATCH 15/23] Adding grunt step to CONTRIBUTING.md --- CONTRIBUTING.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e57fe84b11c..e0a3aa85af9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,10 +7,12 @@ The process for contributing to any of the Elasticsearch repositories is similar 1. Sign the contributor license agreement Please make sure you have signed the [Contributor License Agreement](http://www.elasticsearch.org/contributor-agreement/). We are not asking you to assign copyright to us, but to give us the right to distribute your code without restriction. We ask this of all contributors in order to assure our users of the origin and continuing existence of the code. You only need to sign the CLA once. -2. Rebase your changes +2. Run the grunt build process and ensure it completes without errors with your changes. + +3. Rebase your changes Update your local repository with the most recent code from the main Kibana repository, and rebase your branch on top of the latest master branch. We prefer your changes to be squashed into a single commit. -3. Submit a pull request +4. Submit a pull request Push your local changes to your forked copy of the repository and submit a pull request. In the pull request, describe what your changes do and mention the number of the issue where discussion has taken place, eg “Closes #123″. Then sit back and wait. There will probably be discussion about the pull request and, if any changes are needed, we would love to work with you to get your pull request merged into Kibana. From 136bf66d3946d3501276fc035bd60482b4651806 Mon Sep 17 00:00:00 2001 From: Spencer Alger Date: Fri, 23 Aug 2013 09:11:14 -0700 Subject: [PATCH 16/23] Rebased to master to include latest changes updated the getFlotPairs() function to only check before and after a value, adding only enough zeros to maintain line graphs. --- panels/histogram/module.js | 64 +++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/panels/histogram/module.js b/panels/histogram/module.js index 60b823f8185..5593e6d9e90 100644 --- a/panels/histogram/module.js +++ b/panels/histogram/module.js @@ -95,7 +95,7 @@ angular.module('kibana.histogram', []) }; $scope.get_interval = function () { - var interval = $scope.panel.interval, + var interval = $scope.panel.interval, range; if ($scope.panel.auto_int) { range = $scope.get_time_range(); @@ -188,8 +188,8 @@ angular.module('kibana.histogram', []) // Make sure we're still on the same query/queries if($scope.query_id === query_id && _.difference(facetIds, $scope.panel.queries.ids).length === 0) { - var i = 0, - time_series, + var i = 0, + time_series, hits; _.each($scope.panel.queries.ids, function(id) { @@ -342,8 +342,19 @@ angular.module('kibana.histogram', []) lineWidth: scope.panel.linewidth, steps: false }, - bars: { show: scope.panel.bars, fill: 1, lineWidth:0, barWidth: barwidth/1.7, zero: false }, - points: { show: scope.panel.points, fill: 1, fillColor: false, radius: 5}, + bars: { + show: scope.panel.bars, + fill: 1, + barWidth: barwidth/1.8, + zero: false, + lineWidth: 0 + }, + points: { + show: scope.panel.points, + fill: 1, + fillColor: false, + radius: 5 + }, shadowSize: 1 }, yaxis: { @@ -485,24 +496,33 @@ angular.module('kibana.histogram', []) */ this.ZeroFilled.prototype.getFlotPairs = function () { // var startTime = performance.now(); - var times = _.map(_.keys(this._data), base10Int).sort(), - result = [], - i, - next, - expected_next; - for(i = 0; i < times.length; i++) { - result.push([ times[i], this._data[times[i]] ]); - next = times[i + 1]; - expected_next = times[i] + this.interval_ms; - for(; times.length > i && next > expected_next; expected_next+= this.interval_ms) { - /** - * since we don't know how the server will round subsequent segments - * we have to recheck for blanks each time. - */ - // this._data[expected_next] = 0; - result.push([expected_next, 0]); + var times = _.map(_.keys(this._data), base10Int).sort(), + result = []; + _.each(times, function (time, i, times) { + var next, expected_next, prev, expected_prev; + + // check for previous measurement + if (i > 0) { + prev = times[i - 1]; + expected_prev = time - this.interval_ms; + if (prev < expected_prev) { + result.push([expected_prev, 0]); + } } - } + + // add the current time + result.push([ time, this._data[time] ]); + + // check for next measurement + if (times.length > i) { + next = times[i + 1]; + expected_next = time + this.interval_ms; + if (next > expected_next) { + result.push([expected_next, 0]); + } + } + + }, this); // console.log(Math.round((performance.now() - startTime)*100)/100, 'ms to get', result.length, 'pairs'); return result; }; From 5dd1c556274041363a294a0ec0441b46e0d8fdb7 Mon Sep 17 00:00:00 2001 From: Rashid Khan Date: Mon, 26 Aug 2013 10:21:28 -0700 Subject: [PATCH 17/23] Fixed subsecond intervals in interval_to_seconds --- js/shared.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/shared.js b/js/shared.js index 217289ba09b..94c09d3f30d 100644 --- a/js/shared.js +++ b/js/shared.js @@ -192,7 +192,7 @@ // histogram & trends kbn.interval_to_seconds = function(string) { - var matches = string.match(/(\d+)([Mwdhmsy])/); + var matches = string.match(/(\d+(?:\.\d+)?)([Mwdhmsy])/); switch (matches[2]) { case 'y': return matches[1]*31536000; From 0a4f6d0dadb78658bb2a50f7e0502364718a5995 Mon Sep 17 00:00:00 2001 From: Spencer Alger Date: Mon, 26 Aug 2013 08:51:51 -0700 Subject: [PATCH 18/23] added check for 404 response from the index_save call, assumes that auto_create_index is disabled and attempts to create the index before saving a second time --- js/services.js | 4 +++ panels/dashcontrol/module.js | 59 +++++++++++++++++++++++++----------- 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/js/services.js b/js/services.js index 101579b4de7..4ad364f744c 100644 --- a/js/services.js +++ b/js/services.js @@ -800,6 +800,10 @@ angular.module('kibana.services', []) ); }; + this.elasticsearch_create_index = function () { + return ejs.client.post('/'+config.kibana_index); + }; + this.elasticsearch_delete = function(id) { return ejs.Document(config.kibana_index,'dashboard',id).doDelete( // Success diff --git a/panels/dashcontrol/module.js b/panels/dashcontrol/module.js index 1119943b34b..015898e048a 100644 --- a/panels/dashcontrol/module.js +++ b/panels/dashcontrol/module.js @@ -17,7 +17,7 @@ * hide_control :: Upon save, hide this panel * elasticsearch_size :: show this many dashboards under the ES section in the load drop down * temp :: Allow saving of temp dashboards - * ttl :: Enable setting ttl. + * ttl :: Enable setting ttl. * temp_ttl :: How long should temp dashboards persist */ @@ -61,6 +61,10 @@ angular.module('kibana.dashcontrol', []) services: {} }; + function notice(type, title, message) { + alertSrv.set(title, message, type, 5000); + } + $scope.init = function() { $scope.gist_pattern = /(^\d{5,}$)|(^[a-z0-9]{10,}$)|(gist.github.com(\/*.*)\/[a-z0-9]{5,}\/*$)/; $scope.gist = {}; @@ -69,17 +73,17 @@ angular.module('kibana.dashcontrol', []) $scope.set_default = function() { if(dashboard.set_default()) { - alertSrv.set('Local Default Set',dashboard.current.title+' has been set as your local default','success',5000); + notice('success', 'Local Default Set', dashboard.current.title+' has been set as your local default'); } else { - alertSrv.set('Incompatible Browser','Sorry, your browser is too old for this feature','error',5000); + notice('error', 'Incompatible Browser', 'Sorry, your browser is too old for this feature'); } }; $scope.purge_default = function() { if(dashboard.purge_default()) { - alertSrv.set('Local Default Clear','Your local default dashboard has been cleared','success',5000); + notice('success', 'Local Default Clear', 'Your local default dashboard has been cleared'); } else { - alertSrv.set('Incompatible Browser','Sorry, your browser is too old for this feature','error',5000); + notice('error', 'Incompatible Browser', 'Sorry, your browser is too old for this feature'); } }; @@ -91,13 +95,30 @@ angular.module('kibana.dashcontrol', []) ).then( function(result) { if(!_.isUndefined(result._id)) { - alertSrv.set('Dashboard Saved','This dashboard has been saved to Elasticsearch as "' + - result._id + '"','success',5000); + notice( + 'success', + 'Dashboard Saved', + 'This dashboard has been saved to Elasticsearch as "'+result._id + '"' + ); if(type === 'temp') { - $scope.share = dashboard.share_link(dashboard.current.title,'temp',result._id); + $scope.share = dashboard.share_link(dashboard.current.title, 'temp', result._id); } } else { - alertSrv.set('Save failed','Dashboard could not be saved to Elasticsearch','error',5000); + if (result.status === 404) { + // auto create must be disabled and the index doesn't exist, create it! + return dashboard.elasticsearch_create_index().then(function () { + return $scope.elasticsearch_save(type, ttl); + }, function () { + notice( + 'error', + 'Save failed', + 'Dashboard could not be saved because the "'+config.kibana_index+'" '+ + 'index does not exist and could not be created.' + ); + }); + } else { + notice('error', 'Save failed', 'Dashboard could not be saved to Elasticsearch'); + } } }); }; @@ -107,15 +128,15 @@ angular.module('kibana.dashcontrol', []) function(result) { if(!_.isUndefined(result)) { if(result.found) { - alertSrv.set('Dashboard Deleted',id+' has been deleted','success',5000); + notice('success', 'Dashboard Deleted', id+' has been deleted'); // Find the deleted dashboard in the cached list and remove it var toDelete = _.where($scope.elasticsearch.dashboards,{_id:id})[0]; $scope.elasticsearch.dashboards = _.without($scope.elasticsearch.dashboards,toDelete); } else { - alertSrv.set('Dashboard Not Found','Could not find '+id+' in Elasticsearch','warning',5000); + notice('warning', 'Dashboard Not Found', 'Could not find '+id+' in Elasticsearch'); } } else { - alertSrv.set('Dashboard Not Deleted','An error occurred deleting the dashboard','error',5000); + notice('error', 'Dashboard Not Deleted', 'An error occurred deleting the dashboard'); } } ); @@ -137,10 +158,14 @@ angular.module('kibana.dashcontrol', []) function(link) { if(!_.isUndefined(link)) { $scope.gist.last = link; - alertSrv.set('Gist saved','You will be able to access your exported dashboard file at '+ - ''+link+' in a moment','success'); + notice( + 'success', + 'Gist saved', + 'You will be able to access your exported dashboard file at '+ + ''+link+' in a moment' + ); } else { - alertSrv.set('Save failed','Gist could not be saved','error',5000); + notice('error', 'Save failed', 'Gist could not be saved'); } }); }; @@ -151,7 +176,7 @@ angular.module('kibana.dashcontrol', []) if(files && files.length > 0) { $scope.gist.files = files; } else { - alertSrv.set('Gist Failed','Could not retrieve dashboard list from gist','error',5000); + notice('error', 'Gist Failed', 'Could not retrieve dashboard list from gist',5000); } }); }; @@ -183,7 +208,7 @@ angular.module('kibana.dashcontrol', []) // Something document.getElementById('dashupload').addEventListener('change', file_selected, false); } else { - alertSrv.set('Oops','Sorry, the HTML5 File APIs are not fully supported in this browser.','error'); + alertSrv.set('Oops', 'Sorry, the HTML5 File APIs are not fully supported in this browser.', 'error', 5000); } } }; From d891d00fa42448e5ac3799155fd2c9348c50c64e Mon Sep 17 00:00:00 2001 From: Spencer Alger Date: Fri, 23 Aug 2013 13:20:31 -0700 Subject: [PATCH 19/23] Restored the "fill all the holes" strategy to the ZeroFill class, and introduced the fill_style option ("minimal" and "all" are possible values). Also allowed a set of required times to be specified when asking the times series for it's data. This way, we can ensure in the stacked bar chart that each data point in use has a value preventing the bars from stacking incorrectly. --- panels/histogram/module.js | 188 +++++++++++++++++++++++++++---------- 1 file changed, 137 insertions(+), 51 deletions(-) diff --git a/panels/histogram/module.js b/panels/histogram/module.js index 5593e6d9e90..ee184fdc3e5 100644 --- a/panels/histogram/module.js +++ b/panels/histogram/module.js @@ -197,12 +197,12 @@ angular.module('kibana.histogram', []) // we need to initialize the data variable on the first run, // and when we are working on the first segment of the data. if(_.isUndefined($scope.data[i]) || segment === 0) { - time_series = new timeSeries.ZeroFilled( - _interval, - // range may be false - _range && _range.from, - _range && _range.to - ); + time_series = new timeSeries.ZeroFilled({ + interval: _interval, + start_date: _range && _range.from, + end_date: _range && _range.to, + fillStyle: 'minimal' + }); hits = 0; } else { time_series = $scope.data[i].time_series; @@ -216,9 +216,8 @@ angular.module('kibana.histogram', []) $scope.hits += entry.count; // Entire dataset level hits counter }); $scope.data[i] = { - time_series: time_series, info: querySrv.list[id], - data: time_series.getFlotPairs(), + time_series: time_series, hits: hits }; @@ -310,7 +309,7 @@ angular.module('kibana.histogram', []) // Populate from the query service try { - _.each(scope.data,function(series) { + _.each(scope.data, function(series) { series.label = series.info.alias; series.color = series.info.color; }); @@ -383,6 +382,19 @@ angular.module('kibana.histogram', []) options.selection = { mode: "x", color: '#666' }; } + // when rendering stacked bars, we need to ensure each point that has data is zero-filled + // so that the stacking happens in the proper order + var required_times = []; + if (scope.panel.bars && stack) { + required_times = Array.prototype.concat.apply([], _.map(scope.data, function (series) { + return series.time_series.getOrderedTimes(); + })); + } + + for (var i = 0; i < scope.data.length; i++) { + scope.data[i].data = scope.data[i].time_series.getFlotPairs(required_times); + } + scope.plot = $.plot(elem, scope.data, options); } catch(e) { @@ -448,36 +460,53 @@ angular.module('kibana.histogram', []) }; }) .service('timeSeries', function () { + // map compatable parseInt + function base10Int(val) { + return parseInt(val, 10); + } + /** * Certain graphs require 0 entries to be specified for them to render * properly (like the line graph). So with this we will caluclate all of * the expected time measurements, and fill the missing ones in with 0 - * @param date start The start time for the result set - * @param date end The end time for the result set - * @param integer interval The length between measurements, in es interval - * notation (1m, 30s, 1h, 15d) + * @param object opts An object specifying some/all of the options + * + * OPTIONS: + * @opt string interval The interval notion describing the expected spacing between + * each data point. + * @opt date start_date (optional) The start point for the time series, setting this and the + * end_date will ensure that the series streches to resemble the entire + * expected result + * @opt date end_date (optional) The end point for the time series, see start_date + * @opt string fill_style Either "minimal", or "all" describing the strategy used to zero-fill + * the series. */ - var undef; - function base10Int(val) { - return parseInt(val, 10); - } - this.ZeroFilled = function (interval, start, end) { + this.ZeroFilled = function (opts) { + this.opts = _.defaults(opts, { + interval: '10m', + start_date: null, + end_date: null, + fill_style: 'minimal' + }); + // the expected differenece between readings. - this.interval_ms = base10Int(kbn.interval_to_seconds(interval)) * 1000; + this.interval_ms = base10Int(kbn.interval_to_seconds(opts.interval)) * 1000; + // will keep all values here, keyed by their time this._data = {}; - if (start) { - this.addValue(start, null); + if (opts.start_date) { + this.addValue(opts.start_date, null); } - if (end) { - this.addValue(end, null); + if (opts.end_date) { + this.addValue(opts.end_date, null); } }; + /** * Add a row - * @param int time The time for the value, in - * @param any value The value at this time + * @param {int} time The time for the value, in + * @param {any} value The value at this time */ this.ZeroFilled.prototype.addValue = function (time, value) { if (time instanceof Date) { @@ -486,44 +515,101 @@ angular.module('kibana.histogram', []) time = base10Int(time); } if (!isNaN(time)) { - this._data[time] = (value === undef ? 0 : value); + this._data[time] = (_.isUndefined(value) ? 0 : value); } + this._cached_times = null; }; + + /** + * Get an array of the times that have been explicitly set in the series + * @param {array} include (optional) list of timestamps to include in the response + * @return {array} An array of integer times. + */ + this.ZeroFilled.prototype.getOrderedTimes = function (include) { + var times = _.map(_.keys(this._data), base10Int).sort(); + if (_.isArray(include)) { + times = times.concat(include); + } + return times; + }; + /** * return the rows in the format: * [ [time, value], [time, value], ... ] - * @return array + * + * Heavy lifting is done by _get(Min|All)FlotPairs() + * @param {array} required_times An array of timestamps that must be in the resulting pairs + * @return {array} */ - this.ZeroFilled.prototype.getFlotPairs = function () { - // var startTime = performance.now(); - var times = _.map(_.keys(this._data), base10Int).sort(), - result = []; - _.each(times, function (time, i, times) { - var next, expected_next, prev, expected_prev; + this.ZeroFilled.prototype.getFlotPairs = function (required_times) { + var times = this.getOrderedTimes(required_times), + strategy, + pairs; - // check for previous measurement - if (i > 0) { - prev = times[i - 1]; - expected_prev = time - this.interval_ms; - if (prev < expected_prev) { - result.push([expected_prev, 0]); - } + if(this.opts.fill_style === 'all') { + strategy = this._getAllFlotPairs; + } else { + strategy = this._getMinFlotPairs; + } + + return _.reduce( + times, // what + strategy, // how + [], // where + this // context + ); + }; + + /** + * ** called as a reduce stragegy in getFlotPairs() ** + * Fill zero's on either side of the current time, unless there is already a measurement there or + * we are looking at an edge. + * @return {array} An array of points to plot with flot + */ + this.ZeroFilled.prototype._getMinFlotPairs = function (result, time, i, times) { + var next, expected_next, prev, expected_prev; + + // check for previous measurement + if (i > 0) { + prev = times[i - 1]; + expected_prev = time - this.interval_ms; + if (prev < expected_prev) { + result.push([expected_prev, 0]); } + } - // add the current time - result.push([ time, this._data[time] ]); + // add the current time + result.push([ time, this._data[time] || 0 ]); - // check for next measurement - if (times.length > i) { - next = times[i + 1]; - expected_next = time + this.interval_ms; - if (next > expected_next) { - result.push([expected_next, 0]); - } + // check for next measurement + if (times.length > i) { + next = times[i + 1]; + expected_next = time + this.interval_ms; + if (next > expected_next) { + result.push([expected_next, 0]); } + } - }, this); - // console.log(Math.round((performance.now() - startTime)*100)/100, 'ms to get', result.length, 'pairs'); return result; }; + + /** + * ** called as a reduce stragegy in getFlotPairs() ** + * Fill zero's to the right of each time, until the next measurement is reached or we are at the + * last measurement + * @return {array} An array of points to plot with flot + */ + this.ZeroFilled.prototype._getAllFlotPairs = function (result, time, i, times) { + var next, expected_next; + + result.push([ times[i], this._data[times[i]] || 0 ]); + next = times[i + 1]; + expected_next = times[i] + this.interval_ms; + for(; times.length > i && next > expected_next; expected_next+= this.interval_ms) { + result.push([expected_next, 0]); + } + + return result; + }; + }); \ No newline at end of file From 1b87ce387d9d8b91c9e1ab73e5e3a93135b92239 Mon Sep 17 00:00:00 2001 From: Spencer Alger Date: Fri, 23 Aug 2013 14:31:59 -0700 Subject: [PATCH 20/23] cleaned up the documentation a bit --- panels/histogram/module.js | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/panels/histogram/module.js b/panels/histogram/module.js index ee184fdc3e5..ed1eec0fcbc 100644 --- a/panels/histogram/module.js +++ b/panels/histogram/module.js @@ -108,11 +108,17 @@ angular.module('kibana.histogram', []) $scope.panel.interval = interval || '10m'; return $scope.panel.interval; }; + /** * Fetch the data for a chunk of a queries results. Multiple segments occur when several indicies * need to be consulted (like timestamped logstash indicies) - * @param number segment The segment count, (0 based) - * @param number query_id The id of the query, generated on the first run and passed back when + * + * The results of this function are stored on the scope's data property. This property will be an + * array of objects with the properties info, time_series, and hits. These objects are used in the + * render_panel function to create the historgram. + * + * @param {number} segment The segment count, (0 based) + * @param {number} query_id The id of the query, generated on the first run and passed back when * this call is made recursively for more segments */ $scope.get_data = function(segment, query_id) { @@ -469,17 +475,17 @@ angular.module('kibana.histogram', []) * Certain graphs require 0 entries to be specified for them to render * properly (like the line graph). So with this we will caluclate all of * the expected time measurements, and fill the missing ones in with 0 - * @param object opts An object specifying some/all of the options + * @param {object} opts An object specifying some/all of the options * * OPTIONS: - * @opt string interval The interval notion describing the expected spacing between - * each data point. - * @opt date start_date (optional) The start point for the time series, setting this and the - * end_date will ensure that the series streches to resemble the entire - * expected result - * @opt date end_date (optional) The end point for the time series, see start_date - * @opt string fill_style Either "minimal", or "all" describing the strategy used to zero-fill - * the series. + * @opt {string} interval The interval notion describing the expected spacing between + * each data point. + * @opt {date} start_date (optional) The start point for the time series, setting this and the + * end_date will ensure that the series streches to resemble the entire + * expected result + * @opt {date} end_date (optional) The end point for the time series, see start_date + * @opt {string} fill_style Either "minimal", or "all" describing the strategy used to zero-fill + * the series. */ this.ZeroFilled = function (opts) { this.opts = _.defaults(opts, { @@ -522,7 +528,7 @@ angular.module('kibana.histogram', []) /** * Get an array of the times that have been explicitly set in the series - * @param {array} include (optional) list of timestamps to include in the response + * @param {array} include (optional) list of timestamps to include in the response * @return {array} An array of integer times. */ this.ZeroFilled.prototype.getOrderedTimes = function (include) { @@ -538,7 +544,7 @@ angular.module('kibana.histogram', []) * [ [time, value], [time, value], ... ] * * Heavy lifting is done by _get(Min|All)FlotPairs() - * @param {array} required_times An array of timestamps that must be in the resulting pairs + * @param {array} required_times An array of timestamps that must be in the resulting pairs * @return {array} */ this.ZeroFilled.prototype.getFlotPairs = function (required_times) { From 0ab6e73f06a84ffd90d26c1c94d5287debfa2fd4 Mon Sep 17 00:00:00 2001 From: Spencer Alger Date: Sat, 24 Aug 2013 14:24:54 -0700 Subject: [PATCH 21/23] fixed a property typo --- panels/histogram/module.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panels/histogram/module.js b/panels/histogram/module.js index ed1eec0fcbc..fe7de099773 100644 --- a/panels/histogram/module.js +++ b/panels/histogram/module.js @@ -207,7 +207,7 @@ angular.module('kibana.histogram', []) interval: _interval, start_date: _range && _range.from, end_date: _range && _range.to, - fillStyle: 'minimal' + fill_style: 'minimal' }); hits = 0; } else { From 71cf9dfa8c5a268b339b26364a043b0d58071113 Mon Sep 17 00:00:00 2001 From: Rashid Khan Date: Mon, 26 Aug 2013 13:38:44 -0700 Subject: [PATCH 22/23] Revert "added check for 404 response from the index_save call, assumes that auto_create_index is disabled and attempts to create the index before saving a second time" This reverts commit 556925f840f4680a3be61db51577ceada022d4f3. --- js/services.js | 4 --- panels/dashcontrol/module.js | 59 +++++++++++------------------------- 2 files changed, 17 insertions(+), 46 deletions(-) diff --git a/js/services.js b/js/services.js index 4ad364f744c..101579b4de7 100644 --- a/js/services.js +++ b/js/services.js @@ -800,10 +800,6 @@ angular.module('kibana.services', []) ); }; - this.elasticsearch_create_index = function () { - return ejs.client.post('/'+config.kibana_index); - }; - this.elasticsearch_delete = function(id) { return ejs.Document(config.kibana_index,'dashboard',id).doDelete( // Success diff --git a/panels/dashcontrol/module.js b/panels/dashcontrol/module.js index 015898e048a..1119943b34b 100644 --- a/panels/dashcontrol/module.js +++ b/panels/dashcontrol/module.js @@ -17,7 +17,7 @@ * hide_control :: Upon save, hide this panel * elasticsearch_size :: show this many dashboards under the ES section in the load drop down * temp :: Allow saving of temp dashboards - * ttl :: Enable setting ttl. + * ttl :: Enable setting ttl. * temp_ttl :: How long should temp dashboards persist */ @@ -61,10 +61,6 @@ angular.module('kibana.dashcontrol', []) services: {} }; - function notice(type, title, message) { - alertSrv.set(title, message, type, 5000); - } - $scope.init = function() { $scope.gist_pattern = /(^\d{5,}$)|(^[a-z0-9]{10,}$)|(gist.github.com(\/*.*)\/[a-z0-9]{5,}\/*$)/; $scope.gist = {}; @@ -73,17 +69,17 @@ angular.module('kibana.dashcontrol', []) $scope.set_default = function() { if(dashboard.set_default()) { - notice('success', 'Local Default Set', dashboard.current.title+' has been set as your local default'); + alertSrv.set('Local Default Set',dashboard.current.title+' has been set as your local default','success',5000); } else { - notice('error', 'Incompatible Browser', 'Sorry, your browser is too old for this feature'); + alertSrv.set('Incompatible Browser','Sorry, your browser is too old for this feature','error',5000); } }; $scope.purge_default = function() { if(dashboard.purge_default()) { - notice('success', 'Local Default Clear', 'Your local default dashboard has been cleared'); + alertSrv.set('Local Default Clear','Your local default dashboard has been cleared','success',5000); } else { - notice('error', 'Incompatible Browser', 'Sorry, your browser is too old for this feature'); + alertSrv.set('Incompatible Browser','Sorry, your browser is too old for this feature','error',5000); } }; @@ -95,30 +91,13 @@ angular.module('kibana.dashcontrol', []) ).then( function(result) { if(!_.isUndefined(result._id)) { - notice( - 'success', - 'Dashboard Saved', - 'This dashboard has been saved to Elasticsearch as "'+result._id + '"' - ); + alertSrv.set('Dashboard Saved','This dashboard has been saved to Elasticsearch as "' + + result._id + '"','success',5000); if(type === 'temp') { - $scope.share = dashboard.share_link(dashboard.current.title, 'temp', result._id); + $scope.share = dashboard.share_link(dashboard.current.title,'temp',result._id); } } else { - if (result.status === 404) { - // auto create must be disabled and the index doesn't exist, create it! - return dashboard.elasticsearch_create_index().then(function () { - return $scope.elasticsearch_save(type, ttl); - }, function () { - notice( - 'error', - 'Save failed', - 'Dashboard could not be saved because the "'+config.kibana_index+'" '+ - 'index does not exist and could not be created.' - ); - }); - } else { - notice('error', 'Save failed', 'Dashboard could not be saved to Elasticsearch'); - } + alertSrv.set('Save failed','Dashboard could not be saved to Elasticsearch','error',5000); } }); }; @@ -128,15 +107,15 @@ angular.module('kibana.dashcontrol', []) function(result) { if(!_.isUndefined(result)) { if(result.found) { - notice('success', 'Dashboard Deleted', id+' has been deleted'); + alertSrv.set('Dashboard Deleted',id+' has been deleted','success',5000); // Find the deleted dashboard in the cached list and remove it var toDelete = _.where($scope.elasticsearch.dashboards,{_id:id})[0]; $scope.elasticsearch.dashboards = _.without($scope.elasticsearch.dashboards,toDelete); } else { - notice('warning', 'Dashboard Not Found', 'Could not find '+id+' in Elasticsearch'); + alertSrv.set('Dashboard Not Found','Could not find '+id+' in Elasticsearch','warning',5000); } } else { - notice('error', 'Dashboard Not Deleted', 'An error occurred deleting the dashboard'); + alertSrv.set('Dashboard Not Deleted','An error occurred deleting the dashboard','error',5000); } } ); @@ -158,14 +137,10 @@ angular.module('kibana.dashcontrol', []) function(link) { if(!_.isUndefined(link)) { $scope.gist.last = link; - notice( - 'success', - 'Gist saved', - 'You will be able to access your exported dashboard file at '+ - ''+link+' in a moment' - ); + alertSrv.set('Gist saved','You will be able to access your exported dashboard file at '+ + ''+link+' in a moment','success'); } else { - notice('error', 'Save failed', 'Gist could not be saved'); + alertSrv.set('Save failed','Gist could not be saved','error',5000); } }); }; @@ -176,7 +151,7 @@ angular.module('kibana.dashcontrol', []) if(files && files.length > 0) { $scope.gist.files = files; } else { - notice('error', 'Gist Failed', 'Could not retrieve dashboard list from gist',5000); + alertSrv.set('Gist Failed','Could not retrieve dashboard list from gist','error',5000); } }); }; @@ -208,7 +183,7 @@ angular.module('kibana.dashcontrol', []) // Something document.getElementById('dashupload').addEventListener('change', file_selected, false); } else { - alertSrv.set('Oops', 'Sorry, the HTML5 File APIs are not fully supported in this browser.', 'error', 5000); + alertSrv.set('Oops','Sorry, the HTML5 File APIs are not fully supported in this browser.','error'); } } }; From dcdea1d8ccb3d18794031b83ef3ddf79ce5a7ec9 Mon Sep 17 00:00:00 2001 From: Rashid Khan Date: Tue, 27 Aug 2013 10:33:37 -0700 Subject: [PATCH 23/23] Fixing merge conflict --- .gitignore | 1 + .jshintrc | 40 ++++++++++++++++++++++++++++++++++++++ Gruntfile.js | 31 ++--------------------------- panels/histogram/module.js | 12 ++++++------ 4 files changed, 49 insertions(+), 35 deletions(-) create mode 100644 .gitignore create mode 100644 .jshintrc diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000000..b512c09d476 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 00000000000..2092e6445e0 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,40 @@ +{ + "jquery": true, + "browser": true, + + "bitwise":false, + "curly": true, + "eqnull": true, + "globalstrict": true, + "devel": true, + "eqeqeq": true, + "forin": false, + "immed": true, + "supernew": true, + "expr": true, + "indent": 2, + "latedef": true, + "newcap": true, + "noarg": true, + "noempty": true, + "undef": true, + "boss": true, + "trailing": false, + "laxbreak": true, + "laxcomma": true, + "sub": true, + + "maxlen": 140, + + "globals": { + "$LAB": false, + "_": false, + "$": false, + "kbn" : false, + "angular" : false, + "exports": true, + "module": false, + "config": false, + "moment": false + } +} \ No newline at end of file diff --git a/Gruntfile.js b/Gruntfile.js index 4e05a3067bc..1d640ff229b 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -17,34 +17,7 @@ module.exports = function (grunt) { jshint: { files: ['Gruntfile.js', 'js/*.js', 'panels/*/*.js' ], options: { - bitwise: true, - maxlen: 140, - curly: true, - eqeqeq: true, - immed: true, - indent: 2, - latedef: true, - newcap: true, - noarg: true, - sub: true, - undef: true, - boss: true, - eqnull: true, - globalstrict: true, - devel: true, - node: true, - globals: { - '$LAB': false, - '_': false, - '$': false, - 'kbn' : false, - window: false, - document: false, - exports: true, - module: false, - config: false, - moment: false - } + jshintrc: '.jshintrc' } }, less: { @@ -70,4 +43,4 @@ module.exports = function (grunt) { // Default task. grunt.registerTask('default', ['jshint','less']); -}; +}; \ No newline at end of file diff --git a/panels/histogram/module.js b/panels/histogram/module.js index fe7de099773..e8b3a9c71b7 100644 --- a/panels/histogram/module.js +++ b/panels/histogram/module.js @@ -392,9 +392,9 @@ angular.module('kibana.histogram', []) // so that the stacking happens in the proper order var required_times = []; if (scope.panel.bars && stack) { - required_times = Array.prototype.concat.apply([], _.map(scope.data, function (series) { - return series.time_series.getOrderedTimes(); - })); + required_times = _.uniq(Array.prototype.concat.apply([], _.map(scope.data, function (query) { + return query.time_series.getOrderedTimes(); + })).sort(), true); } for (var i = 0; i < scope.data.length; i++) { @@ -532,11 +532,11 @@ angular.module('kibana.histogram', []) * @return {array} An array of integer times. */ this.ZeroFilled.prototype.getOrderedTimes = function (include) { - var times = _.map(_.keys(this._data), base10Int).sort(); + var times = _.map(_.keys(this._data), base10Int); if (_.isArray(include)) { times = times.concat(include); } - return times; + return _.uniq(times.sort(), true); }; /** @@ -618,4 +618,4 @@ angular.module('kibana.histogram', []) return result; }; -}); \ No newline at end of file +});