Rashid Khan 12 лет назад
Родитель
Сommit
71b14e4643
3 измененных файлов с 192 добавлено и 71 удалено
  1. 4 0
      js/services.js
  2. 42 17
      panels/dashcontrol/module.js
  3. 146 54
      panels/histogram/module.js

+ 4 - 0
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

+ 42 - 17
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 '+
-          '<a href="'+link+'">'+link+'</a> in a moment','success');
+        notice(
+          'success',
+          'Gist saved',
+          'You will be able to access your exported dashboard file at '+
+            '<a href="'+link+'">'+link+'</a> 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);
       }
     }
   };

+ 146 - 54
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;
-
-      // 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]);
-        }
+  this.ZeroFilled.prototype.getFlotPairs = function (required_times) {
+    var times = this.getOrderedTimes(required_times),
+      strategy,
+      pairs;
+
+    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;
+  };
+
 });