Просмотр исходного кода

Refactored panels to used dashboard, query and filter services and store them on the dashboard

Rashid Khan 12 лет назад
Родитель
Сommit
0d0d3f4a36

+ 0 - 2
common/css/main.css

@@ -123,8 +123,6 @@
   max-width: 500px;
 }
 
-.popover-title { display: none; }
-
 .tiny {
   font-size: 50%;
 }

+ 2 - 1
js/controllers.js

@@ -3,7 +3,8 @@
 'use strict';
 
 angular.module('kibana.controllers', [])
-.controller('DashCtrl', function($scope, $rootScope, $http, $timeout, $route, ejsResource, eventBus, fields, dashboard) {
+.controller('DashCtrl', function($scope, $rootScope, $http, $timeout, $route, ejsResource, eventBus, 
+  fields, dashboard) {
 
   var _d = {
     title: "",

+ 220 - 7
js/services.js

@@ -84,6 +84,7 @@ angular.module('kibana.services', [])
 
 })
 .service('kbnIndex',function($http) {
+
   // returns a promise containing an array of all indices matching the index
   // pattern that exist in a given range
   this.indices = function(from,to,pattern,interval) {
@@ -233,14 +234,14 @@ angular.module('kibana.services', [])
       var _query = {
         query: '*',
         alias: '',
-        color: colorAt(_id)
+        color: colorAt(_id),
+        id: _id
       }
       _.defaults(query,_query)
       self.list[_id] = query;
       self.ids.push(_id)
-      return id;
+      return _id;
     }
-
   }
 
   this.remove = function(id) {
@@ -256,6 +257,10 @@ angular.module('kibana.services', [])
     }
   }
 
+  this.findQuery = function(queryString) {
+    return _.findWhere(self.list,{query:queryString})
+  }
+
   var nextId = function() {
     if(_q.idQueue.length > 0) {
       return _q.idQueue.shift()
@@ -271,14 +276,185 @@ angular.module('kibana.services', [])
   init();
 
 })
-.service('dashboard', function($routeParams, $http, $rootScope, ejsResource, timer) {
+.service('filterSrv', function(dashboard, ejsResource) {
+  // Create an object to hold our service state on the dashboard
+  dashboard.current.services.filter = dashboard.current.services.filter || {};
+  _.defaults(dashboard.current.services.filter,{
+    idQueue : [],
+    list : {},
+    ids : [],
+  });
+
+  // For convenience
+  var ejs = ejsResource(config.elasticsearch);  
+  var _f = dashboard.current.services.filter;
+
+  // Save a reference to this
+  var self = this;
+
+  // Accessors
+  this.list = dashboard.current.services.filter.list;
+  this.ids = dashboard.current.services.filter.ids;
+
+  // 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'})
+    if(!_.isUndefined(id)) {
+      if(!_.isUndefined(self.list[id])) {
+        _.extend(self.list[id],filter);
+        return id;
+      } else {
+        return false;
+      }
+    } else {
+      if(_.isUndefined(filter.type)) {
+        return false;
+      } else {
+        var _id = nextId();
+        var _filter = {
+          alias: '',
+          id: _id
+        }
+        _.defaults(filter,_filter)
+        self.list[_id] = filter;
+        self.ids.push(_id)
+        return _id;
+      }
+    }
+  }
+
+  this.getBoolFilter = function(ids) {
+    // A default match all filter, just in case there are no other filters
+    var bool = ejs.BoolFilter().must(ejs.MatchAllFilter());
+    _.each(ids,function(id) {
+      switch(self.list[id].mandate) 
+      {
+      case 'mustNot':
+        bool = bool.mustNot(self.getEjsObj(id));
+        break;
+      case 'should':
+        bool = bool.should(self.getEjsObj(id));
+        break;
+      default:
+        bool = bool.must(self.getEjsObj(id));
+      }
+    })
+    return bool;
+  }
+
+  this.getEjsObj = function(id) {
+    return self.toEjsObj(self.list[id])
+  }
+
+  this.toEjsObj = function (filter) {
+    switch(filter.type)
+    {
+    case 'time':
+      return ejs.RangeFilter(filter.field)
+        .from(filter.from)
+        .to(filter.to)
+      break;
+    case 'range':
+      return ejs.RangeFilter(filter.field)
+        .from(filter.from)
+        .to(filter.to)
+      break;
+    case 'querystring':
+      console.log(filter.query)
+      return ejs.QueryFilter(ejs.QueryStringQuery(filter.query))
+      break;
+    case 'terms':
+      return ejs.TermsFilter(filter.field,filter.value)
+      break;
+    case 'exists':
+      return ejs.ExistsFilter(filter.field)
+      break;
+    case 'missing':
+      return ejs.MissingFilter(filter.field)
+      break;
+    default:
+      return false;
+    }
+  }
+
+  this.getByType = function(type) {
+    return _.pick(self.list,self.idsByType(type))
+  }
+
+  this.removeByType = function(type) {
+    var ids = self.idsByType(type)
+    _.each(ids,function(id) {
+      self.remove(id)
+    })
+    return ids;
+  }
+
+  this.idsByType = function(type) {
+    return _.pluck(_.where(self.list,{type:type}),'id')
+  }
+
+  // This special function looks for all time filters, and returns a time range according to the mode
+  this.timeRange = function(mode) {
+    var _t = _.where(self.list,{type:'time'})
+    if(_t.length == 0) {
+      return false;
+    }
+    switch(mode) {
+    case "min":
+      return {
+        from: new Date(_.max(_.pluck(_t,'from'))),
+        to: new Date(_.min(_.pluck(_t,'to')))
+      }
+      break;
+    case "max":
+      return {
+        from: new Date(_.min(_.pluck(_t,'from'))),
+        to: new Date(_.max(_.pluck(_t,'to')))
+      }
+      break;
+    default:
+      return false;
+    }
+
+  } 
+
+  this.remove = function(id) {
+    if(!_.isUndefined(self.list[id])) {
+      delete self.list[id];
+      // This must happen on the full path also since _.without returns a copy
+      self.ids = dashboard.current.services.filter.ids = _.without(self.ids,id)
+      _f.idQueue.unshift(id)
+      _f.idQueue.sort(function(a,b){return a-b});
+      return true;
+    } else {
+      return false;
+    }
+  }
+
+
+  var nextId = function() {
+    if(_f.idQueue.length > 0) {
+      return _f.idQueue.shift()
+    } else {
+      return self.ids.length;
+    }
+  }
+
+})
+.service('dashboard', function($routeParams, $http, $rootScope, $injector, ejsResource, timer, kbnIndex) {
   // A hash of defaults to use when loading a dashboard
 
   var _dash = {
     title: "",
     editable: true,
     rows: [],
-    services: {}
+    services: {},
+    index: {
+      interval: 'none',
+      pattern: '_all',
+      default: '_all'
+    },
   };
 
   // An elasticJS client to use
@@ -288,9 +464,11 @@ angular.module('kibana.services', [])
   // Empty dashboard object
   this.current = {};
   this.last = {};
+  this.indices = [];
 
   // Store a reference to this
   var self = this;
+  var filterSrv,query;
 
   $rootScope.$on('$routeChangeSuccess',function(){
     route();
@@ -326,6 +504,33 @@ angular.module('kibana.services', [])
     }
   }
 
+  // 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') {
+      if(filterSrv.idsByType('time').length > 0) {
+        var _range = filterSrv.timeRange('min');
+        kbnIndex.indices(_range.from,_range.to,
+          self.current.index.pattern,self.current.index.interval
+        ).then(function (p) {
+          if(p.length > 0) {
+            self.indices = p;          
+          } else {
+            self.indices = [self.current.index.default]
+          }
+          $rootScope.$broadcast('refresh')
+        });
+      } else {
+        // This is not optimal, we should be getting the entire index list here, or at least every
+        // index that possibly matches the pattern
+        self.indices = [self.current.index.default]
+      }
+    } else {
+      self.indices = [self.current.index.pattern]
+      $rootScope.$broadcast('refresh')
+    }
+  }
+
   this.to_file = function() {
     var blob = new Blob([angular.toJson(self.current,true)], {type: "application/json;charset=utf-8"});
     // from filesaver.js
@@ -422,8 +627,6 @@ angular.module('kibana.services', [])
         return false;
       }
     );
-
-
   }
 
   this.elasticsearch_delete = function(id) {
@@ -497,8 +700,18 @@ angular.module('kibana.services', [])
   }
 
   this.dash_load = function(dashboard) {
+
+    if(dashboard.index.interval === 'none') {
+      self.indices = [dashboard.index.pattern]
+    }
+
     self.current = dashboard;
+
     timer.cancel_all();
+
+    // Ok, now that we've setup the current dashboard, we can inject our services
+    query = $injector.get('query');
+    filterSrv = $injector.get('filterSrv')
     return true;
   }
 

+ 25 - 35
panels/bettermap/module.js

@@ -12,17 +12,10 @@
   * field :: field containing a 2 element array in the format [lon,lat]
   * tooltip :: field to extract the tool tip value from
   * spyable :: Show the 'eye' icon that reveals the last ES query
-
-  ### Group Events
-  #### Sends
-  * get_time :: On panel initialization get time range to query
-  #### Receives
-  * time :: An object containing the time range to use and the index(es) to query
-  * query :: An Array of queries, this panel uses only the first one
 */
 
 angular.module('kibana.bettermap', [])
-.controller('bettermap', function($scope, eventBus, query) {
+.controller('bettermap', function($scope, eventBus, query, dashboard, filterSrv) {
 
   // Set and populate defaults
   var _d = {
@@ -37,50 +30,53 @@ angular.module('kibana.bettermap', [])
   _.defaults($scope.panel,_d)
 
   $scope.init = function() {
-
     $scope.$on('refresh',function(){
       $scope.get_data();
     })
-
-    eventBus.register($scope,'time', function(event,time){set_time(time)});
-
-    // Now that we're all setup, request the time from our group
-    eventBus.broadcast($scope.$id,$scope.panel.group,'get_time')
+    $scope.get_data();
   }
 
-$scope.get_data = function(segment,query_id) {
+  $scope.get_data = function(segment,query_id) {
     $scope.panel.error =  false;
 
     // Make sure we have everything for the request to complete
-    if(_.isUndefined($scope.index) || _.isUndefined($scope.time))
-      return
+    if(dashboard.indices.length == 0) {
+      return;
+    }
 
     if(_.isUndefined($scope.panel.field)) {
       $scope.panel.error = "Please select a field that contains geo point in [lon,lat] format"
       return
     }
     
-    //$scope.panel.loading = true;
+    // Determine the field to sort on
+    var timeField = _.uniq(_.pluck(filterSrv.getByType('time'),'field'))
+    if(timeField.length > 1) {
+      $scope.panel.error = "Time field must be consistent amongst time filters"
+    } else if(timeField.length == 0) {
+      timeField = null;
+    } else {
+      timeField = timeField[0]
+    }
 
     var _segment = _.isUndefined(segment) ? 0 : segment
-    $scope.segment = _segment;
 
     var boolQuery = ejs.BoolQuery();
     _.each(query.list,function(q) {
-      boolQuery = boolQuery.should(ejs.QueryStringQuery((q.query || '*') + " AND _exists_:"+$scope.panel.field))
+      boolQuery = boolQuery.should(ejs.QueryStringQuery((q.query || '*')))
     })
 
-    var request = $scope.ejs.Request().indices($scope.index[_segment])
+    var request = $scope.ejs.Request().indices(dashboard.indices[_segment])
       .query(ejs.FilteredQuery(
         boolQuery,
-        ejs.RangeFilter($scope.time.field)
-          .from($scope.time.from)
-          .to($scope.time.to)
-        )
-      )
+        filterSrv.getBoolFilter(filterSrv.ids).must(ejs.ExistsFilter($scope.panel.field))
+      ))
       .fields([$scope.panel.field,$scope.panel.tooltip])
       .size($scope.panel.size)
-      .sort($scope.time.field,'desc');
+
+    if(!_.isNull(timeField)) {
+      request = request.sort(timeField,'desc');
+    }
 
     $scope.populate_modal(request)
 
@@ -125,7 +121,7 @@ $scope.get_data = function(segment,query_id) {
       $scope.$emit('draw')
 
       // Get $size results then stop querying
-      if($scope.data.length < $scope.panel.size && _segment+1 < $scope.index.length)
+      if($scope.data.length < $scope.panel.size && _segment+1 < dashboard.indices.length)
         $scope.get_data(_segment+1,$scope.query_id)
 
     });
@@ -136,18 +132,12 @@ $scope.get_data = function(segment,query_id) {
     $scope.modal = {
       title: "Inspector",
       body : "<h5>Last Elasticsearch Query</h5><pre>"+
-          'curl -XGET '+config.elasticsearch+'/'+$scope.index+"/_search?pretty -d'\n"+
+          'curl -XGET '+config.elasticsearch+'/'+dashboard.indices+"/_search?pretty -d'\n"+
           angular.toJson(JSON.parse(request.toString()),true)+
         "'</pre>", 
     } 
   }
 
-  function set_time(time) {
-    $scope.time = time;
-    $scope.index = _.isUndefined(time.index) ? $scope.index : time.index
-    $scope.get_data();
-  }
-
 })
 .directive('bettermap', function() {
   return {

+ 11 - 7
panels/derivequeries/editor.html

@@ -1,22 +1,26 @@
 <div>
-  <div class="row-fluid">    
-    <div class="span12">
-      The derive queries panel takes a query and a field, then runs a terms facet against both and generates a list of terms to query on. For example, you might want to see a histogram of the top 5 requests that return a 404. <strong>You should be careful not to select a high cardinality field</strong> as Elasticsearch must load all of these values into memory.<p>
-      Query Mode allows to optionally append original query to each term in the list.
-    </div>
-  </div>
   <div class="row-fluid">
     <div class="span1">
       <label class="small">Length</label>
       <input type="number" style="width:80%" ng-model="panel.size" ng-change="set_refresh(true)">
     </div>
+    <div class="span3">
+      <label class="small">Field</label>
+      <input type="text" bs-typeahead="fields.list" style="width:80%" ng-change="set_refresh(true)" ng-model='panel.field'></select>
+    </div>
     <div class="span3">
       <label class="small">Query Mode</label>
       <select style="width:80%" ng-change="set_refresh(true)" ng-model='panel.mode' ng-options="f for f in ['terms only','AND', 'OR']"></select>
     </div>
-    <div class="span8">
+    <div class="span5">
       <label class="small">Exclude Terms(s) (comma seperated)</label>
       <input array-join type="text" style="width:90%" ng-change="set_refresh(true)" ng-model='panel.exclude'></input>
     </div>
   </div>
+  <div class="row-fluid">    
+    <div class="span12">
+      The derive queries panel takes a query and a field, runs a terms facet, then creates queries based on them. For example, you might want to see a histogram of the top 5 requests that return a 404. <strong>You should be careful not to select a high cardinality field</strong> as Elasticsearch must load all of these values into memory.<p>
+      Query Mode allows to optionally append original query to each term in the list.
+    </div>
+  </div>
 </div>

+ 28 - 1
panels/derivequeries/module.html

@@ -1,8 +1,23 @@
 <kibana-panel ng-controller='derivequeries' ng-init="init()">
+<style>
+  .end-derive {
+    position:absolute;
+    right:15px;
+    top:5px;
+  }
+  .panel-derive {
+    padding-right: 35px !important;
+    height: 31px !important;
+    -webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
+    -moz-box-sizing: border-box;    /* Firefox, other Gecko */
+    box-sizing: border-box;         /* Opera/IE 8+ */
+  }
+</style>
   <span ng-show='panel.spyable' style="position:absolute;right:0px;top:0px" class='panelextra pointer'>
       <i bs-modal="'partials/modal.html'" class="icon-eye-open"></i>
   </span>
-  <div ng-show="!panel.multi">
+  <!--
+  <div>
     <form>
       <table class="form-horizontal">
         <tr>
@@ -29,4 +44,16 @@
       </table>
     </form>
   </div>
+  -->
+
+
+  <label class="small">Create new queries from <strong>{{panel.field}}</strong> ({{panel.mode}} mode)</label>
+  <div>
+    <form class="form-search" style="position:relative" ng-submit="get_data()">
+      <input class="search-query panel-derive input-block-level" bs-typeahead="panel.history" data-min-length=0 data-items=100 type="text" ng-model="panel.query"/>
+      <span class="end-derive">
+        <i class="icon-search pointer" ng-click="get_data()"></i>
+      </span
+    </form>
+  </div>
 </kibana-panel>

+ 25 - 31
panels/derivequeries/module.js

@@ -1,6 +1,6 @@
 /*
 
-  ## Termsquery
+  ## Derivequeries
 
   Broadcasts an array of queries based on the results of a terms facet
 
@@ -23,13 +23,15 @@
 */
 
 angular.module('kibana.derivequeries', [])
-.controller('derivequeries', function($scope, eventBus) {
+.controller('derivequeries', function($scope, $rootScope, query, eventBus, fields, dashboard, filterSrv) {
 
   // Set and populate defaults
   var _d = {
+    loading : false,
     status  : "Beta",
     label   : "Search",
     query   : "*",
+    ids     : [],
     group   : "default",
     field   : '_type',
     fields  : [],
@@ -43,26 +45,19 @@ angular.module('kibana.derivequeries', [])
   _.defaults($scope.panel,_d);
 
   $scope.init = function() {
-    eventBus.register($scope,'fields', function(event, fields) {
-      $scope.panel.fields = fields.all;
-    });
-    eventBus.register($scope,'time', function(event,time){set_time(time)});
-    eventBus.register($scope,'query', function(event, query) {
-      $scope.panel.query = _.isArray(query) ? query[0] : query;
-      $scope.get_data();
-    });
-    // Now that we're all setup, request the time from our group
-    eventBus.broadcast($scope.$id,$scope.panel.group,'get_time')
+    $scope.panel.fields = fields.list
   }
 
   $scope.get_data = function() {
     update_history($scope.panel.query);
+    
     // Make sure we have everything for the request to complete
-    if(_.isUndefined($scope.index) || _.isUndefined($scope.time))
+    if(dashboard.indices.length == 0) {
       return
+    }
 
     $scope.panel.loading = true;
-    var request = $scope.ejs.Request().indices($scope.index);
+    var request = $scope.ejs.Request().indices(dashboard.indices);
 
     // Terms mode
     request = request
@@ -73,9 +68,7 @@ angular.module('kibana.derivequeries', [])
         .facetFilter(ejs.QueryFilter(
           ejs.FilteredQuery(
             ejs.QueryStringQuery($scope.panel.query || '*'),
-            ejs.RangeFilter($scope.time.field)
-              .from($scope.time.from)
-              .to($scope.time.to)
+            filterSrv.getBoolFilter(filterSrv.ids)
             )))).size(0)
 
     $scope.populate_modal(request);
@@ -93,10 +86,22 @@ angular.module('kibana.derivequeries', [])
       } else if ($scope.panel.mode === 'OR') {
         var suffix = ' OR (' + $scope.panel.query + ')';
       }
+      var ids = [];
       _.each(results.facets.query.terms, function(v) {
-        data.push($scope.panel.field+':"'+v.term+'"'+suffix)
+        var _q = $scope.panel.field+':"'+v.term+'"'+suffix;
+        // if it isn't in the list, remove it
+        var _iq = query.findQuery(_q)
+        if(!_iq) {
+          ids.push(query.set({query:_q}));
+        } else {
+          ids.push(_iq.id);
+        }
       });
-      $scope.send_query(data)
+      _.each(_.difference($scope.panel.ids,ids),function(id){
+        query.remove(id)
+      })
+      $scope.panel.ids = ids;
+      dashboard.refresh();
     });
   }
 
@@ -114,23 +119,12 @@ angular.module('kibana.derivequeries', [])
     $scope.modal = {
       title: "Inspector",
       body : "<h5>Last Elasticsearch Query</h5><pre>"+
-          'curl -XGET '+config.elasticsearch+'/'+$scope.index+"/_search?pretty -d'\n"+
+          'curl -XGET '+config.elasticsearch+'/'+dashboard.indices+"/_search?pretty -d'\n"+
           angular.toJson(JSON.parse(request.toString()),true)+
         "'</pre>", 
     } 
   }
 
-  function set_time(time) {
-    $scope.time = time;
-    $scope.index = _.isUndefined(time.index) ? $scope.index : time.index
-    $scope.get_data();
-  }
-
-  $scope.send_query = function(query) {
-    var _query = _.isArray(query) ? query : [query]
-    eventBus.broadcast($scope.$id,$scope.panel.group,'query',_query)
-  }
-
   var update_history = function(query) {
     query = _.isArray(query) ? query : [query];
     if($scope.panel.remember > 0) {

+ 2 - 2
panels/fields/micropanel.html

@@ -1,8 +1,8 @@
 <a class="close" ng-click="dismiss()" href="">×</a>
 <h4>
   Micro Analysis of {{micropanel.field}} 
-  <i class="pointer icon-search" ng-click="build_search('_exists_',micropanel.field);dismiss();"></i>
-  <i class="pointer icon-ban-circle" ng-click="build_search('_missing_',micropanel.field);dismiss();"></i>
+  <i class="pointer icon-search" ng-click="fieldExists(micropanel.field,'exists');dismiss();"></i>
+  <i class="pointer icon-ban-circle" ng-click="fieldExists(micropanel.field,'missing');dismiss();"></i>
   <br><small>{{micropanel.count}} events in the table set</small>
 </h4>
 <table style="width:480px" class='table table-bordered table-striped table-condensed'>

+ 10 - 4
panels/fields/module.js

@@ -18,7 +18,7 @@
 
 */
 angular.module('kibana.fields', [])
-.controller('fields', function($scope, eventBus, $timeout) {
+.controller('fields', function($scope, eventBus, $timeout, dashboard, query, filterSrv) {
 
   // Set and populate defaults
   var _d = {
@@ -79,9 +79,15 @@ angular.module('kibana.fields', [])
     eventBus.broadcast($scope.$id,$scope.panel.group,"selected_fields",$scope.active)
   }
 
-  $scope.build_search = function(field, value,negate) {
-    $scope.panel.query = [add_to_query($scope.panel.query,field,value,negate)]
-    eventBus.broadcast($scope.$id,$scope.panel.group,'query',$scope.panel.query);
+  $scope.build_search = function(field,value,negate) {
+    var query = (negate ? '-':'+')+field+":\""+value+"\""
+    filterSrv.set({type:'querystring',query:query})
+    dashboard.refresh();
+  }
+
+  $scope.fieldExists = function(field,mode) {
+    filterSrv.set({type:mode,field:field})
+    dashboard.refresh();
   }
 
   $scope.is_active = function(field) {

+ 9 - 3
panels/histogram/editor.html

@@ -4,13 +4,19 @@
       <label class="small">Mode</label> 
       <select ng-change="set_refresh(true)" class="input-small" ng-model="panel.mode" ng-options="f for f in ['count','min','mean','max','total']"></select>
     </div>
-    <div class="span3" ng-show="panel.mode != 'count'">
-      <label class="small">Field</label>
+    <div class="span2">
+      <label class="small">Time Field</label>
+      <form>
+        <input ng-change="set_refresh(true)" placeholder="Start typing" bs-typeahead="fields.list" type="text" class="input-small" ng-model="panel.time_field">
+      </form>
+    </div>
+    <div class="span2" ng-show="panel.mode != 'count'">
+      <label class="small">Value Field</label>
       <form>
         <input ng-change="set_refresh(true)" placeholder="Start typing" bs-typeahead="fields.list" type="text" class="input-small" ng-model="panel.value_field">
       </form>
     </div>
-    <div class="span5" ng-show="panel.mode != 'count'">
+    <div class="span3" ng-show="panel.mode != 'count'">
       <label class="small">Note</label><small> In <strong>{{panel.mode}}</strong> mode the configured field <strong>must</strong> be a numeric type</small>
     </div>
   </div>

+ 4 - 4
panels/histogram/module.html

@@ -22,12 +22,12 @@
   </span>
   <div>
   <span ng-show='panel.zoomlinks && data'>
-    <a class='small' ng-click='zoom(0.5)'><i class='icon-zoom-in'></i> Zoom In</a>
+    <!--<a class='small' ng-click='zoom(0.5)'><i class='icon-zoom-in'></i> Zoom In</a>-->
     <a class='small' ng-click='zoom(2)'><i class='icon-zoom-out'></i> Zoom Out</a> | 
   </span>
-  <span ng-show="panel.legend" ng-repeat='series in plot.getData()' class="histogram-legend">
-    <div class="histogram-legend-dot" style="background:{{series.color}};"></div>
-    <div class='small histogram-legend-item'>{{series.label}} ({{series.hits}})</div>
+  <span ng-show="panel.legend" ng-repeat='series in data' class="histogram-legend">
+    <div class="histogram-legend-dot" style="background:{{series.info.color}};"></div>
+    <div class='small histogram-legend-item'>{{series.info.alias}} ({{series.hits}})</div>
   </span>
   <span ng-show="panel.legend" class="small"><span ng-show="panel.value_field && panel.mode != 'count'">{{panel.value_field}}</span> {{panel.mode}} per <strong>{{panel.interval}}</strong> | (<strong>{{hits}}</strong> hits)</span>
   </div>

+ 66 - 37
panels/histogram/module.js

@@ -42,7 +42,7 @@
 */
 
 angular.module('kibana.histogram', [])
-.controller('histogram', function($scope, eventBus,query) {
+.controller('histogram', function($scope, eventBus, query, dashboard, filterSrv) {
 
   // Set and populate defaults
   var _d = {
@@ -50,6 +50,7 @@ angular.module('kibana.histogram', [])
     group       : "default",
     query       : [ {query: "*", label:"Query"} ],
     mode        : 'count',
+    time_field  : '@timestamp',
     value_field : null,
     auto_int    : true,
     resolution  : 100, 
@@ -75,50 +76,46 @@ angular.module('kibana.histogram', [])
 
     $scope.queries = query;
 
-    eventBus.register($scope,'time', function(event,time){$scope.set_time(time)});
-
     $scope.$on('refresh',function(){
       $scope.get_data();
     })
 
-    // Now that we're all setup, request the time from our group if we don't 
-    // have it yet
-    if(_.isUndefined($scope.time))
-      eventBus.broadcast($scope.$id,$scope.panel.group,'get_time')
   }
 
   $scope.get_data = function(segment,query_id) {
     delete $scope.panel.error
+
     // Make sure we have everything for the request to complete
-    if(_.isUndefined($scope.index) || _.isUndefined($scope.time))
+    if(dashboard.indices.length == 0) {
       return
+    }
 
+    var _range = $scope.range = filterSrv.timeRange('min');
+    
     if ($scope.panel.auto_int)
-      $scope.panel.interval = secondsToHms(calculate_interval($scope.time.from,$scope.time.to,$scope.panel.resolution,0)/1000);
-
+      $scope.panel.interval = secondsToHms(calculate_interval(_range.from,_range.to,$scope.panel.resolution,0)/1000);
+    
     $scope.panel.loading = true;
     var _segment = _.isUndefined(segment) ? 0 : segment
-    var request = $scope.ejs.Request().indices($scope.index[_segment]);
+    var request = $scope.ejs.Request().indices(dashboard.indices[_segment]);
 
     // Build the query
     _.each($scope.queries.ids, function(id) {
       var query = $scope.ejs.FilteredQuery(
         ejs.QueryStringQuery($scope.queries.list[id].query || '*'),
-        ejs.RangeFilter($scope.time.field)
-          .from($scope.time.from)
-          .to($scope.time.to)
+        filterSrv.getBoolFilter(filterSrv.ids)
       )
 
       var facet = $scope.ejs.DateHistogramFacet(id)
       
       if($scope.panel.mode === 'count') {
-        facet = facet.field($scope.time.field)
+        facet = facet.field($scope.panel.time_field)
       } else {
         if(_.isNull($scope.panel.value_field)) {
           $scope.panel.error = "In " + $scope.panel.mode + " mode a field must be specified";
           return
         }
-        facet = facet.keyField($scope.time.field).valueField($scope.panel.value_field)
+        facet = facet.keyField($scope.panel.time_field).valueField($scope.panel.value_field)
       }
       facet = facet.interval($scope.panel.interval).facetFilter($scope.ejs.QueryFilter(query))
       request = request.facet(facet).size(0)
@@ -132,6 +129,7 @@ angular.module('kibana.histogram', [])
 
     // Populate scope when we have results
     results.then(function(results) {
+
       $scope.panel.loading = false;
       if(_segment == 0) {
         $scope.hits = 0;
@@ -145,15 +143,21 @@ angular.module('kibana.histogram', [])
         return;
       }
 
-      // Make sure we're still on the same query
-      if($scope.query_id === query_id) {
+      // Convert facet ids to numbers
+      var facetIds = _.map(_.keys(results.facets),function(k){return parseInt(k);})
+
+      // Make sure we're still on the same query/queries
+      if($scope.query_id === query_id && 
+        _.intersection(facetIds,query.ids).length == query.ids.length
+        ) {
 
         var i = 0;
-        _.each(results.facets, function(v, id) {
+        _.each(query.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) {
-            var data = [[$scope.time.from.getTime(), null],[$scope.time.to.getTime(), null]];
+            var data = [[_range.from.getTime(), null],[_range.to.getTime(), null]];
             var hits = 0;
           } else {
             var data = $scope.data[i].data
@@ -172,7 +176,7 @@ angular.module('kibana.histogram', [])
           // Create the flot series object
           var series = { 
             data: {
-              id: id,
+              info: $scope.queries.list[id],
               data: data,
               hits: hits
             },
@@ -187,7 +191,7 @@ angular.module('kibana.histogram', [])
         $scope.$emit('render')
 
         // If we still have segments left, get them
-        if(_segment < $scope.index.length-1) {
+        if(_segment < dashboard.indices.length-1) {
           $scope.get_data(_segment+1,query_id)
         }
       
@@ -198,7 +202,32 @@ angular.module('kibana.histogram', [])
   // function $scope.zoom
   // factor :: Zoom factor, so 0.5 = cuts timespan in half, 2 doubles timespan
   $scope.zoom = function(factor) {
-    eventBus.broadcast($scope.$id,$scope.panel.group,'zoom',factor);
+    var _now = Date.now();
+    var _range = filterSrv.timeRange('min');
+    var _timespan = (_range.to.valueOf() - _range.from.valueOf());
+    var _center = _range.to.valueOf() - _timespan/2
+
+    var _to = (_center + (_timespan*factor)/2)
+    var _from = (_center - (_timespan*factor)/2)
+
+    // If we're not already looking into the future, don't.
+    if(_to > Date.now() && _range.to < Date.now()) {
+      var _offset = _to - Date.now()
+      _from = _from - _offset
+      _to = Date.now();
+    }
+
+    if(factor > 1) {
+      filterSrv.removeByType('time')
+    }
+    filterSrv.set({
+      type:'time',
+      from:moment.utc(_from),
+      to:moment.utc(_to),
+      field:$scope.panel.time_field
+    })
+    dashboard.refresh();
+
   }
 
   // I really don't like this function, too much dom manip. Break out into directive?
@@ -223,14 +252,8 @@ angular.module('kibana.histogram', [])
     $scope.$emit('render');
   }
 
-  $scope.set_time = function(time) {
-    $scope.time = time;
-    $scope.index = time.index || $scope.index    
-    $scope.get_data();
-  }
-
 })
-.directive('histogramChart', function(eventBus) {
+.directive('histogramChart', function(dashboard, eventBus, filterSrv, $rootScope) {
   return {
     restrict: 'A',
     link: function(scope, elem, attrs, ctrl) {
@@ -249,10 +272,12 @@ angular.module('kibana.histogram', [])
       function render_panel() {
  
         // Populate from the query service
-        _.each(scope.data,function(series) {
-          series.label = scope.queries.list[series.id].alias,
-          series.color = scope.queries.list[series.id].color
-        })
+        try {
+          _.each(scope.data,function(series) {
+            series.label = series.info.alias,
+            series.color = series.info.color
+          })
+        } catch(e) {return}
 
         // Set barwidth based on specified interval
         var barwidth = interval_to_seconds(scope.panel.interval)*1000
@@ -366,9 +391,13 @@ angular.module('kibana.histogram', [])
       });
 
       elem.bind("plotselected", function (event, ranges) {
-        scope.time.from = moment(ranges.xaxis.from);
-        scope.time.to   = moment(ranges.xaxis.to)
-        eventBus.broadcast(scope.$id,scope.panel.group,'set_time',scope.time)
+        var _id = filterSrv.set({
+          type  : 'time',
+          from  : moment.utc(ranges.xaxis.from),
+          to    : moment.utc(ranges.xaxis.to),
+          field : scope.panel.time_field
+        })
+        dashboard.refresh();
       });
     }
   };

+ 13 - 10
panels/hits/module.html

@@ -3,14 +3,14 @@
   <div ng-show="panel.counter_pos == 'above' && (panel.chart == 'bar' || panel.chart == 'pie')" id='{{$id}}-legend'>
     <!-- vertical legend -->
     <table class="small" ng-show="panel.arrangement == 'vertical'">  
-      <tr ng-repeat="query in plot.getData()">
-        <td><div style="display:inline-block;border-radius:5px;background:{{query.color}};height:10px;width:10px"></div></td> <td style="padding-right:10px;padding-left:10px;">{{query.label}}</td><td>{{query.data[0][1]}}</td>
+      <tr ng-repeat="query in data">
+        <td><div style="display:inline-block;border-radius:5px;background:{{query.info.color}};height:10px;width:10px"></div></td> <td style="padding-right:10px;padding-left:10px;">{{query.info.alias}}</td><td>{{query.data[0][1]}}</td>
       </tr>
     </table>
 
     <!-- horizontal legend -->
-    <div class="small" ng-show="panel.arrangement == 'horizontal'" ng-repeat="query in plot.getData()" style="float:left;padding-left: 10px;">
-     <span><div style="display:inline-block;border-radius:5px;background:{{query.color}};height:10px;width:10px"></div></span> {{query.label}} ({{query.data[0][1]}}) </span>
+    <div class="small" ng-show="panel.arrangement == 'horizontal'" ng-repeat="query in data" style="float:left;padding-left: 10px;">
+     <span><div style="display:inline-block;border-radius:5px;background:{{query.info.color}};height:10px;width:10px"></div></span> {{query.info.alias}} ({{query.data[0][1]}}) </span>
     </div><br>
 
   </div>
@@ -20,23 +20,26 @@
   <div ng-show="panel.chart == 'pie' || panel.chart == 'bar'" hits-chart params="{{panel}}" style="height:{{panel.height || row.height}};position:relative"></div>
 
   <div ng-show="panel.counter_pos == 'below' && (panel.chart == 'bar' || panel.chart == 'pie')" id='{{$id}}-legend'>
-
     <!-- vertical legend -->
     <table class="small" ng-show="panel.arrangement == 'vertical'">  
-      <tr ng-repeat="query in plot.getData()">
-        <td><div style="display:inline-block;border-radius:5px;background:{{query.color}};height:10px;width:10px"></div></td> <td style="padding-right:10px;padding-left:10px;">{{query.label}}</td><td>{{query.data[0][1]}}</td>
+      <tr ng-repeat="query in data">
+        <td><div style="display:inline-block;border-radius:5px;background:{{query.info.color}};height:10px;width:10px"></div></td> <td style="padding-right:10px;padding-left:10px;">{{query.info.alias}}</td><td>{{query.data[0][1]}}</td>
       </tr>
     </table>
 
     <!-- horizontal legend -->
-    <div class="small" ng-show="panel.arrangement == 'horizontal'" ng-repeat="query in plot.getData()" style="float:left;padding-left: 10px;">
-     <span><div style="display:inline-block;border-radius:5px;background:{{query.color}};height:10px;width:10px"></div></span> {{query.label}} ({{query.data[0][1]}}) </span>
+    <div class="small" ng-show="panel.arrangement == 'horizontal'" ng-repeat="query in data" style="float:left;padding-left: 10px;">
+     <span><div style="display:inline-block;border-radius:5px;background:{{query.info.color}};height:10px;width:10px"></div></span> {{query.info.alias}} ({{query.data[0][1]}}) </span>
     </div><br>
 
   </div>
 
   <div ng-show="panel.chart == 'total'"><div ng-style="panel.style" style="line-height:{{panel.style['font-size']}}">{{hits}}</div></div>
 
-  <span ng-show="panel.chart == 'list'"><span ng-style="panel.style" style="line-height:{{panel.style['font-size']}}" ng-repeat="query in data">{{query.label}} ({{query.hits}})<span></span><br ng-show="panel.arrangement == 'vertical' && panel.chart == 'list'">
+  <span ng-show="panel.chart == 'list'">
+    <div ng-style="panel.style" style="display:inline-block;line-height:{{panel.style['font-size']}}" ng-repeat="query in data">
+      <i class="icon-circle" style="color:{{query.info.color}}"></i> {{query.info.alias}} ({{query.hits}})
+    </div>
+  </span><br ng-show="panel.arrangement == 'vertical' && panel.chart == 'list'">
 
 </kibana-panel>         

+ 34 - 51
panels/hits/module.js

@@ -22,7 +22,7 @@
 
 */
 angular.module('kibana.hits', [])
-.controller('hits', function($scope, eventBus, query) {
+.controller('hits', function($scope, eventBus, query, dashboard, filterSrv) {
 
   // Set and populate defaults
   var _d = {
@@ -30,8 +30,8 @@ angular.module('kibana.hits', [])
     query   : ["*"],
     group   : "default",
     style   : { "font-size": '10pt'},
-    arrangement : 'vertical',
-    chart       : 'none',
+    arrangement : 'horizontal',
+    chart       : 'bar',
     counter_pos : 'above',
     donut   : false,
     tilt    : false,
@@ -40,22 +40,13 @@ angular.module('kibana.hits', [])
   _.defaults($scope.panel,_d)
 
   $scope.init = function () {
-    $scope.queries = query;
-
     $scope.hits = 0;
-    eventBus.register($scope,'time', function(event,time){
-      set_time(time)
-    });
-
-
+   
     $scope.$on('refresh',function(){
-      console.log($scope.queries)
-      console.log(query)
       $scope.get_data();
     })
+    $scope.get_data();
 
-    // Now that we're all setup, request the time from our group
-    eventBus.broadcast($scope.$id,$scope.panel.group,'get_time')
   }
 
   $scope.get_data = function(segment,query_id) {
@@ -63,23 +54,22 @@ angular.module('kibana.hits', [])
     $scope.panel.loading = true;
 
     // Make sure we have everything for the request to complete
-    if(_.isUndefined($scope.index) || _.isUndefined($scope.time))
+    if(dashboard.indices.length == 0) {
       return
+    }
 
     var _segment = _.isUndefined(segment) ? 0 : segment
-    var request = $scope.ejs.Request().indices($scope.index[_segment]);
+    var request = $scope.ejs.Request().indices(dashboard.indices[_segment]);
     
     // Build the question part of the query
-    _.each($scope.queries.ids, function(id) {
-      var query = $scope.ejs.FilteredQuery(
-        ejs.QueryStringQuery($scope.queries.list[id].query || '*'),
-        ejs.RangeFilter($scope.time.field)
-          .from($scope.time.from)
-          .to($scope.time.to))
+    _.each(query.ids, function(id) {
+      var _q = $scope.ejs.FilteredQuery(
+        ejs.QueryStringQuery(query.list[id].query || '*'),
+        filterSrv.getBoolFilter(filterSrv.ids));
     
       request = request
         .facet($scope.ejs.QueryFacet(id)
-          .query(query)
+          .query(_q)
         ).size(0)
     });
 
@@ -91,7 +81,6 @@ angular.module('kibana.hits', [])
 
     // Populate scope when we have results
     results.then(function(results) {
-
       $scope.panel.loading = false;
       if(_segment == 0) {
         $scope.hits = 0;
@@ -104,16 +93,24 @@ angular.module('kibana.hits', [])
         $scope.panel.error = $scope.parse_error(results.error);
         return;
       }
-      if($scope.query_id === query_id) {
+
+      // Convert facet ids to numbers
+      var facetIds = _.map(_.keys(results.facets),function(k){return parseInt(k);})
+
+      // Make sure we're still on the same query/queries
+      if($scope.query_id === query_id && 
+        _.intersection(facetIds,query.ids).length == query.ids.length
+        ) {
         var i = 0;
-        _.each(results.facets, function(v, id) {
+        _.each(query.ids, function(id) {
+          var v = results.facets[id]
           var hits = _.isUndefined($scope.data[i]) || _segment == 0 ? 
             v.count : $scope.data[i].hits+v.count
           $scope.hits += v.count
 
           // Create series
           $scope.data[i] = { 
-            //label: $scope.panel.query[i].label || "query"+(parseInt(i)+1), 
+            info: query.list[id],
             id: id,
             hits: hits,
             data: [[i,hits]]
@@ -122,26 +119,13 @@ angular.module('kibana.hits', [])
           i++;
         });
         $scope.$emit('render');
-        if(_segment < $scope.index.length-1) 
+        if(_segment < dashboard.indices.length-1) 
           $scope.get_data(_segment+1,query_id)
         
       }
     });
   }
 
-  $scope.remove_query = function(q) {
-    $scope.panel.query = _.without($scope.panel.query,q);
-    $scope.get_data();
-  }
-
-  $scope.add_query = function(label,query) {
-    $scope.panel.query.unshift({
-      query: query,
-      label: label, 
-    });
-    $scope.get_data();
-  }
-
   $scope.set_refresh = function (state) { 
     $scope.refresh = state; 
   }
@@ -155,11 +139,10 @@ angular.module('kibana.hits', [])
 
   function set_time(time) {
     $scope.time = time;
-    $scope.index = _.isUndefined(time.index) ? $scope.index : time.index
     $scope.get_data();
   }
 
-}).directive('hitsChart', function(eventBus) {
+}).directive('hitsChart', function(eventBus, query) {
   return {
     restrict: 'A',
     link: function(scope, elem, attrs, ctrl) {
@@ -177,10 +160,12 @@ angular.module('kibana.hits', [])
       // Function for rendering panel
       function render_panel() {
 
-        _.each(scope.data,function(series) {
-          series.label = scope.queries.list[series.id].alias,
-          series.color = scope.queries.list[series.id].color
-        })
+        try {
+          _.each(scope.data,function(series) {
+            series.label = series.info.alias,
+            series.color = series.info.color
+          })
+        } catch(e) {return}
 
         var scripts = $LAB.script("common/lib/panels/jquery.flot.js").wait()
                           .script("common/lib/panels/jquery.flot.pie.js")
@@ -201,13 +186,12 @@ angular.module('kibana.hits', [])
                 yaxis: { show: true, min: 0, color: "#c8c8c8" },
                 xaxis: { show: false },
                 grid: {
-                  backgroundColor: '#272b30',
                   borderWidth: 0,
                   borderColor: '#eee',
                   color: "#eee",
                   hoverable: true,
                 },
-                colors: ['#86B22D','#BF6730','#1D7373','#BFB930','#BF3030','#77207D']
+                colors: query.colors
               })
             if(scope.panel.chart === 'pie')
               scope.plot = $.plot(elem, scope.data, {
@@ -223,7 +207,6 @@ angular.module('kibana.hits', [])
                       label: 'The Rest'
                     },
                     stroke: {
-                      color: '#272b30',
                       width: 0
                     },
                     label: { 
@@ -239,7 +222,7 @@ angular.module('kibana.hits', [])
                 },
                 //grid: { hoverable: true, clickable: true },
                 grid:   { hoverable: true, clickable: true },
-                colors: ['#86B22D','#BF6730','#1D7373','#BFB930','#BF3030','#77207D']
+                colors: query.colors
               });
 
             // Compensate for the height of the  legend. Gross

+ 20 - 1
panels/map/module.html

@@ -3,6 +3,7 @@
     .jvectormap-label {
         position: absolute;
         display: none;
+        visibility: hidden;
         border: solid 1px #CDCDCD;
         -webkit-border-radius: 3px;
         -moz-border-radius: 3px;
@@ -30,6 +31,10 @@
         text-align: center;
     }
 
+    .jvectormap {
+        position: relative;
+    }
+
     .jvectormap-zoomin {
         display: none;
         top: 10px;
@@ -39,9 +44,23 @@
         display: none;
         top: 30px;
     }
+
+    .map-legend {
+        color   : #c8c8c8;
+        padding : 10px;
+        font-size: 11pt;
+        font-weight: 200;
+        background-color: #1f1f1f;
+        border-radius: 5px;
+        position: absolute;
+        right: 0px;
+        top: 15px;
+        display: none;
+        z-index: 99;
+    }
   </style>
   <span ng-show="panel.spyable" class='spy panelextra pointer'>
     <i bs-modal="'partials/modal.html'" class="icon-eye-open"></i>
   </span>
-  <div map params="{{panel}}" style="height:{{panel.height || row.height}}"></div>
+  <div class="jvectormap" map params="{{panel}}" style="height:{{panel.height || row.height}}"></div>
 </kibana-panel>

+ 24 - 35
panels/map/module.js

@@ -28,7 +28,7 @@
 */
 
 angular.module('kibana.map', [])
-.controller('map', function($scope, eventBus) {
+.controller('map', function($scope, $rootScope, eventBus, query, dashboard, filterSrv) {
 
   // Set and populate defaults
   var _d = {
@@ -45,22 +45,24 @@ angular.module('kibana.map', [])
   _.defaults($scope.panel,_d)
 
   $scope.init = function() {
-    eventBus.register($scope,'time', function(event,time){set_time(time)});
-    eventBus.register($scope,'query', function(event, query) {
-      $scope.panel.query = _.isArray(query) ? query[0] : query;
-      $scope.get_data();
-    });
-    // Now that we're all setup, request the time from our group
-    eventBus.broadcast($scope.$id,$scope.panel.group,'get_time')
+    $scope.$on('refresh',function(){$scope.get_data()})
+    $scope.get_data();
   }
 
   $scope.get_data = function() {
+    
     // Make sure we have everything for the request to complete
-    if(_.isUndefined($scope.index) || _.isUndefined($scope.time))
+    if(dashboard.indices.length == 0) {
       return
+    }
 
     $scope.panel.loading = true;
-    var request = $scope.ejs.Request().indices($scope.index);
+    var request = $scope.ejs.Request().indices(dashboard.indices);
+
+    var boolQuery = ejs.BoolQuery();
+    _.each(query.list,function(q) {
+      boolQuery = boolQuery.should(ejs.QueryStringQuery(q.query || '*'))
+    })
 
     // Then the insert into facet and make the request
     var request = request
@@ -70,10 +72,8 @@ angular.module('kibana.map', [])
         .exclude($scope.panel.exclude)
         .facetFilter(ejs.QueryFilter(
           ejs.FilteredQuery(
-            ejs.QueryStringQuery($scope.panel.query || '*'),
-            ejs.RangeFilter($scope.time.field)
-              .from($scope.time.from)
-              .to($scope.time.to)
+            boolQuery,
+            filterSrv.getBoolFilter(filterSrv.ids)
             )))).size(0);
 
     $scope.populate_modal(request);
@@ -97,22 +97,17 @@ angular.module('kibana.map', [])
     $scope.modal = {
       title: "Inspector",
       body : "<h5>Last Elasticsearch Query</h5><pre>"+
-          'curl -XGET '+config.elasticsearch+'/'+$scope.index+"/_search?pretty -d'\n"+
+          'curl -XGET '+config.elasticsearch+'/'+dashboard.indices+"/_search?pretty -d'\n"+
           angular.toJson(JSON.parse(request.toString()),true)+
         "'</pre>", 
     } 
   }
 
-  function set_time(time) {
-    $scope.time = time;
-    $scope.index = _.isUndefined(time.index) ? $scope.index : time.index
-    $scope.get_data();
-  }
-
   $scope.build_search = function(field,value) {
-    $scope.panel.query = add_to_query($scope.panel.query,field,value,false)
-    $scope.get_data();
-    eventBus.broadcast($scope.$id,$scope.panel.group,'query',[$scope.panel.query]);
+    _.each(query.list,function(q) {
+      q.query = add_to_query(q.query,field,value,false);
+    })
+    dashboard.refresh();
   }
 
 })
@@ -155,20 +150,12 @@ angular.module('kibana.map', [])
               }]
             },
             onRegionLabelShow: function(event, label, code){
-              $('.jvectormap-label').css({
-                "position"    : "absolute",
-                "display"     : "none",
-                'color'       : "#c8c8c8",
-                'padding'     : '10px',
-                'font-size'   : '11pt',
-                'font-weight' : 200,
-                'background-color': '#1f1f1f',
-                'border-radius': '5px'
-              })
+              elem.children('.map-legend').show()
               var count = _.isUndefined(scope.data[code]) ? 0 : scope.data[code];
-              $('.jvectormap-label').text(label.text() + ": " + count);
+              elem.children('.map-legend').text(label.text() + ": " + count);
             },
             onRegionOut: function(event, code) {
+              $('.map-legend').hide();
             },
             onRegionClick: function(event, code) {
               var count = _.isUndefined(scope.data[code]) ? 0 : scope.data[code];
@@ -176,6 +163,8 @@ angular.module('kibana.map', [])
                 scope.build_search(scope.panel.field,code)
             }
           });
+          elem.prepend('<span class="map-legend"></span>');
+          $('.map-legend').hide();
         })
       }
     }

+ 52 - 73
panels/pie/editor.html

@@ -1,83 +1,62 @@
-<div class="row-fluid" ng-switch="panel.mode">
-<div class="span3">
-  <label class="small">Mode</label> 
-  <select class="input-small" ng-change="set_mode(panel.mode)" ng-model="panel.mode" ng-options="f for f in ['terms','goal']"></select>
-</div> 
-  <div ng-switch-when="terms">
+<div>
+  <div class="row-fluid" ng-switch="panel.mode">
     <div class="row-fluid">
-      <div class="span3">
-        <form style="margin-bottom: 0px">
+      <div class="span2">
+        <label class="small">Mode</label> 
+        <select class="input-small" ng-change="set_mode(panel.mode);set_refresh(true)" ng-model="panel.mode" ng-options="f for f in ['terms','goal']"></select>
+      </div> 
+    </div>
+    <div ng-switch-when="terms">
+      <div class="row-fluid">
+        <div class="span2">
           <label class="small">Field</label>
-          <input type="text" style="width:90%" bs-typeahead="fields.list" ng-model="panel.query.field">
-        </form>
-      </div>
-      <div class="span5">
-        <form class="input-append" style="margin-bottom: 0px">
-          <label class="small">Query</label>
-          <input type="text" style="width:80%" ng-model="panel.query.query">
-          <button class="btn" ng-click="get_data()"><i class="icon-search"></i></button>
-        </form>
-      </div>
-    </div>  
-    <div class="row-fluid">
-      <div class="span3">
-        <label class="small">Length</label>
-        <input type="number" style="width:80%" ng-model="panel.size" ng-change="get_data()">
-      </div>
-      <div class="span8">
-        <form class="input-append" style="margin-bottom: 0px">
+          <input type="text" class="input-small" bs-typeahead="fields.list" ng-model="panel.query.field" ng-change="set_refresh(true)">
+        </div>
+        <div class="span2">
+          <label class="small">Length</label>
+          <input class="input-small" type="number" ng-model="panel.size" ng-change="set_refresh(true)">
+        </div>
+        <div class="span6">
           <label class="small">Exclude Terms(s) (comma seperated)</label>
-          <input array-join type="text" style="width:90%"  ng-model='panel.exclude'></input>
-          <button class="btn" ng-click="get_data()"><i class="icon-search"></i></button>
-        </form>
-      </div>
+          <input array-join type="text" ng-model='panel.exclude'></input>
+        </div>
+      </div>  
     </div>
-  </div>
-  <div ng-switch-when="goal">
-    <div class="row-fluid">
-      <div class="span3">
-        <label class="small">Mode</label> 
-        <select class="input-small" ng-change="set_mode(panel.mode)" ng-model="panel.mode" ng-options="f for f in ['terms','goal']"></select>
-      </div>   
-      <div class="span2">
-        <form style="margin-bottom: 0px">
-          <label class="small">Goal</label>
-          <input type="number" style="width:90%" ng-model="panel.query.goal">
-        </form>
-      </div>
-      <div class="span5">
-        <form class="input-append" style="margin-bottom: 0px">
-          <label class="small">Query</label>
-          <input type="text" style="width:80%" ng-model="panel.query.query">
-          <button class="btn" ng-click="get_data()"><i class="icon-search"></i></button>
-        </form>
+    <div ng-switch-when="goal">
+      <div class="row-fluid">
+        <div class="span2">
+          <form style="margin-bottom: 0px">
+            <label class="small">Goal</label>
+            <input type="number" style="width:90%" ng-model="panel.query.goal" ng-change="set_refresh(true)">
+          </form>
+        </div>
       </div>
     </div>
   </div>
-</div>
-<div class="row-fluid">    
-  <div class="span1">
-    <label class="small"> Donut </label><input type="checkbox" ng-model="panel.donut" ng-checked="panel.donut">
-  </div>
-  <div class="span1">
-    <label class="small"> Tilt </label><input type="checkbox" ng-model="panel.tilt" ng-checked="panel.tilt">
-  </div>
-  <div class="span1">
-    <label class="small"> Labels </label><input type="checkbox" ng-model="panel.labels" ng-checked="panel.labels">
-  </div>
-  <div class="span3"> 
-    <label class="small">Legend</label> 
-    <select class="input-small" ng-model="panel.legend" ng-options="f for f in ['above','below','none']"></select></span>
-  </div>
-</div>
-<h5>Panel Spy</h5>
-<div class="row-fluid">
-  <div class="span2"> 
-    <label class="small"> Spyable </label><input type="checkbox" ng-model="panel.spyable" ng-checked="panel.spyable">
+  <div class="row-fluid">    
+    <div class="span1">
+      <label class="small"> Donut </label><input type="checkbox" ng-model="panel.donut" ng-checked="panel.donut">
+    </div>
+    <div class="span1">
+      <label class="small"> Tilt </label><input type="checkbox" ng-model="panel.tilt" ng-checked="panel.tilt">
+    </div>
+    <div class="span1">
+      <label class="small"> Labels </label><input type="checkbox" ng-model="panel.labels" ng-checked="panel.labels">
+    </div>
+    <div class="span3"> 
+      <label class="small">Legend</label> 
+      <select class="input-small" ng-model="panel.legend" ng-options="f for f in ['above','below','none']"></select></span>
+    </div>
   </div>
-  <div class="span9 small">
-    The panel spy shows 'behind the scenes' information about a panel. It can
-    be accessed by clicking the <i class='icon-eye-open'></i> in the top right
-    of the panel.
+  <h5>Panel Spy</h5>
+  <div class="row-fluid">
+    <div class="span2"> 
+      <label class="small"> Spyable </label><input type="checkbox" ng-model="panel.spyable" ng-checked="panel.spyable">
+    </div>
+    <div class="span9 small">
+      The panel spy shows 'behind the scenes' information about a panel. It can
+      be accessed by clicking the <i class='icon-eye-open'></i> in the top right
+      of the panel.
+    </div>
   </div>
 </div>

+ 44 - 62
panels/pie/module.js

@@ -33,17 +33,17 @@
 */
 
 angular.module('kibana.pie', [])
-.controller('pie', function($scope, eventBus) {
+.controller('pie', function($scope, $rootScope, eventBus, query, dashboard, filterSrv) {
 
   // Set and populate defaults
   var _d = {
     status  : "Deprecating Soon",
-    query   : { field:"_all", query:"*", goal: 1}, 
+    query   : { field:"_type", goal: 100}, 
     size    : 10,
     exclude : [],
     donut   : false,
     tilt    : false,
-    legend  : true,
+    legend  : "above",
     labels  : true,
     mode    : "terms",
     group   : "default",
@@ -53,30 +53,7 @@ angular.module('kibana.pie', [])
   _.defaults($scope.panel,_d)
 
   $scope.init = function() {
-    eventBus.register($scope,'time', function(event,time){set_time(time)});
-    eventBus.register($scope,'query', function(event, query) {
-      $scope.panel.query.query = _.isArray(query) ? query[0] : query;
-      $scope.get_data();
-    });
-    // Now that we're all setup, request the time from our group
-    eventBus.broadcast($scope.$id,$scope.panel.group,'get_time')
-  }
-
-
-  $scope.remove_query = function(q) {
-    if($scope.panel.mode !== 'query') 
-      return false;
-    $scope.panel.query = _.without($scope.panel.query,q);
-    $scope.get_data();
-  }
-
-  $scope.add_query = function(label,query) {
-    if($scope.panel.mode !== 'query') 
-      return false;
-    $scope.panel.query.unshift({
-      query: query,
-      label: label, 
-    });
+    $scope.$on('refresh',function(){$scope.get_data()})
     $scope.get_data();
   }
 
@@ -84,21 +61,40 @@ angular.module('kibana.pie', [])
     switch(mode)
     {
     case 'terms':
-      $scope.panel.query = {query:"*",field:"_all"};
+      $scope.panel.query = {field:"_all"};
       break;
     case 'goal':
-      $scope.panel.query = {query:"*",goal:100};
+      $scope.panel.query = {goal:100};
       break;
     }
   }
 
+  $scope.set_refresh = function (state) { 
+    $scope.refresh = state; 
+  }
+
+  $scope.close_edit = function() {
+    if($scope.refresh)
+      $scope.get_data();
+    $scope.refresh =  false;
+    $scope.$emit('render');
+  }
+
   $scope.get_data = function() {
+    
     // Make sure we have everything for the request to complete
-    if(_.isUndefined($scope.index) || _.isUndefined($scope.time))
+    if(dashboard.indices.length == 0) {
       return
+    } 
 
     $scope.panel.loading = true;
-    var request = $scope.ejs.Request().indices($scope.index);
+    var request = $scope.ejs.Request().indices(dashboard.indices);
+
+    // This could probably be changed to a BoolFilter 
+    var boolQuery = ejs.BoolQuery();
+    _.each(query.list,function(q) {
+      boolQuery = boolQuery.should(ejs.QueryStringQuery(q.query || '*'))
+    })
 
     // Terms mode
     if ($scope.panel.mode == "terms") {
@@ -109,10 +105,8 @@ angular.module('kibana.pie', [])
           .exclude($scope.panel.exclude)
           .facetFilter(ejs.QueryFilter(
             ejs.FilteredQuery(
-              ejs.QueryStringQuery($scope.panel.query.query || '*'),
-              ejs.RangeFilter($scope.time.field)
-                .from($scope.time.from)
-                .to($scope.time.to)
+              boolQuery,
+              filterSrv.getBoolFilter(filterSrv.ids)
               )))).size(0)
 
       $scope.populate_modal(request);
@@ -141,11 +135,8 @@ angular.module('kibana.pie', [])
     // Goal mode
     } else {
       request = request
-        .query(ejs.QueryStringQuery($scope.panel.query.query || '*'))
-        .filter(ejs.RangeFilter($scope.time.field)
-          .from($scope.time.from)
-          .to($scope.time.to)
-          .cache(false))
+        .query(boolQuery)
+        .filter(filterSrv.getBoolFilter(filterSrv.ids))
         .size(0)
       
       $scope.populate_modal(request);
@@ -169,26 +160,14 @@ angular.module('kibana.pie', [])
     $scope.modal = {
       title: "Inspector",
       body : "<h5>Last Elasticsearch Query</h5><pre>"+
-          'curl -XGET '+config.elasticsearch+'/'+$scope.index+"/_search?pretty -d'\n"+
+          'curl -XGET '+config.elasticsearch+'/'+dashboard.indices+"/_search?pretty -d'\n"+
           angular.toJson(JSON.parse(request.toString()),true)+
         "'</pre>", 
     } 
   }
 
-  $scope.build_search = function(field,value) {
-    $scope.panel.query.query = add_to_query($scope.panel.query.query,field,value,false)
-    $scope.get_data();
-    eventBus.broadcast($scope.$id,$scope.panel.group,'query',[$scope.panel.query.query]);
-  }
-
-  function set_time(time) {
-    $scope.time = time;
-    $scope.index = _.isUndefined(time.index) ? $scope.index : time.index
-    $scope.get_data();
-  }
-  
 })
-.directive('pie', function() {
+.directive('pie', function(query, filterSrv, dashboard) {
   return {
     restrict: 'A',
     link: function(scope, elem, attrs) {
@@ -228,8 +207,8 @@ angular.module('kibana.pie', [])
             show: scope.panel.labels,
             radius: 2/3,
             formatter: function(label, series){
-              return '<div ng-click="build_search(panel.query.field,\''+label+'\') "style="font-size:8pt;text-align:center;padding:2px;color:white;">'+
-                label+'<br/>'+Math.round(series.percent)+'%</div>';
+              return '<div "style="font-size:8pt;text-align:center;padding:2px;color:white;">'+
+                series.info.alias+'<br/>'+Math.round(series.percent)+'%</div>';
             },
             threshold: 0.1 
           }
@@ -258,7 +237,7 @@ angular.module('kibana.pie', [])
             clickable: true 
           },
           legend: { show: false },
-          colors: ['#86B22D','#BF6730','#1D7373','#BFB930','#BF3030','#77207D']
+          colors: query.colors
         };
 
         // Populate element
@@ -269,7 +248,7 @@ angular.module('kibana.pie', [])
         }
       }
 
-      function piett(x, y, contents) {
+      function tt(x, y, contents) {
         var tooltip = $('#pie-tooltip').length ? 
           $('#pie-tooltip') : $('<div id="pie-tooltip"></div>');
 
@@ -287,16 +266,19 @@ angular.module('kibana.pie', [])
       }
 
       elem.bind("plotclick", function (event, pos, object) {
-        if (!object)
+        if (!object) {
           return;
-        if(scope.panel.mode === 'terms')
-          scope.build_search(scope.panel.query.field,object.series.label);
+        }
+        if(scope.panel.mode === 'terms') {
+          filterSrv.set({type:'terms',field:scope.panel.query.field,value:object.series.label})
+          dashboard.refresh();
+        }
       });
 
       elem.bind("plothover", function (event, pos, item) {
         if (item) {
           var percent = parseFloat(item.series.percent).toFixed(1) + "%";
-          piett(pos.pageX, pos.pageY, "<div style='vertical-align:middle;display:inline-block;background:"+item.series.color+";height:15px;width:15px;border-radius:10px;'></div> " + 
+          tt(pos.pageX, pos.pageY, "<div style='vertical-align:middle;display:inline-block;background:"+item.series.color+";height:15px;width:15px;border-radius:10px;'></div> " + 
             (item.series.label||"")+ " " + percent);
         } else {
           $("#pie-tooltip").remove();

+ 15 - 5
panels/query/meta.html

@@ -1,5 +1,15 @@
-<style>
-</style>
-<a class="close" ng-click="render();dismiss();" href="">×</a>
-<input class="input-medium" type="text" ng-model="queries.list[id].alias" placeholder='Alias...' />
-<i ng-repeat="color in queries.colors" class="pointer" ng-class="{'icon-circle-blank':queries.list[id].color == color,'icon-circle':queries.list[id].color != color}" style="color:{{color}}" ng-click="queries.list[id].color = color;render();"> </i>
+<div>
+  <style>
+    .input-query-alias {
+      margin-bottom: 5px !important;
+    }
+  </style>
+  <a class="close" ng-click="render();dismiss();" href="">×</a>
+  <h6>Query Alias</h6>
+  <form>
+    <input class="input-medium input-query-alias" type="text" ng-model="queries.list[id].alias" placeholder='Alias...' />
+    <div>
+      <i ng-repeat="color in queries.colors" class="pointer" ng-class="{'icon-circle-blank':queries.list[id].color == color,'icon-circle':queries.list[id].color != color}" style="color:{{color}}" ng-click="queries.list[id].color = color;render();"> </i>
+    </div>
+  </form>
+</div>

+ 0 - 2
panels/query/module.js

@@ -39,12 +39,10 @@ angular.module('kibana.query', [])
   }
 
   $scope.refresh = function(query) {
-    console.log('refresh')
     $rootScope.$broadcast('refresh')
   }
 
   $scope.render = function(query) {
-    console.log('render')
     $rootScope.$broadcast('render')
   }
 

+ 13 - 26
panels/table/module.js

@@ -29,7 +29,7 @@
 */
 
 angular.module('kibana.table', [])
-.controller('table', function($rootScope, $scope, eventBus, fields, query) {
+.controller('table', function($rootScope, $scope, eventBus, fields, query, dashboard, filterSrv) {
 
   // Set and populate defaults
   var _d = {
@@ -61,10 +61,6 @@ angular.module('kibana.table', [])
 
   $scope.set_listeners = function(group) {
     $scope.$on('refresh',function(){$scope.get_data()})
-    eventBus.register($scope,'time',function(event,time) {
-      $scope.panel.offset = 0;
-      set_time(time)
-    });
     eventBus.register($scope,'sort', function(event,sort){
       $scope.panel.sort = _.clone(sort);
       $scope.get_data();
@@ -112,26 +108,26 @@ angular.module('kibana.table', [])
   }
 
   $scope.build_search = function(field,value,negate) {
-    _.each(query.list,function(q) {
-      q.query = add_to_query(q.query,field,value,negate);
-    })
+    var query = (negate ? '-':'+')+field+":\""+value+"\""
+    filterSrv.set({type:'querystring',query:query})
     $scope.panel.offset = 0;
-    $rootScope.$broadcast('refresh')
+    dashboard.refresh();
   }
 
   $scope.get_data = function(segment,query_id) {
     $scope.panel.error =  false;
 
     // Make sure we have everything for the request to complete
-    if(_.isUndefined($scope.index) || _.isUndefined($scope.time))
+    if(dashboard.indices.length == 0) {
       return
+    }
     
     $scope.panel.loading = true;
 
     var _segment = _.isUndefined(segment) ? 0 : segment
     $scope.segment = _segment;
 
-    var request = $scope.ejs.Request().indices($scope.index[_segment])
+    var request = $scope.ejs.Request().indices(dashboard.indices[_segment])
 
     var boolQuery = ejs.BoolQuery();
     _.each(query.list,function(q) {
@@ -141,11 +137,8 @@ angular.module('kibana.table', [])
     request = request.query(
       ejs.FilteredQuery(
         boolQuery,
-        ejs.RangeFilter($scope.time.field)
-          .from($scope.time.from)
-          .to($scope.time.to)
-        )
-      )
+        filterSrv.getBoolFilter(filterSrv.ids)
+      ))
       .highlight(
         ejs.Highlight($scope.panel.highlight)
         .fragmentSize(2147483647) // Max size of a 32bit unsigned int
@@ -209,10 +202,10 @@ angular.module('kibana.table', [])
       // If we're not sorting in reverse chrono order, query every index for
       // size*pages results
       // Otherwise, only get size*pages results then stop querying
-      if(
-          ($scope.data.length < $scope.panel.size*$scope.panel.pages || 
-            !(($scope.panel.sort[0] === $scope.time.field) && $scope.panel.sort[1] === 'desc')) && 
-          _segment+1 < $scope.index.length
+      if($scope.data.length < $scope.panel.size*$scope.panel.pages
+        //($scope.data.length < $scope.panel.size*$scope.panel.pages
+         // || !(($scope.panel.sort[0] === $scope.time.field) && $scope.panel.sort[1] === 'desc'))
+        && _segment+1 < dashboard.indices.length
       ) {
         $scope.get_data(_segment+1,$scope.query_id)
       }
@@ -265,12 +258,6 @@ angular.module('kibana.table', [])
   }
 
 
-  function set_time(time) {
-    $scope.time = time;
-    $scope.index = _.isUndefined(time.index) ? $scope.index : time.index
-    $scope.get_data();
-  }
-
 })
 .filter('highlight', function() {
   return function(text) {

+ 0 - 34
panels/timepicker/editor.html

@@ -8,40 +8,6 @@
       <input type="text" class="input-small" ng-model="panel.timefield">
     </div>
   </div>
-  <div class="row-fluid">    
-    <h5>Index Settings</h5>
-    <div ng-show="panel.index_interval != 'none'" class="row-fluid"> 
-       <div class="span12">
-        <p class="small">
-          Time stamped indices use your selected time range to create a list of 
-          indices that match a specified timestamp pattern. This can be very 
-          efficient for some data sets (eg, logs) For example, to match the 
-          default logstash index pattern you might use 
-          <code>[logstash-]YYYY.MM.DD</code>. The [] in "[logstash-]" are 
-          important as they instruct Kibana not to treat those letters as a 
-          pattern.
-        </p>
-        <p class="small">
-          See <a href="http://momentjs.com/docs/#/displaying/format/">http://momentjs.com/docs/#/displaying/format/</a>
-          for documentation on date formatting.
-        </p>
-
-       </div>
-    </div>
-    <div class="row-fluid"> 
-      <div class="span2">
-        <h6>Timestamp</h6><select class="input-mini" ng-model="panel.index_interval" ng-options='f for f in ["none","hour","day","week","month","year"]'></select>
-      </div>
-      <div class="span5">
-        <h6>Index <span ng-show="panel.index_interval != 'none'">pattern <small>Absolutes in []</small></span></h6>
-        <input type="text" class="input-medium" ng-model="panel.index">
-      </div>
-      <div class="span4">
-        <h6>Failover Index <small>If index not found</small></h6>
-        <input type="text" class="input-medium" ng-model="panel.defaultindex">
-      </div>
-    </div>
-  </div>
   <div class="row-fluid">
     <h5>Relative mode <small>settings</small></h5>  
     <div class="span6">

+ 53 - 68
panels/timepicker/module.js

@@ -11,9 +11,6 @@
   * time_options :: An array of possible time options. Default: ['5m','15m','1h','6h','12h','24h','2d','7d','30d']
   * timespan :: The default options selected for the relative view. Default: '15m'
   * timefield :: The field in which time is stored in the document.
-  * index :: Index pattern to match. Literals should be double quoted. Default: '_all'
-  * defaultindex :: Index to failover to if index not found
-  * index_interval :: Time between timestamped indices (can be 'none') for static index
   * refresh: Object containing refresh parameters
     * enable :: true/false, enable auto refresh by default. Default: false
     * interval :: Seconds between auto refresh. Default: 30
@@ -28,7 +25,7 @@
 */
 
 angular.module('kibana.timepicker', [])
-.controller('timepicker', function($scope, eventBus, $timeout, timer, $http, kbnIndex) {
+.controller('timepicker', function($scope, $rootScope, eventBus, $timeout, timer, $http, dashboard, filterSrv) {
 
   // Set and populate defaults
   var _d = {
@@ -37,9 +34,6 @@ angular.module('kibana.timepicker', [])
     time_options  : ['5m','15m','1h','6h','12h','24h','2d','7d','30d'],
     timespan      : '15m',
     timefield     : '@timestamp',
-    index         : '_all',
-    defaultindex  : "_all",
-    index_interval: "none",
     timeformat    : "",
     group         : "default",
     refresh       : {
@@ -58,6 +52,7 @@ angular.module('kibana.timepicker', [])
     // unnecessary refreshes during changes
     $scope.refresh_interval = $scope.panel.refresh.interval
 
+
     // Init a private time object with Date() objects depending on mode
     switch($scope.panel.mode) {
       case 'absolute':
@@ -86,35 +81,20 @@ angular.module('kibana.timepicker', [])
     if ($scope.panel.refresh.enable)
       $scope.set_interval($scope.panel.refresh.interval);
 
-    // In the case that a panel is not ready to receive a time event, it may
-    // request one be sent by broadcasting a 'get_time' with its _id to its group
-    // This panel can handle multiple groups
-    eventBus.register($scope,"get_time", function(event,id) {
-      eventBus.broadcast($scope.$id,id,'time',compile_time($scope.time))
-    });
-
     // In case some other panel broadcasts a time, set us to an absolute range
-    eventBus.register($scope,"set_time", function(event,time) {
-      $scope.panel.mode = 'absolute';
-      set_timepicker(moment(time.from),moment(time.to))
-      $scope.time_apply()
-    });
-    
-    eventBus.register($scope,"zoom", function(event,factor) {
-      var _timespan = ($scope.time.to.valueOf() - $scope.time.from.valueOf());
-      try {
-        if($scope.panel.mode != 'absolute') {
-          $scope.panel.mode = 'since'
-          set_timepicker(moment($scope.time.to.valueOf() - _timespan*factor),$scope.time.to)
-        } else {
-          var _center = $scope.time.to.valueOf() - _timespan/2
-          set_timepicker(moment(_center - (_timespan*factor)/2),
-                         moment(_center + (_timespan*factor)/2))        
-        }
-      } catch (e) {
-        console.log(e)
-      }     
-      $scope.time_apply();
+    $scope.$on('refresh', function() {
+      var time = filterSrv.timeRange('min')
+
+      if($scope.time.from.diff(moment.utc(time.from)) != 0 
+        || $scope.time.to.diff(moment.utc(time.to)) != 0)
+      {
+        $scope.panel.mode = 'absolute';
+
+        // These 3 statements basicly do everything time_apply() does
+        set_timepicker(moment(time.from),moment(time.to))
+        $scope.time = $scope.time_calc();
+        update_panel()
+      }
     });
   }
 
@@ -146,10 +126,26 @@ angular.module('kibana.timepicker', [])
     }
   }
 
+  var update_panel = function() {
+    // Update panel's string representation of the time object.Don't update if
+    // we're in relative mode since we dont want to store the time object in the
+    // json for relative periods
+    if($scope.panel.mode !== 'relative') {
+      $scope.panel.time = { 
+        from : $scope.time.from.format("MM/DD/YYYY HH:mm:ss"),
+        to : $scope.time.to.format("MM/DD/YYYY HH:mm:ss"),
+      };
+    } else {
+      delete $scope.panel.time;
+    }
+  }
+
   $scope.set_mode = function(mode) {
     $scope.panel.mode = mode;
     $scope.panel.refresh.enable = mode === 'absolute' ? 
       false : $scope.panel.refresh.enable
+
+    update_panel();
   }
 
   $scope.to_now = function() {
@@ -201,45 +197,36 @@ angular.module('kibana.timepicker', [])
     };
   }
 
-  $scope.time_apply = function() {   
+  $scope.time_apply = function() { 
     $scope.panel.error = "";   
     // Update internal time object
+
+    // Remove all other time filters
+    filterSrv.removeByType('time')
+
     $scope.time = $scope.time_calc();
     $scope.time.field = $scope.panel.timefield
+    update_panel()
 
-    // Get indices for the time period, then broadcast time range and index list
-    // in a single object. Not sure if I like this.
-    if($scope.panel.index_interval !== 'none') {
-      kbnIndex.indices($scope.time.from,
-        $scope.time.to,
-        $scope.panel.index,
-        $scope.panel.index_interval
-      ).then(function (p) {
-        if(p.length > 0) {
-          $scope.time.index = p;
-          eventBus.broadcast($scope.$id,$scope.panel.group,'time',compile_time($scope.time))
-        } else {
-          $scope.panel.error = "Could not match index pattern to any ElasticSearch indices"
-        }
-      });
-    } else {
-      $scope.time.index = [$scope.panel.index];
-      eventBus.broadcast($scope.$id,$scope.panel.group,'time',compile_time($scope.time))
-    }
+    set_time_filter($scope.time)
+    dashboard.refresh();
 
-    // Update panel's string representation of the time object.Don't update if
-    // we're in relative mode since we dont want to store the time object in the
-    // json for relative periods
-    if($scope.panel.mode !== 'relative') {
-      $scope.panel.time = { 
-        from : $scope.time.from.format("MM/DD/YYYY HH:mm:ss"),
-        to : $scope.time.to.format("MM/DD/YYYY HH:mm:ss"),
-        index : $scope.time.index,
-      };
+  };
+
+
+  function set_time_filter(time) {
+    time.type = 'time'
+    // Check if there's a time filter we remember, if not, set one and remember it
+    if(!_.isUndefined($scope.panel.filter_id) && 
+      !_.isUndefined(filterSrv.list[$scope.panel.filter_id]) && 
+      filterSrv.list[$scope.panel.filter_id].type == 'time') 
+    {
+      filterSrv.set(compile_time(time),$scope.panel.filter_id)
     } else {
-      delete $scope.panel.time;
+      $scope.panel.filter_id = filterSrv.set(compile_time(time))
     }
-  };
+    return $scope.panel.filter_id;
+  }
 
   // Prefer to pass around Date() objects in the EventBus since interacting with
   // moment objects in libraries that are expecting Date()s can be tricky
@@ -247,8 +234,6 @@ angular.module('kibana.timepicker', [])
     time = _.clone(time)
     time.from = time.from.toDate()
     time.to   = time.to.toDate()
-    time.interval = $scope.panel.index_interval
-    time.pattern = $scope.panel.index 
     return time;
   }
 

+ 1 - 1
panels/trends/module.html

@@ -5,7 +5,7 @@
     <span ng-class="{'text-success': query.hits.new >= query.hits.old, 'text-error': query.hits.old > query.hits.new}" class='strong'>
       <i class='large' ng-class="{'icon-caret-up': query.hits.new >= query.hits.old, 'icon-caret-down': query.hits.old > query.hits.new}"></i> {{query.percent}}% 
     </span>
-    <span class="tiny pointer light" bs-tooltip="'Then: '+query.hits.old+', Now: '+query.hits.new" ng-show="query.label != ''">({{query.info.alias}})</span>
+    <span class="tiny pointer light" bs-tooltip="'Then: '+query.hits.old+', Now: '+query.hits.new" ng-show="query.info.alias != ''">({{query.info.alias}})</span>
     <br ng-show="panel.arrangement == 'vertical'">
   </div>
 </kibana-panel>         

+ 19 - 14
panels/trends/module.js

@@ -19,7 +19,7 @@
 
 */
 angular.module('kibana.trends', [])
-.controller('trends', function($scope, eventBus, kbnIndex, query) {
+.controller('trends', function($scope, eventBus, kbnIndex, query, dashboard, filterSrv) {
 
   // Set and populate defaults
   var _d = {
@@ -34,15 +34,12 @@ angular.module('kibana.trends', [])
 
   $scope.init = function () {
     $scope.hits = 0;
+
+    $scope.$on('refresh',function(){$scope.get_data()})
+
     eventBus.register($scope,'time', function(event,time){
       set_time(time)
     });
-    eventBus.register($scope,'query', function(event, query) {
-      $scope.panel.query = _.map(query,function(q) {
-        return {query: q, label: q};
-      })
-      $scope.get_data();
-    });
     // Now that we're all setup, request the time from our group
     eventBus.broadcast($scope.$id,$scope.panel.group,'get_time')
   }
@@ -52,8 +49,11 @@ angular.module('kibana.trends', [])
     $scope.panel.loading = true;
 
     // Make sure we have everything for the request to complete
-    if(_.isUndefined($scope.index) || _.isUndefined($scope.time))
+    if(dashboard.indices.length == 0) {
       return
+    } else {
+      $scope.index = dashboard.indices
+    }
 
     $scope.old_time = {
       from : new Date($scope.time.from.getTime() - interval_to_seconds($scope.panel.ago)*1000),
@@ -67,9 +67,8 @@ angular.module('kibana.trends', [])
     _.each(query.ids, function(id) {
       var q = $scope.ejs.FilteredQuery(
         ejs.QueryStringQuery(query.list[id].query || '*'),
-        ejs.RangeFilter($scope.time.field)
-          .from($scope.time.from)
-          .to($scope.time.to))
+        filterSrv.getBoolFilter(filterSrv.ids))
+
       request = request
         .facet($scope.ejs.QueryFacet(id)
           .query(q)
@@ -110,7 +109,6 @@ angular.module('kibana.trends', [])
     // Populate scope when we have results
     function process_results(results) { 
       results.then(function(results) {
-        console.log(results)
 
         $scope.panel.loading = false;
         if(_segment == 0) {
@@ -124,9 +122,17 @@ angular.module('kibana.trends', [])
           $scope.panel.error = $scope.parse_error(results.error);
           return;
         }
-        if($scope.query_id === query_id) {
+
+        // Convert facet ids to numbers
+        var facetIds = _.map(_.keys(results.facets),function(k){if(!isNaN(k)){return parseInt(k)}})
+
+        // Make sure we're still on the same query/queries
+        if($scope.query_id === query_id && 
+          _.intersection(facetIds,query.ids).length == query.ids.length
+          ) {
           var i = 0;
           _.each(query.ids, function(id) {
+            var v = results.facets[id]
             var n = results.facets[id].count
             var o = results.facets['old_'+id].count
 
@@ -193,7 +199,6 @@ angular.module('kibana.trends', [])
 
   function set_time(time) {
     $scope.time = time;
-    $scope.index = time.index || $scope.index
     $scope.get_data();
   }
 

+ 56 - 18
partials/dasheditor.html

@@ -12,9 +12,64 @@
       <label class="small"> Editable </label><input type="checkbox" ng-model="dashboard.current.editable" ng-checked="dashboard.current.editable" />
     </div>
   </div>
+  <div class="row-fluid">    
+    <h4>Index Settings</h4>
+    <div ng-show="dashboard.current.index.interval != 'none'" class="row-fluid"> 
+       <div class="span12">
+        <p class="small">
+          Time stamped indices use your selected time range to create a list of 
+          indices that match a specified timestamp pattern. This can be very 
+          efficient for some data sets (eg, logs) For example, to match the 
+          default logstash index pattern you might use 
+          <code>[logstash-]YYYY.MM.DD</code>. The [] in "[logstash-]" are 
+          important as they instruct Kibana not to treat those letters as a 
+          pattern.
+        </p>
+        <p class="small">
+          See <a href="http://momentjs.com/docs/#/displaying/format/">http://momentjs.com/docs/#/displaying/format/</a>
+          for documentation on date formatting.
+        </p>
+
+       </div>
+    </div>
+    <div class="row-fluid"> 
+      <div class="span3">
+        <h6>Timestamping</h6><select class="input-small" ng-model="dashboard.current.index.interval" ng-options='f for f in ["none","hour","day","week","month","year"]'></select>
+      </div>
+      <div class="span5">
+        <h6>Index <span ng-show="dashboard.current.index.interval != 'none'">pattern <small>Absolutes in []</small></span></h6>
+        <input type="text" class="input-medium" ng-model="dashboard.current.index.pattern">
+      </div>
+      <div class="span4">
+        <h6>Failover Index <small>If index not found</small></h6>
+        <input type="text" class="input-medium" ng-model="dashboard.current.index.default">
+      </div>
+    </div>
+  </div>
+  <hr/>
+  <h4>Rows</h4>
+  <div class="row-fluid">
+    <form>
+      <div class="span5">      
+        <label class="small">Title</label>
+        <input type="text" class="input-large" ng-model='row.title'></input>
+      </div>
+      <div class="span2">
+        <label class="small">Height</label>
+        <input type="text" class="input-mini" ng-model='row.height'></input>
+      </div>
+      <div class="span1">
+        <label class="small"> Editable </label>
+        <input type="checkbox" ng-model="row.editable" ng-checked="row.editable" />
+      </div>
+      <div class="span4">
+        <label>&nbsp</label>
+        <button ng-click="add_row(dashboard.current,row); reset_row();" class="btn btn-primary">Create Row</button>
+      </div>
+    </form>
+  </div>
   <div class="row-fluid">
     <div class="span12">
-      <h4>Rows</h4>
       <table class="table table-condensed table-striped">
         <thead>
           <th>Title</th>
@@ -30,23 +85,6 @@
       </table>
     </div>
   </div>
-  <h4>New row</h4>
-    <div class="row-fluid">
-      <div class="span8">      
-        <label class="small">Title</label>
-        <input type="text" class="input-large" ng-model='row.title'></input>
-      </div>
-      <div class="span2">
-        <label class="small">Height</label>
-        <input type="text" class="input-mini" ng-model='row.height'></input>
-      </div>
-      <div class="span1">
-        <label class="small"> Editable </label>
-        <input type="checkbox" ng-model="row.editable" ng-checked="row.editable" />
-      </div>
-    </div>
-    <button ng-click="add_row(dashboard.current,row); reset_row();" class="btn btn-primary">Create Row</button><br>
-  </div>
 </div>
 <div class="modal-footer">
   <button type="button" class="btn btn-success" ng-click="dismiss();reset_panel();">Close</button>