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);
}
}
};
diff --git a/panels/histogram/module.js b/panels/histogram/module.js
index 5593e6d9e90..fe7de099773 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) {
@@ -197,12 +203,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,
+ fill_style: 'minimal'
+ });
hits = 0;
} else {
time_series = $scope.data[i].time_series;
@@ -216,9 +222,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 +315,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 +388,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 +466,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 +521,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