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

New timepicker, collapsable query and filter sections

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

+ 135 - 2
src/app/components/kbn.js

@@ -1,5 +1,5 @@
-define(['jquery', 'underscore'],
-function($, _) {
+define(['jquery', 'underscore','moment'],
+function($, _, moment) {
   'use strict';
   'use strict';
 
 
   var kbn = {};
   var kbn = {};
@@ -247,6 +247,139 @@ function($, _) {
     return new Date(new Date().getTime() - (kbn.interval_to_ms(string)));
     return new Date(new Date().getTime() - (kbn.interval_to_ms(string)));
   };
   };
 
 
+  /* This is a simplified version of elasticsearch's date parser */
+  kbn.parseDate = function(text) {
+    if(_.isDate(text)) {
+      return text;
+    }
+    var time,
+      mathString = "",
+      index,
+      parseString;
+    if (text.substring(0,3) === "now") {
+      time = new Date();
+      mathString = text.substring("now".length);
+    } else {
+      index = text.indexOf("||");
+      parseString;
+      if (index === -1) {
+        parseString = text;
+        mathString = ""; // nothing else
+      } else {
+        parseString = text.substring(0, index);
+        mathString = text.substring(index + 2);
+      }
+      // We're going to just require ISO8601 timestamps, k?
+      time = new Date(parseString);
+    }
+
+    if (!mathString.length) {
+      return time;
+    }
+
+    //return [time,parseString,mathString];
+    return kbn.parseDateMath(mathString, time);
+  };
+
+  kbn.parseDateMath = function(mathString, time, roundUp) {
+    var dateTime = moment(time);
+    for (var i = 0; i < mathString.length; ) {
+      var c = mathString.charAt(i++),
+        type,
+        num,
+        unit;
+      if (c === '/') {
+        type = 0;
+      } else if (c === '+') {
+        type = 1;
+      } else if (c === '-') {
+        type = 2;
+      } else {
+        console.log("no type");
+        return false;
+      }
+
+      if (isNaN(mathString.charAt(i))) {
+        num = 1;
+      } else {
+        var numFrom = i;
+        while (!isNaN(mathString.charAt(i))) {
+          i++;
+        }
+        num = parseInt(mathString.substring(numFrom, i),10);
+      }
+      if (type === 0) {
+        // rounding is only allowed on whole numbers
+        if (num !== 1) {
+          console.log("nbad rounding");
+          return false;
+        }
+      }
+      unit = mathString.charAt(i++);
+      switch (unit) {
+      case 'M':
+        if (type === 0) {
+          roundUp ? dateTime.endOf('month') : dateTime.startOf('month');
+        } else if (type === 1) {
+          dateTime.add('months',num);
+        } else if (type === 2) {
+          dateTime.subtract('months',num);
+        }
+        break;
+      case 'w':
+        if (type === 0) {
+          roundUp ? dateTime.endOf('week') : dateTime.startOf('week');
+        } else if (type === 1) {
+          dateTime.add('weeks',num);
+        } else if (type === 2) {
+          dateTime.subtract('weeks',num);
+        }
+        break;
+      case 'd':
+        if (type === 0) {
+          roundUp ? dateTime.endOf('day') : dateTime.startOf('day');
+        } else if (type === 1) {
+          dateTime.add('days',num);
+        } else if (type === 2) {
+          dateTime.subtract('days',num);
+        }
+        break;
+      case 'h':
+      case 'H':
+        if (type === 0) {
+          roundUp ? dateTime.endOf('hour') : dateTime.startOf('hour');
+        } else if (type === 1) {
+          dateTime.add('hours',num);
+        } else if (type === 2) {
+          dateTime.subtract('hours',num);
+        }
+        break;
+      case 'm':
+        if (type === 0) {
+          roundUp ? dateTime.endOf('minute') : dateTime.startOf('minute');
+        } else if (type === 1) {
+          dateTime.add('minutes',num);
+        } else if (type === 2) {
+          dateTime.subtract('minutes',num);
+        }
+        break;
+      case 's':
+        if (type === 0) {
+          roundUp ? dateTime.endOf('second') : dateTime.startOf('second');
+        } else if (type === 1) {
+          dateTime.add('seconds',num);
+        } else if (type === 2) {
+          dateTime.subtract('seconds',num);
+        }
+        break;
+      default:
+        console.log("unknown unit");
+        return false;
+      }
+    }
+    return dateTime.toDate();
+  };
+
   // LOL. hahahahaha. DIE.
   // LOL. hahahahaha. DIE.
   kbn.flatten_json = function(object,root,array) {
   kbn.flatten_json = function(object,root,array) {
     if (typeof array === 'undefined') {
     if (typeof array === 'undefined') {

+ 1 - 1
src/app/controllers/all.js

@@ -1,5 +1,5 @@
 define([
 define([
   './dash',
   './dash',
   './dashLoader',
   './dashLoader',
-  './row',
+  './row'
 ], function () {});
 ], function () {});

+ 3 - 0
src/app/controllers/row.js

@@ -16,6 +16,7 @@ function (angular, app, _) {
         collapsable: true,
         collapsable: true,
         editable: true,
         editable: true,
         panels: [],
         panels: [],
+        notice: false
       };
       };
 
 
       _.defaults($scope.row,_d);
       _.defaults($scope.row,_d);
@@ -34,6 +35,8 @@ function (angular, app, _) {
           $timeout(function() {
           $timeout(function() {
             $scope.$broadcast('render');
             $scope.$broadcast('render');
           });
           });
+        } else {
+          row.notice = false;
         }
         }
       };
       };
 
 

+ 1 - 0
src/app/directives/all.js

@@ -3,6 +3,7 @@ define([
   './arrayJoin',
   './arrayJoin',
   './dashUpload',
   './dashUpload',
   './kibanaPanel',
   './kibanaPanel',
+  './kibanaSimplePanel',
   './ngBlur',
   './ngBlur',
   './ngModelOnBlur',
   './ngModelOnBlur',
   './tip',
   './tip',

+ 53 - 0
src/app/directives/kibanaSimplePanel.js

@@ -0,0 +1,53 @@
+define([
+  'angular'
+],
+function (angular) {
+  'use strict';
+
+  angular
+    .module('kibana.directives')
+    .directive('kibanaSimplePanel', function($compile) {
+      return {
+        restrict: 'E',
+        link: function($scope, elem, attr) {
+          // once we have the template, scan it for controllers and
+          // load the module.js if we have any
+
+          // compile the module and uncloack. We're done
+          function loadModule($module) {
+            $module.appendTo(elem);
+            /* jshint indent:false */
+            $compile(elem.contents())($scope);
+            elem.removeClass("ng-cloak");
+          }
+
+          $scope.$watch(attr.type, function (name) {
+            elem.addClass("ng-cloak");
+
+            // load the panels module file, then render it in the dom.
+            $scope.require([
+              'jquery',
+              'text!panels/'+name+'/module.html'
+            ], function ($, moduleTemplate) {
+              var $module = $(moduleTemplate);
+              // top level controllers
+              var $controllers = $module.filter('ngcontroller, [ng-controller], .ng-controller');
+              // add child controllers
+              $controllers = $controllers.add($module.find('ngcontroller, [ng-controller], .ng-controller'));
+
+              if ($controllers.length) {
+                $scope.require([
+                  'panels/'+name+'/module'
+                ], function() {
+                  loadModule($module);
+                });
+              } else {
+                loadModule($module);
+              }
+            });
+          });
+        }
+      };
+    });
+
+});

+ 11 - 1
src/app/filters/all.js

@@ -1,4 +1,4 @@
-define(['angular', 'jquery', 'underscore'], function (angular, $, _) {
+define(['angular', 'jquery', 'underscore', 'moment'], function (angular, $, _, moment) {
   'use strict';
   'use strict';
 
 
   var module = angular.module('kibana.filters');
   var module = angular.module('kibana.filters');
@@ -42,6 +42,16 @@ define(['angular', 'jquery', 'underscore'], function (angular, $, _) {
     };
     };
   });
   });
 
 
+  module.filter('moment', function() {
+    return function(date,mode) {
+      switch(mode) {
+      case 'ago':
+        return moment(date).fromNow();
+      }
+      return moment(date).fromNow();
+    };
+  });
+
   module.filter('noXml', function() {
   module.filter('noXml', function() {
     var noXml = function(text) {
     var noXml = function(text) {
       return _.isString(text)
       return _.isString(text)

+ 1 - 1
src/app/panels/filtering/module.html

@@ -55,7 +55,7 @@
 
 
         <i class="filter-action pointer icon-remove" bs-tooltip="'Remove'" ng-click="remove(id)"></i>
         <i class="filter-action pointer icon-remove" bs-tooltip="'Remove'" ng-click="remove(id)"></i>
         <i class="filter-action pointer" ng-class="{'icon-check': filterSrv.list[id].active,'icon-check-empty': !filterSrv.list[id].active}" bs-tooltip="'Toggle'" ng-click="toggle(id)"></i>
         <i class="filter-action pointer" ng-class="{'icon-check': filterSrv.list[id].active,'icon-check-empty': !filterSrv.list[id].active}" bs-tooltip="'Toggle'" ng-click="toggle(id)"></i>
-        <i class="filter-action pointer icon-edit" ng-hide="filterSrv.list[id].editing" bs-tooltip="'Edit'" ng-click="filterSrv.list[id].editing = true"></i>
+        <i class="filter-action pointer icon-edit" ng-hide="filterSrv.list[id].editing || !isEditable(filterSrv.list[id])" bs-tooltip="'Edit'" ng-click="filterSrv.list[id].editing = true"></i>
 
 
       </div>
       </div>
 
 

+ 4 - 0
src/app/panels/filtering/module.js

@@ -27,6 +27,10 @@ function (angular, app, _) {
     };
     };
     _.defaults($scope.panel,_d);
     _.defaults($scope.panel,_d);
 
 
+    $scope.$on('filter', function() {
+      $scope.row.notice = true;
+    });
+
     $scope.init = function() {
     $scope.init = function() {
       $scope.filterSrv = filterSrv;
       $scope.filterSrv = filterSrv;
     };
     };

+ 4 - 4
src/app/panels/histogram/module.js

@@ -137,7 +137,7 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
      * @return {[type]} [description]
      * @return {[type]} [description]
      */
      */
     $scope.get_time_range = function () {
     $scope.get_time_range = function () {
-      var range = $scope.range = filterSrv.timeRange('min');
+      var range = $scope.range = filterSrv.timeRange('last');
       return range;
       return range;
     };
     };
 
 
@@ -289,7 +289,7 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
     // function $scope.zoom
     // function $scope.zoom
     // factor :: Zoom factor, so 0.5 = cuts timespan in half, 2 doubles timespan
     // factor :: Zoom factor, so 0.5 = cuts timespan in half, 2 doubles timespan
     $scope.zoom = function(factor) {
     $scope.zoom = function(factor) {
-      var _range = filterSrv.timeRange('min');
+      var _range = filterSrv.timeRange('last');
       var _timespan = (_range.to.valueOf() - _range.from.valueOf());
       var _timespan = (_range.to.valueOf() - _range.from.valueOf());
       var _center = _range.to.valueOf() - _timespan/2;
       var _center = _range.to.valueOf() - _timespan/2;
 
 
@@ -496,8 +496,8 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
         elem.bind("plotselected", function (event, ranges) {
         elem.bind("plotselected", function (event, ranges) {
           filterSrv.set({
           filterSrv.set({
             type  : 'time',
             type  : 'time',
-            from  : moment.utc(ranges.xaxis.from),
-            to    : moment.utc(ranges.xaxis.to),
+            from  : moment.utc(ranges.xaxis.from).toDate(),
+            to    : moment.utc(ranges.xaxis.to).toDate(),
             field : scope.panel.time_field
             field : scope.panel.time_field
           });
           });
         });
         });

+ 78 - 0
src/app/panels/timepicker2/custom.html

@@ -0,0 +1,78 @@
+  <div class="modal-body">
+    <style>
+      .timepicker-to-column {
+        margin-top: 10px;
+      }
+
+      .timepicker-input input {
+        outline: 0 !important;
+        border: 0px !important;
+        -webkit-box-shadow: 0;
+        -moz-box-shadow: 0;
+        box-shadow: 0;
+        position: relative;
+      }
+
+      .timepicker-input input::-webkit-outer-spin-button,
+      .timepicker-input input::-webkit-inner-spin-button {
+          -webkit-appearance: none;
+          margin: 0;
+      }
+
+      input.timepicker-date {
+        width: 90px;
+      }
+      input.timepicker-hms {
+        width: 20px;
+      }
+      input.timepicker-ms {
+        width: 25px;
+      }
+      div.timepicker-now {
+        float: right;
+      }
+    </style>
+
+    <div class="timepicker form-horizontal">
+        <form name="input">
+
+        <div class="timepicker-from-column">
+          <label class="small">From</label>
+          <div class="fake-input timepicker-input">
+            <input class="timepicker-date" type="text" ng-change="validate(temptime)" ng-model="temptime.from.date" data-date-format="mm/dd/yyyy" required bs-datepicker />@
+            <input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.from.hour" required ng-pattern="patterns.hour" onClick="this.select();"/>:
+            <input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.from.minute" required ng-pattern="patterns.minute" onClick="this.select();"/>:
+            <input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.from.second" required ng-pattern="patterns.second" onClick="this.select();"/>.
+            <input class="timepicker-ms" type="text" maxlength="3" ng-change="validate(temptime)" ng-model="temptime.from.millisecond" required ng-pattern="patterns.millisecond"  onClick="this.select();"/>
+          </div>
+        </div>
+
+        <div class="timepicker-to-column">
+
+          <label class="small">To (<a class="link" ng-class="{'strong':panel.now}" ng-click="setNow();panel.now=true">now</a>)</label>
+
+          <div class="fake-input timepicker-input">
+            <div ng-hide="panel.now">
+              <input class="timepicker-date" type="text" ng-change="validate(temptime)" ng-model="temptime.to.date" data-date-format="mm/dd/yyyy" required bs-datepicker />@
+              <input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.to.hour" required ng-pattern="patterns.hour" onClick="this.select();"/>:
+              <input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.to.minute" required ng-pattern="patterns.minute" onClick="this.select();"/>:
+              <input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.to.second" required ng-pattern="patterns.second" onClick="this.select();"/>.
+              <input class="timepicker-ms" type="text" maxlength="3" ng-change="validate(temptime)" ng-model="temptime.to.millisecond" required ng-pattern="patterns.millisecond" onClick="this.select();"/>
+            </div>
+            <span type="text" ng-show="panel.now" ng-disabled="panel.now">&nbsp <i class="pointer icon-remove-sign" ng-click="setNow();panel.now=false"></i> Right Now <input type="text" name="dummy" style="visibility:hidden" /></span>
+          </div>
+        </div>
+
+        </form>
+        <div class="clearfix"></div>
+    </div>
+  </div>
+
+  <div class="modal-footer">
+    <form name="input" style="margin-bottom:0">
+    <span class="" ng-hide="input.$valid">Invalid date or range</span>
+    <button ng-click="setAbsoluteTimeFilter(validate(temptime));dismiss();" ng-disabled="!input.$valid" class="btn btn-success">Apply</button>
+    <button ng-click="dismiss();" class="btn btn-danger">Cancel</button>
+
+    </form>
+  </div>

+ 10 - 0
src/app/panels/timepicker2/editor.html

@@ -0,0 +1,10 @@
+  <div class="row-fluid">
+    <div class="span6">
+      <label class="small">Relative time options <small>comma seperated</small></label>
+      <input type="text" array-join class="input-large" ng-model="panel.time_options">
+    </div>
+    <div class="span3">
+      <label class="small">Time Field</label>
+      <input type="text" class="input-small" ng-model="panel.timefield">
+    </div>
+  </div>

+ 39 - 0
src/app/panels/timepicker2/module.html

@@ -0,0 +1,39 @@
+<div ng-controller='timepicker2' ng-init="init()">
+  <style>
+    .timepicker-timestring {
+      font-weight: normal;
+    }
+
+    .timepicker-dropdown {
+      margin: 0px !important;
+      border: 0px !important;
+    }
+  </style>
+  <!--  This is a complete hack. The form actually exists in the modal, but due to transclusion
+        $scope.input isn't available on the controller unless the form element is in this file -->
+  <form name="input" style="margin:3px 0 0 0">
+    <ul class="nav nav-pills timepicker-dropdown">
+      <li class="dropdown" bs-tooltip="(time.from.date | date:'yyyy-MM-dd HH:mm:ss.sss') + ' to ' +(time.to.date | date:'yyyy-MM-dd HH:mm:ss.sss')" data-placement="bottom" ng-click="dismiss();">
+        <a class="dropdown-toggle timepicker-dropdown" data-toggle="dropdown" href="#">
+
+          <span class="small" ng-show="filterSrv.idsByType('time').length">
+            <span class="pointer" ng-hide="panel.now">{{time.from.date | date:'MMM d, y HH:mm:ss'}}</span>
+            <span class="pointer" ng-show="panel.now">{{time.from.date | moment:'ago'}}</span>
+            to
+            <span class="pointer" ng-hide="panel.now" >{{time.to.date | date:'MMM d, y HH:mm:ss'}}</span>
+            <span class="pointer" ng-show="panel.now">{{time.to.date | moment:'ago'}}</span>
+          </span>
+          <span ng-hide="filterSrv.idsByType('time').length">Set a time filter</span>
+          <i class="small icon-caret-down"></i>
+        </a>
+        <ul class="dropdown-menu">
+          <li ng-repeat='timespan in panel.time_options'>
+            <a ng-click="setRelativeFilter(timespan)">Last {{timespan}}</a>
+          </li>
+          <li><a ng-click="customTime()">Custom</a></li>
+        </ul>
+      </li>
+    </ul>
+
+  </form>
+</div>

+ 223 - 0
src/app/panels/timepicker2/module.js

@@ -0,0 +1,223 @@
+/*
+
+  ## Timepicker2
+
+  ### Parameters
+  * mode :: The default mode of the panel. Options: 'relative', 'absolute' 'since' Default: 'relative'
+  * 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.
+  * refresh: Object containing refresh parameters
+    * enable :: true/false, enable auto refresh by default. Default: false
+    * interval :: Seconds between auto refresh. Default: 30
+    * min :: The lowest interval a user may set
+*/
+define([
+  'angular',
+  'app',
+  'underscore',
+  'moment',
+  'kbn'
+],
+function (angular, app, _, moment, kbn) {
+  'use strict';
+
+  var module = angular.module('kibana.panels.timepicker2', []);
+  app.useModule(module);
+
+  module.controller('timepicker2', function($scope, $modal, $q, filterSrv) {
+    $scope.panelMeta = {
+      status  : "Stable",
+      description : "A panel for controlling the time range filters. If you have time based data, "+
+        " or if you're using time stamped indices, you need one of these"
+    };
+
+
+    // Set and populate defaults
+    var _d = {
+      status        : "Stable",
+      mode          : "relative",
+      time_options  : ['5m','15m','1h','6h','12h','24h','2d','7d','30d'],
+      timespan      : '15m',
+      timefield     : '@timestamp',
+      timeformat    : "",
+      refresh       : {
+        enable  : false,
+        interval: 30,
+        min     : 3
+      }
+    };
+    _.defaults($scope.panel,_d);
+
+    var customTimeModal = $modal({
+      template: './app/panels/timepicker2/custom.html',
+      persist: true,
+      show: false,
+      scope: $scope,
+      keyboard: false
+    });
+
+    $scope.filterSrv = filterSrv;
+
+    // ng-pattern regexs
+    $scope.patterns = {
+      date: /^[0-9]{2}\/[0-9]{2}\/[0-9]{4}$/,
+      hour: /^([01]?[0-9]|2[0-3])$/,
+      minute: /^[0-5][0-9]$/,
+      second: /^[0-5][0-9]$/,
+      millisecond: /^[0-9]*$/
+    };
+
+    $scope.$on('refresh', function(){$scope.init();});
+
+    $scope.init = function() {
+      var time = filterSrv.timeRange('last');
+      if(time) {
+        $scope.panel.now = filterSrv.timeRange(false).to === "now" ? true : false;
+        $scope.time = getScopeTimeObj(time.from,time.to);
+      }
+    };
+
+    $scope.customTime = function() {
+      // Assume the form is valid since we're setting it to something valid
+      $scope.input.$setValidity("dummy", true);
+      $scope.temptime = cloneTime($scope.time);
+
+      // Date picker needs the date to be at the start of the day
+      $scope.temptime.from.date.setHours(0,0,0,0);
+      $scope.temptime.to.date.setHours(0,0,0,0);
+
+      $q.when(customTimeModal).then(function(modalEl) {
+        modalEl.modal('show');
+      });
+    };
+
+    // Constantly validate the input of the fields. This function does not change any date variables
+    // outside of its own scope
+    $scope.validate = function(time) {
+      // Assume the form is valid. There is a hidden dummy input for invalidating it programatically.
+      $scope.input.$setValidity("dummy", true);
+
+      var _from = datepickerToLocal(time.from.date),
+        _to = datepickerToLocal(time.to.date),
+        _t = time;
+
+      if($scope.input.$valid) {
+
+        _from.setHours(_t.from.hour,_t.from.minute,_t.from.second,_t.from.millisecond);
+        _to.setHours(_t.to.hour,_t.to.minute,_t.to.second,_t.to.millisecond);
+
+        // Check that the objects are valid and to is after from
+        if(isNaN(_from.getTime()) || isNaN(_to.getTime()) || _from.getTime() >= _to.getTime()) {
+          $scope.input.$setValidity("dummy", false);
+          return false;
+        }
+      } else {
+        return false;
+      }
+
+      return {from:_from,to:_to};
+    };
+
+    $scope.setNow = function() {
+      $scope.time.to = getTimeObj(new Date());
+    };
+
+    /*
+      time : {
+        from: Date
+        to: Date
+      }
+    */
+    $scope.setAbsoluteTimeFilter = function (time) {
+
+      // Create filter object
+      var _filter = _.clone(time);
+
+      _filter.type = 'time';
+      _filter.field = $scope.panel.timefield;
+
+      if($scope.panel.now) {
+        _filter.to = "now";
+      }
+
+      // Clear all time filters, set a new one
+      filterSrv.removeByType('time',true);
+
+      // Set the filter
+      $scope.panel.filter_id = filterSrv.set(_filter);
+
+      // Update our representation
+      $scope.time = getScopeTimeObj(time.from,time.to);
+
+      return $scope.panel.filter_id;
+    };
+
+    $scope.setRelativeFilter = function(timespan) {
+
+      $scope.panel.now = true;
+      // Create filter object
+      var _filter = {
+        type : 'time',
+        field : $scope.panel.timefield,
+        from : "now-"+timespan,
+        to: "now"
+      };
+
+      // Clear all time filters, set a new one
+      filterSrv.removeByType('time',true);
+
+      // Set the filter
+      $scope.panel.filter_id = filterSrv.set(_filter);
+
+      // Update our representation
+      $scope.time = getScopeTimeObj(kbn.parseDate(_filter.from),new Date());
+
+      return $scope.panel.filter_id;
+    };
+
+    var pad = function(n, width, z) {
+      z = z || '0';
+      n = n + '';
+      return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n;
+    };
+
+    var cloneTime = function(time) {
+      var _n = {
+        from: _.clone(time.from),
+        to: _.clone(time.to)
+      };
+      // Create new dates as _.clone is shallow.
+      _n.from.date = new Date(_n.from.date);
+      _n.to.date = new Date(_n.to.date);
+      return _n;
+    };
+
+    var getScopeTimeObj = function(from,to) {
+      return {
+        from: getTimeObj(from),
+        to: getTimeObj(to)
+      };
+    };
+
+    var getTimeObj = function(date) {
+      return {
+        date: new Date(date),
+        hour: pad(date.getHours(),2),
+        minute: pad(date.getMinutes(),2),
+        second: pad(date.getSeconds(),2),
+        millisecond: pad(date.getMilliseconds(),3)
+      };
+    };
+
+    // Do not use the results of this function unless you plan to use setHour/Minutes/etc on the result
+    var datepickerToLocal = function(date) {
+      console.log(date);
+      date = moment(date).clone().toDate();
+      console.log(moment(new Date(date.getTime() + date.getTimezoneOffset() * 60000)).toDate());
+      return moment(new Date(date.getTime() + date.getTimezoneOffset() * 60000)).toDate();
+    };
+
+
+  });
+});

+ 5 - 0
src/app/panels/timepicker2/refreshctrl.html

@@ -0,0 +1,5 @@
+<form name="refreshPopover" class='form-inline input-append' style="margin:0px">
+    <label><small>Interval (seconds)</small></label><br>
+    <input type="number" class="input-mini" ng-model="refresh_interval">
+    <button type="button" class="btn" ng-click="set_interval(refresh_interval);dismiss()"><i class="icon-ok"></i></button>
+</form>

+ 2 - 1
src/app/panels/trends/module.js

@@ -87,7 +87,8 @@ function (angular, app, _, kbn) {
         timeField = timeField[0];
         timeField = timeField[0];
       }
       }
 
 
-      $scope.time = filterSrv.timeRange('min');
+      // This logic can be simplifie greatly with the new kbn.parseDate
+      $scope.time = filterSrv.timeRange('last');
       $scope.old_time = {
       $scope.old_time = {
         from : new Date($scope.time.from.getTime() - kbn.interval_to_ms($scope.panel.ago)),
         from : new Date($scope.time.from.getTime() - kbn.interval_to_ms($scope.panel.ago)),
         to   : new Date($scope.time.to.getTime() - kbn.interval_to_ms($scope.panel.ago))
         to   : new Date($scope.time.to.getTime() - kbn.interval_to_ms($scope.panel.ago))

+ 1 - 0
src/app/partials/dashLoader.html

@@ -1,3 +1,4 @@
+<li ng-controller="RowCtrl"><kibana-simple-panel type="'timepicker2'" ng-cloak></kibana-simple-panel></li>
 
 
 <li><a bs-tooltip="'Goto saved default'" data-placement="bottom" href='#/dashboard'><i class='icon-home'></i></a></li>
 <li><a bs-tooltip="'Goto saved default'" data-placement="bottom" href='#/dashboard'><i class='icon-home'></i></a></li>
 
 

+ 77 - 61
src/app/partials/dashboard.html

@@ -1,77 +1,93 @@
-<div class="row-fluid container" style="margin-top:10px; width:98%">
+<!-- is there a better way to repeat without actually affecting the page? -->
+<nil ng-repeat="row in dashboard.current.pulldowns" ng-controller="RowCtrl">
+  <div class="top-row-open" ng-hide="row.collapse">
+    <kibana-simple-panel type="row.type" ng-cloak></kibana-simple-panel>
+  </div>
+  <div class="top-row-close pointer" ng-click="toggle_row(row);dismiss();" bs-tooltip="'Toggle '+row.type" data-placement="bottom">
+    <span class="small row-text">{{row.type}}</span>
+    <i class="small" ng-class="{'icon-expand':row.collapse,'icon-collapse-top':!row.collapse}"></i>
+    <i class="small icon-circle text-warning" ng-show="row.notice && row.collapse"></i>
+  </div>
+</nil>
 
 
-  <!-- Rows -->
-  <div class="row-fluid kibana-row" ng-controller="RowCtrl" ng-repeat="(row_name, row) in dashboard.current.rows" ng-style="row_style(row)">
-    <div class="row-control">
-      <div class="row-fluid" style="padding:0px;margin:0px;position:relative;">
+<div class="container-fluid main">
+  <div class="row-fluid">
+    <div class="row-fluid container" style="margin-top:10px; width:98%">
+      <!-- Rows -->
+      <div class="row-fluid kibana-row" ng-controller="RowCtrl" ng-repeat="(row_name, row) in dashboard.current.rows" ng-style="row_style(row)">
+        <div class="row-control">
+          <div class="row-fluid" style="padding:0px;margin:0px;position:relative;">
 
 
-        <div class="row-close span12" ng-show="row.collapse" data-placement="bottom" >
-          <span class="row-button" bs-modal="'app/partials/roweditor.html'" class="pointer">
-            <i bs-tooltip="'Configure row'" data-placement="right" ng-show="row.editable" class="icon-cog pointer"></i>
-          </span>
-          <span class="row-button" ng-click="toggle_row(row)" ng-show="row.collapsable">
-            <i bs-tooltip="'Expand row'" data-placement="right" ng-show="row.editable" class="icon-expand pointer" ></i>
-          </span>
-          <span class="row-button row-text" ng-click="toggle_row(row)" ng-class="{'pointer':row.collapsable}">{{row.title || 'Row '+$index}}</span>
-        </div>
+            <div class="row-close span12" ng-show="row.collapse" data-placement="bottom" >
+              <span class="row-button" bs-modal="'app/partials/roweditor.html'" class="pointer">
+                <i bs-tooltip="'Configure row'" data-placement="right" ng-show="row.editable" class="icon-cog pointer"></i>
+              </span>
+              <span class="row-button" ng-click="toggle_row(row)" ng-show="row.collapsable">
+                <i bs-tooltip="'Expand row'" data-placement="right" ng-show="row.editable" class="icon-expand pointer" ></i>
+              </span>
+              <span class="row-button row-text" ng-click="toggle_row(row)" ng-class="{'pointer':row.collapsable}">{{row.title || 'Row '+$index}}</span>
+            </div>
 
 
-        <div style="text-align:left" class="row-open" ng-show="!row.collapse">
-          <span ng-show="row.collapsable">
-            <i bs-tooltip="'Hide row'" data-placement="right"  class="icon-collapse-top" ng-click="toggle_row(row)"></i>
-            <br>
-          </span>
-          <span bs-modal="'app/partials/roweditor.html'" ng-show="row.editable">
-            <i bs-tooltip="'Configure row'" data-placement="right"  class="icon-cog pointer"></i>
-            <br>
-          </span>
-          <span ng-show="rowSpan(row) == 12 && row.editable">
-            <i bs-tooltip="'Row full. Create a new row to add more panels'" data-placement="right" class="icon-trello"></i>
-            <br>
-          </span>
-          <span ng-show="rowSpan(row) > 12">
-            <i bs-tooltip="'Total span > 12. This row may format poorly'" data-placement="right" class="icon-warning-sign text-warning"></i>
-            <br>
-          </span>
-        </div>
+            <div style="text-align:left" class="row-open" ng-show="!row.collapse">
+              <span ng-show="row.collapsable">
+                <i bs-tooltip="'Hide row'" data-placement="right"  class="icon-collapse-top" ng-click="toggle_row(row)"></i>
+                <br>
+              </span>
+              <span bs-modal="'app/partials/roweditor.html'" ng-show="row.editable">
+                <i bs-tooltip="'Configure row'" data-placement="right"  class="icon-cog pointer"></i>
+                <br>
+              </span>
+              <span ng-show="rowSpan(row) == 12 && row.editable">
+                <i bs-tooltip="'Row full. Create a new row to add more panels'" data-placement="right" class="icon-trello"></i>
+                <br>
+              </span>
+              <span ng-show="rowSpan(row) > 12">
+                <i bs-tooltip="'Total span > 12. This row may format poorly'" data-placement="right" class="icon-warning-sign text-warning"></i>
+                <br>
+              </span>
+            </div>
 
 
-      </div>
-      <div class="row-fluid" style="padding-top:0px" ng-if="!row.collapse">
+          </div>
+          <div class="row-fluid" style="padding-top:0px" ng-if="!row.collapse">
+
+            <!-- Panels -->
+            <div ng-repeat="(name, panel) in row.panels|filter:isPanel" ng-hide="panel.span == 0 || panel.hide" class="span{{panel.span}} panel nospace" style="min-height:{{row.height}}; position:relative" data-drop="true" ng-model="row.panels" data-jqyoui-options jqyoui-droppable="{index:$index,mutate:false,onDrop:'panelMoveDrop',onOver:'panelMoveOver(true)',onOut:'panelMoveOut'}">
+              <!-- Error Panel -->
+              <div class="row-fluid">
+                <div class="span12 alert alert-error panel-error" ng-hide="!panel.error">
+                  <a class="close" ng-click="panel.error=false">&times;</a>
+                  <i class="icon-exclamation-sign"></i> <strong>Oops!</strong> {{panel.error}}
+                </div>
+              </div>
 
 
-        <!-- Panels -->
-        <div ng-repeat="(name, panel) in row.panels|filter:isPanel" ng-hide="panel.span == 0 || panel.hide" class="span{{panel.span}} panel nospace" style="min-height:{{row.height}}; position:relative" data-drop="true" ng-model="row.panels" data-jqyoui-options jqyoui-droppable="{index:$index,mutate:false,onDrop:'panelMoveDrop',onOver:'panelMoveOver(true)',onOut:'panelMoveOut'}">
-          <!-- Error Panel -->
-          <div class="row-fluid">
-            <div class="span12 alert alert-error panel-error" ng-hide="!panel.error">
-              <a class="close" ng-click="panel.error=false">&times;</a>
-              <i class="icon-exclamation-sign"></i> <strong>Oops!</strong> {{panel.error}}
+              <!-- Content Panel -->
+              <div class="row-fluid" style="position:relative" ng-class="{'dragInProgress':dashboard.panelDragging}" >
+                <kibana-panel type="panel.type" ng-cloak></kibana-panel>
+              </div>
             </div>
             </div>
-          </div>
-          <!-- Content Panel -->
-          <div class="row-fluid" style="position:relative" ng-class="{'dragInProgress':dashboard.panelDragging}" >
-            <kibana-panel type="panel.type" ng-cloak></kibana-panel>
-          </div>
-        </div>
 
 
-        <div ng-hide="(12-rowSpan(row)) < 1 || !dashboard.current.panel_hints" class="panel span{{(12-rowSpan(row))}}" ng-class="{'dragInProgress':dashboard.panelDragging}" style="height:{{row.height}};" data-drop="true" ng-model="row.panels" data-jqyoui-options jqyoui-droppable="{index:row.panels.length,mutate:false,onDrop:'panelMoveDrop',onOver:'panelMoveOver',onOut:'panelMoveOut'}">
+            <div ng-hide="(12-rowSpan(row)) < 1 || !dashboard.current.panel_hints" class="panel span{{(12-rowSpan(row))}}" ng-class="{'dragInProgress':dashboard.panelDragging}" style="height:{{row.height}};" data-drop="true" ng-model="row.panels" data-jqyoui-options jqyoui-droppable="{index:row.panels.length,mutate:false,onDrop:'panelMoveDrop',onOver:'panelMoveOver',onOut:'panelMoveOut'}">
 
 
-          <span bs-modal="'app/partials/roweditor.html'" ng-show="row.editable && !dashboard.panelDragging">
-            <i ng-hide="rowSpan(row) == 0" class="pointer icon-plus-sign" ng-click="editor.index = 2" bs-tooltip="'Add a panel to this row'" data-placement="right"></i>
-            <span ng-click="editor.index = 2" style="margin-top: 8px; margin-left: 3px" ng-show="rowSpan(row) == 0" class="btn btn-mini">Add panel to empty row</btn>
-          </span>
+              <span bs-modal="'app/partials/roweditor.html'" ng-show="row.editable && !dashboard.panelDragging">
+                <i ng-hide="rowSpan(row) == 0" class="pointer icon-plus-sign" ng-click="editor.index = 2" bs-tooltip="'Add a panel to this row'" data-placement="right"></i>
+                <span ng-click="editor.index = 2" style="margin-top: 8px; margin-left: 3px" ng-show="rowSpan(row) == 0" class="btn btn-mini">Add panel to empty row</btn>
+              </span>
 
 
+            </div>
+
+          </div>
         </div>
         </div>
+      </div>
 
 
+      <div class="row-fluid" ng-show='dashboard.current.editable'>
+        <div class="span12" style="text-align:right;">
+          <span style="margin-left: 0px;" class="pointer btn btn-mini" bs-modal="'app/partials/dasheditor.html'">
+            <span ng-click="editor.index = 2"><i class="icon-plus-sign"></i> ADD A ROW</span>
+          </span>
+        </div>
       </div>
       </div>
-    </div>
-  </div>
 
 
-  <div class="row-fluid" ng-show='dashboard.current.editable'>
-    <div class="span12" style="text-align:right;">
-      <span style="margin-left: 0px;" class="pointer btn btn-mini" bs-modal="'app/partials/dasheditor.html'">
-        <span ng-click="editor.index = 2"><i class="icon-plus-sign"></i> ADD A ROW</span>
-      </span>
+
     </div>
     </div>
   </div>
   </div>
-
-
 </div>
 </div>

+ 10 - 2
src/app/services/dashboard.js

@@ -25,6 +25,14 @@ function (angular, $, kbn, _, config, moment, Modernizr) {
       failover: false,
       failover: false,
       panel_hints: true,
       panel_hints: true,
       rows: [],
       rows: [],
+      pulldowns: [
+        {
+          type: 'query',
+        },
+        {
+          type: 'filtering'
+        }
+      ],
       services: {},
       services: {},
       loader: {
       loader: {
         save_gist: false,
         save_gist: false,
@@ -109,7 +117,7 @@ function (angular, $, kbn, _, config, moment, Modernizr) {
     this.refresh = function() {
     this.refresh = function() {
       if(self.current.index.interval !== 'none') {
       if(self.current.index.interval !== 'none') {
         if(filterSrv.idsByType('time').length > 0) {
         if(filterSrv.idsByType('time').length > 0) {
-          var _range = filterSrv.timeRange('min');
+          var _range = filterSrv.timeRange('last');
           kbnIndex.indices(_range.from,_range.to,
           kbnIndex.indices(_range.from,_range.to,
             self.current.index.pattern,self.current.index.interval
             self.current.index.pattern,self.current.index.interval
           ).then(function (p) {
           ).then(function (p) {
@@ -175,7 +183,7 @@ function (angular, $, kbn, _, config, moment, Modernizr) {
       filterSrv.init();
       filterSrv.init();
 
 
       // If there's an index interval set and no existing time filter, send a refresh to set one
       // If there's an index interval set and no existing time filter, send a refresh to set one
-      if(dashboard.index.interval !== 'none' && filterSrv.idsByType('time').length === 0) {
+      if(dashboard.index.interval !== 'none') {
         self.refresh();
         self.refresh();
       }
       }
 
 

+ 27 - 20
src/app/services/filterSrv.js

@@ -1,8 +1,9 @@
 define([
 define([
   'angular',
   'angular',
   'underscore',
   'underscore',
-  'config'
-], function (angular, _, config) {
+  'config',
+  'kbn'
+], function (angular, _, config, kbn) {
   'use strict';
   'use strict';
 
 
   var module = angular.module('kibana.services');
   var module = angular.module('kibana.services');
@@ -35,10 +36,13 @@ define([
       self.ids = dashboard.current.services.filter.ids;
       self.ids = dashboard.current.services.filter.ids;
       _f = dashboard.current.services.filter;
       _f = dashboard.current.services.filter;
 
 
+      // Date filters hold strings now, not dates
+      /*
       _.each(self.getByType('time',true),function(time) {
       _.each(self.getByType('time',true),function(time) {
         self.list[time.id].from = new Date(time.from);
         self.list[time.id].from = new Date(time.from);
         self.list[time.id].to = new Date(time.to);
         self.list[time.id].to = new Date(time.to);
       });
       });
+      */
 
 
     };
     };
 
 
@@ -78,6 +82,7 @@ define([
           dashboard.refresh();
           dashboard.refresh();
         },0);
         },0);
       }
       }
+      $rootScope.$broadcast('filter');
       return _r;
       return _r;
     };
     };
 
 
@@ -101,6 +106,7 @@ define([
           dashboard.refresh();
           dashboard.refresh();
         },0);
         },0);
       }
       }
+      $rootScope.$broadcast('filter');
       return _r;
       return _r;
     };
     };
 
 
@@ -150,10 +156,11 @@ define([
       switch(filter.type)
       switch(filter.type)
       {
       {
       case 'time':
       case 'time':
-        return ejs.RangeFilter(filter.field)
-          .from(filter.from.valueOf())
-          //.from("now-1d")
-          .to(filter.to.valueOf());
+        var _f = ejs.RangeFilter(filter.field).from(kbn.parseDate(filter.from).valueOf());
+        if(!_.isUndefined(filter.to)) {
+          _f = _f.to(filter.to.valueOf());
+        }
+        return _f;
       case 'range':
       case 'range':
         return ejs.RangeFilter(filter.field)
         return ejs.RangeFilter(filter.field)
           .from(filter.from)
           .from(filter.from)
@@ -187,26 +194,26 @@ define([
       return _.pluck(self.getByType('time'),'field');
       return _.pluck(self.getByType('time'),'field');
     };
     };
 
 
-    // This special function looks for all time filters, and returns a time range according to the mode
-    // No idea when max would actually be used
-    this.timeRange = function(mode) {
-      var _t = _.where(self.list,{type:'time',active:true});
-      if(_t.length === 0) {
+    // Parse is used when you need to know about the raw filter
+    this.timeRange = function(parse) {
+      var _t = _.last(_.where(self.list,{type:'time',active:true}));
+      if(_.isUndefined(_t)) {
         return false;
         return false;
       }
       }
-      switch(mode) {
-      case "min":
+      if(parse === false) {
         return {
         return {
-          from: new Date(_.max(_.pluck(_t,'from'))),
-          to: new Date(_.min(_.pluck(_t,'to')))
+          from: _t.from,
+          to: _t.to
         };
         };
-      case "max":
+      } else {
+        var
+          _from = _t.from,
+          _to = _t.to || new Date();
+
         return {
         return {
-          from: new Date(_.min(_.pluck(_t,'from'))),
-          to: new Date(_.max(_.pluck(_t,'to')))
+          from : kbn.parseDate(_from),
+          to : kbn.parseDate(_to)
         };
         };
-      default:
-        return false;
       }
       }
     };
     };
 
 

+ 1 - 1
src/config.js

@@ -51,4 +51,4 @@ function (Settings) {
       'terms'
       'terms'
     ]
     ]
   });
   });
-});
+});

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
src/css/bootstrap.dark.min.css


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
src/css/bootstrap.light.min.css


+ 4 - 6
src/index.html

@@ -34,16 +34,14 @@
       <div class="navbar-inner">
       <div class="navbar-inner">
         <div class="container-fluid">
         <div class="container-fluid">
           <span class="brand">{{dashboard.current.title}}</span>
           <span class="brand">{{dashboard.current.title}}</span>
+
           <ul class="nav pull-right" ng-controller='dashLoader' ng-init="init()" ng-include="'app/partials/dashLoader.html'">
           <ul class="nav pull-right" ng-controller='dashLoader' ng-init="init()" ng-include="'app/partials/dashLoader.html'">
           </ul>
           </ul>
         </div>
         </div>
       </div>
       </div>
     </div>
     </div>
-    <div class="container-fluid main">
-      <div class="row-fluid">
-        <div ng-view></div>
-      </div>
-    </div>
-  </body>
 
 
+    <div ng-view></div>
+
+  </body>
 </html>
 </html>

+ 30 - 4
src/vendor/bootstrap/less/overrides.less

@@ -16,6 +16,10 @@ div.fake-input {
   .border-radius(@inputBorderRadius @inputBorderRadius @inputBorderRadius @inputBorderRadius);
   .border-radius(@inputBorderRadius @inputBorderRadius @inputBorderRadius @inputBorderRadius);
 }
 }
 
 
+form input.ng-invalid {
+  color: @errorText;
+}
+
 .editor-title {
 .editor-title {
   margin-right: 10px;
   margin-right: 10px;
   font-size: 1.7em;
   font-size: 1.7em;
@@ -80,6 +84,22 @@ div.fake-input {
   font-weight: 200;
   font-weight: 200;
 }
 }
 
 
+.top-row-open {
+  background: darken(@bodyBackground, 3%);
+  padding: 5px 25px 5px 25px;
+}
+
+.top-row-close {
+  outline: 1px solid darken(@bodyBackground, 10%);
+  border-top: 1px solid lighten(@bodyBackground, 10%);
+  padding: 0px;
+  margin: 1px 0px 0px 0px;
+  text-align: center;
+  min-height: 16px !important;
+  line-height: 16px;
+  background: darken(@bodyBackground, 3%);
+}
+
 .row-open i {
 .row-open i {
   font-size: 10pt;
   font-size: 10pt;
 }
 }
@@ -146,9 +166,15 @@ div.fake-input {
 }
 }
 
 
 .link {
 .link {
+  color: @linkColor;
   cursor: pointer;
   cursor: pointer;
 }
 }
 
 
+.link:hover {
+  color: @linkColorHover;
+}
+
+
 .pointer:hover {
 .pointer:hover {
   color: @linkColorHover;
   color: @linkColorHover;
 }
 }
@@ -157,10 +183,6 @@ div.fake-input {
   cursor: pointer;
   cursor: pointer;
 }
 }
 
 
-.link:hover {
-  color: @linkColorHover;
-}
-
 .popover {
 .popover {
   max-width: 480px;
   max-width: 480px;
 }
 }
@@ -191,6 +213,10 @@ div.fake-input {
   font-weight: bold;
   font-weight: bold;
 }
 }
 
 
+a {
+  cursor: pointer;
+}
+
 .normal {
 .normal {
   font-weight: normal;
   font-weight: normal;
 }
 }

Некоторые файлы не были показаны из-за большого количества измененных файлов