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

Merge branch 'master' into pro

Torkel Ödegaard 11 лет назад
Родитель
Сommit
d2f21bc93e
51 измененных файлов с 716 добавлено и 373 удалено
  1. 5 0
      CHANGELOG.md
  2. 1 0
      src/app/controllers/all.js
  3. 41 19
      src/app/controllers/loginCtrl.js
  4. 104 0
      src/app/controllers/sidemenuCtrl.js
  5. 27 4
      src/app/directives/dashEditLink.js
  6. 15 0
      src/app/directives/tip.js
  7. 29 0
      src/app/features/account/accountCtrl.js
  8. 3 13
      src/app/features/account/accountUsersCtrl.js
  9. 1 6
      src/app/features/account/apiKeysCtrl.js
  10. 37 0
      src/app/features/account/partials/account.html
  11. 1 1
      src/app/features/account/partials/apikeys.html
  12. 1 1
      src/app/features/account/partials/datasources.html
  13. 2 2
      src/app/features/account/partials/users.html
  14. 1 0
      src/app/features/all.js
  15. 43 0
      src/app/features/dashboard/partials/panelTime.html
  16. 12 3
      src/app/features/dashboard/timeSrv.js
  17. 2 1
      src/app/features/dashboard/viewStateSrv.js
  18. 1 0
      src/app/features/graphite/datasource.js
  19. 1 1
      src/app/features/graphite/queryCtrl.js
  20. 2 9
      src/app/features/influxdb/datasource.js
  21. 47 42
      src/app/features/profile/partials/profile.html
  22. 2 3
      src/app/features/profile/profileCtrl.js
  23. 2 2
      src/app/features/templating/templateValuesSrv.js
  24. 3 3
      src/app/panels/graph/graph.tooltip.js
  25. 4 0
      src/app/panels/graph/module.html
  26. 24 0
      src/app/panels/graph/module.js
  27. 42 16
      src/app/panels/timepicker/editor.html
  28. 11 3
      src/app/panels/timepicker/module.js
  29. 1 1
      src/app/partials/dashboard.html
  30. 15 49
      src/app/partials/dashboard_topnav.html
  31. 1 1
      src/app/partials/dasheditor.html
  32. 0 36
      src/app/partials/loadmetrics.html
  33. 30 38
      src/app/partials/login.html
  34. 6 3
      src/app/partials/navbar.html
  35. 1 1
      src/app/partials/roweditor.html
  36. 23 43
      src/app/partials/sidemenu.html
  37. 2 2
      src/app/partials/templating_editor.html
  38. 4 0
      src/app/routes/backend/all.js
  39. 1 0
      src/app/routes/backend/dashboard.js
  40. 1 1
      src/css/less/bootswatch.dark.less
  41. 1 1
      src/css/less/bootswatch.light.less
  42. 10 0
      src/css/less/grafana.less
  43. 11 2
      src/css/less/graph.less
  44. 0 1
      src/css/less/overrides.less
  45. 115 54
      src/css/less/p_pro.less
  46. 2 2
      src/css/less/panel.less
  47. 6 6
      src/css/less/variables.dark.less
  48. 1 0
      src/test/specs/graph-tooltip-specs.js
  49. 7 0
      src/test/specs/graphiteDatasource-specs.js
  50. 3 3
      src/test/specs/influxdb-datasource-specs.js
  51. 11 0
      src/test/specs/kbn-format-specs.js

+ 5 - 0
CHANGELOG.md

@@ -2,6 +2,8 @@
 
 **New features**
 - [Issue #1331](https://github.com/grafana/grafana/issues/1331). Graph & Singlestat: New axis/unit format selector and more units (kbytes, Joule, Watt, eV), and new design for graph axis & grid tab and single stat options tab views
+- [Issue #1241](https://github.com/grafana/grafana/issues/1242). Timepicker: New option in timepicker (under dashboard settings), to change ``now`` to be for example ``now-1m``, usefull when you want to ignore last minute because it contains incomplete data
+- [Issue #171](https://github.com/grafana/grafana/issues/171).   Panel: Different time periods, panels can override dashboard relative time and/or add a time shift
 
 **Enhancements**
 - [Issue #1297](https://github.com/grafana/grafana/issues/1297). Graphite: Added cumulative and minimumBelow graphite functions
@@ -13,6 +15,9 @@
 - [Issue #1298](https://github.com/grafana/grafana/issues/1298). InfluxDB: Fix handling of empty array in templating variable query
 - [Issue #1309](https://github.com/grafana/grafana/issues/1309). Graph: Fixed issue when using zero as a grid threshold
 - [Issue #1345](https://github.com/grafana/grafana/issues/1345). UI: Fixed position of confirm modal when scrolled down
+- [Issue #1372](https://github.com/grafana/grafana/issues/1372). Graphite: Fix for nested complex queries, where a query references a query that references another query (ie the #[A-Z] syntax)
+- [Issue #1363](https://github.com/grafana/grafana/issues/1363). Templating: Fix to allow custom template variables to contain white space, now only splits on ','
+- [Issue #1359](https://github.com/grafana/grafana/issues/1359). Graph: Fix for all series tooltip showing series with all null values when ``Hide Empty`` option is enabled
 
 **Tech**
 - [Issue #1311](https://github.com/grafana/grafana/issues/1311). Tech: Updated Font-Awesome from 3.2 to 4.2

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

@@ -7,5 +7,6 @@ define([
   './inspectCtrl',
   './jsonEditorCtrl',
   './loginCtrl',
+  './sidemenuCtrl',
   './errorCtrl',
 ], function () {});

+ 41 - 19
src/app/controllers/loginCtrl.js

@@ -8,15 +8,27 @@ function (angular, config) {
   var module = angular.module('grafana.controllers');
 
   module.controller('LoginCtrl', function($scope, backendSrv, $location, $routeParams, alertSrv) {
-    $scope.loginModel = {
+
+    $scope.formModel = {
       user: '',
-      password: ''
+      email: '',
+      password: '',
     };
 
-    $scope.newUser = {};
-
     $scope.grafana.sidemenu = false;
-    $scope.mode = 'login';
+    $scope.loginMode = true;
+    $scope.submitBtnClass = 'btn-inverse';
+    $scope.submitBtnText = 'Log in';
+    $scope.strengthClass = '';
+
+    $scope.init = function() {
+      if ($routeParams.logout) {
+        $scope.logout();
+      }
+
+      $scope.$watch("loginMode", $scope.loginModeChanged);
+      $scope.passwordChanged();
+    };
 
     // build info view model
     $scope.buildInfo = {
@@ -26,30 +38,44 @@ function (angular, config) {
     };
 
     $scope.submit = function() {
-      if ($scope.mode === 'login') {
+      if ($scope.loginMode) {
         $scope.login();
       } else {
         $scope.signUp();
       }
     };
 
-    $scope.init = function() {
-      if ($routeParams.logout) {
-        $scope.logout();
-      }
+    $scope.loginModeChanged = function(newValue) {
+      $scope.submitBtnText = newValue ? 'Log in' : 'Sign up';
     };
 
-    $scope.signUp = function() {
-      if ($scope.mode === 'login') {
-        $scope.mode = 'signup';
+    $scope.passwordChanged = function(newValue) {
+      if (!newValue) {
+        $scope.strengthText = "";
+        $scope.strengthClass = "hidden";
+        return;
+      }
+      if (newValue.length < 4) {
+        $scope.strengthText = "strength: weak sauce.";
+        $scope.strengthClass = "password-strength-bad";
+        return;
+      }
+      if (newValue.length <= 6) {
+        $scope.strengthText = "strength: you can do better.";
+        $scope.strengthClass = "password-strength-ok";
         return;
       }
 
+      $scope.strengthText = "strength: strong like a bull.";
+      $scope.strengthClass = "password-strength-good";
+    };
+
+    $scope.signUp = function() {
       if (!$scope.loginForm.$valid) {
         return;
       }
 
-      backendSrv.put('/api/user/signup', $scope.newUser).then(function() {
+      backendSrv.post('/api/user/signup', $scope.formModel).then(function() {
         window.location.href = config.appSubUrl + '/';
       });
     };
@@ -63,17 +89,13 @@ function (angular, config) {
     };
 
     $scope.login = function() {
-      if ($scope.mode === 'signup') {
-        $scope.mode = 'login';
-        return;
-      }
       delete $scope.loginError;
 
       if (!$scope.loginForm.$valid) {
         return;
       }
 
-      backendSrv.post('/login', $scope.loginModel).then(function() {
+      backendSrv.post('/login', $scope.formModel).then(function() {
         window.location.href = config.appSubUrl + '/';
       });
     };

+ 104 - 0
src/app/controllers/sidemenuCtrl.js

@@ -0,0 +1,104 @@
+define([
+  'angular',
+  'lodash',
+  'jquery',
+  'config',
+],
+function (angular, _, $, config) {
+  'use strict';
+
+  var module = angular.module('grafana.controllers');
+
+  module.controller('SideMenuCtrl', function($scope, $location) {
+
+    $scope.getUrl = function(url) {
+      return config.appSubUrl + url;
+    };
+
+    $scope.menu = [
+      {
+        text: "Dashbord",
+        href: $scope.getUrl("/"),
+        startsWith: config.appSubUrl + '/dashboard/',
+        icon: "fa fa-th-large",
+        links: [
+          { text: 'Settings',    editview: 'settings',    icon: "fa fa-cogs" },
+          { text: 'Templating',  editview: 'templating',  icon: "fa fa-cogs" },
+          { text: 'Annotations', editview: 'annotations', icon: "fa fa-bolt" },
+          { text: 'Export', href:"", icon: "fa fa-bolt" },
+          { text: 'JSON', href:"", icon: "fa fa-bolt" },
+        ]
+      },
+      {
+        text: "Account", href: $scope.getUrl("/account"),
+        icon: "fa fa-shield",
+        links: [
+          { text: 'Info', href: $scope.getUrl("/account"), icon: "fa fa-sitemap" },
+          { text: 'Data sources', href: $scope.getUrl("/account/datasources"), icon: "fa fa-sitemap" },
+          { text: 'Users', href: $scope.getUrl("/account/users"), icon: "fa fa-users" },
+          { text: 'API Keys', href: $scope.getUrl("/account/apikeys"), icon: "fa fa-key" },
+        ]
+      },
+      {
+        text: "Profile", href: $scope.getUrl("/profile"),
+        icon: "fa fa-user",
+        links: [
+          { text: 'Info', href: $scope.getUrl("/profile"), icon: "fa fa-sitemap" },
+          { text: 'Password', href:"", icon: "fa fa-lock" },
+        ]
+      }
+    ];
+
+    $scope.onAppEvent('$routeUpdate', function() {
+      $scope.updateState();
+    });
+
+    $scope.onAppEvent('$routeChangeSuccess', function() {
+      $scope.updateState();
+    });
+
+    $scope.updateState = function() {
+      var currentPath = config.appSubUrl + $location.path();
+      var search = $location.search();
+
+      _.each($scope.menu, function(item) {
+        item.active = false;
+
+        if (item.href === currentPath) {
+          item.active = true;
+        }
+
+        if (item.startsWith) {
+          if (currentPath.indexOf(item.startsWith) === 0) {
+            item.active = true;
+            item.href = currentPath;
+          }
+        }
+
+        _.each(item.links, function(link) {
+          link.active = false;
+
+          if (link.editview) {
+            var params = {};
+            _.each(search, function(value, key) {
+              if (value !== null) { params[key] = value; }
+            });
+
+            params.editview = link.editview;
+            link.href = currentPath + '?' + $.param(params);
+          }
+
+          if (link.href === currentPath) {
+            item.active = true;
+            link.active = true;
+          }
+        });
+      });
+    };
+
+    $scope.init = function() {
+      $scope.updateState();
+    };
+  });
+
+});

+ 27 - 4
src/app/directives/dashEditLink.js

@@ -5,6 +5,12 @@ define([
 function (angular, $) {
   'use strict';
 
+  var editViewMap = {
+    'settings': 'app/partials/dasheditor.html',
+    'annotations': 'app/features/annotations/partials/editor.html',
+    'templating': 'app/partials/templating_editor.html',
+  };
+
   angular
     .module('grafana.directives')
     .directive('dashEditorLink', function($timeout) {
@@ -25,7 +31,7 @@ function (angular, $) {
 
   angular
     .module('grafana.directives')
-    .directive('dashEditorView', function($compile) {
+    .directive('dashEditorView', function($compile, $location) {
       return {
         restrict: 'A',
         link: function(scope, elem) {
@@ -48,10 +54,9 @@ function (angular, $) {
             if (editorScope) { editorScope.dismiss(); }
           }
 
-          scope.onAppEvent("dashboard-loaded", hideEditorPane);
-          scope.onAppEvent('hide-dash-editor', hideEditorPane);
+          function showEditorPane(evt, payload, editview) {
+            if (editview) { payload.src = editViewMap[editview]; }
 
-          scope.onAppEvent('show-dash-editor', function(evt, payload) {
             if (lastEditor === payload.src) {
               hideEditorPane();
               return;
@@ -70,6 +75,14 @@ function (angular, $) {
               lastEditor = null;
               editorScope = null;
               hideScrollbars(false);
+
+              if (editview) {
+                var urlParams = $location.search();
+                if (editview === urlParams.editview) {
+                  delete urlParams.editview;
+                  $location.search(urlParams);
+                }
+              }
             };
 
             // hide page scrollbars while edit pane is visible
@@ -79,8 +92,18 @@ function (angular, $) {
             var view = $('<div class="dashboard-edit-view" ng-include="' + src + '"></div>');
             elem.append(view);
             $compile(elem.contents())(editorScope);
+          }
+
+          scope.$watch("dashboardViewState.state.editview", function(newValue, oldValue) {
+            if (newValue) {
+              showEditorPane(null, {}, newValue);
+            } else if (oldValue) {
+              hideEditorPane();
+            }
           });
 
+          scope.onAppEvent('hide-dash-editor', hideEditorPane);
+          scope.onAppEvent('show-dash-editor', showEditorPane);
         }
       };
     });

+ 15 - 0
src/app/directives/tip.js

@@ -18,6 +18,21 @@ function (angular, kbn) {
       };
     });
 
+  angular
+    .module('grafana.directives')
+    .directive('watchChange', function() {
+      return {
+        scope: { onchange: '&watchChange' },
+        link: function(scope, element) {
+          element.on('input', function() {
+            scope.$apply(function () {
+              scope.onchange({ inputValue: element.val() });
+            });
+          });
+        }
+      };
+    });
+
   angular
     .module('grafana.directives')
     .directive('editorOptBool', function($compile) {

+ 29 - 0
src/app/features/account/accountCtrl.js

@@ -0,0 +1,29 @@
+define([
+  'angular',
+],
+function (angular) {
+  'use strict';
+
+  var module = angular.module('grafana.controllers');
+
+  module.controller('AccountCtrl', function($scope, $http, backendSrv) {
+
+    $scope.init = function() {
+      $scope.getAccount();
+    };
+
+    $scope.getAccount = function() {
+      backendSrv.get('/api/account').then(function(account) {
+        $scope.account = account;
+      });
+    };
+
+    $scope.update = function() {
+      if (!$scope.accountForm.$valid) { return; }
+      backendSrv.put('/api/account', $scope.account).then($scope.getAccount);
+    };
+
+    $scope.init();
+
+  });
+});

+ 3 - 13
src/app/features/account/accountUsersCtrl.js

@@ -24,22 +24,12 @@ function (angular) {
     };
 
     $scope.removeUser = function(user) {
-      backendSrv.request({
-        method: 'DELETE',
-        url: '/api/account/users/' + user.userId
-      }).then($scope.get);
+      backendSrv.delete('/api/account/users/' + user.userId).then($scope.get);
     };
 
     $scope.addUser = function() {
-      if (!$scope.form.$valid) {
-        return;
-      }
-
-      backendSrv.request({
-        method: 'PUT',
-        url: '/api/account/users',
-        data: $scope.user,
-      }).then($scope.get);
+      if (!$scope.form.$valid) { return; }
+      backendSrv.post('/api/account/users', $scope.user).then($scope.get);
     };
 
     $scope.init();

+ 1 - 6
src/app/features/account/apiKeysCtrl.js

@@ -26,12 +26,7 @@ function (angular) {
     };
 
     $scope.addToken = function() {
-      backendSrv.request({
-        method: 'PUT',
-        url: '/api/tokens',
-        data: $scope.token,
-        desc: 'Add token'
-      }).then($scope.getTokens);
+      backendSrv.post('/api/tokens', $scope.token).then($scope.getTokens);
     };
 
     $scope.init();

+ 37 - 0
src/app/features/account/partials/account.html

@@ -0,0 +1,37 @@
+<div ng-include="'app/partials/navbar.html'" ng-init="pageTitle='Account'"></div>
+
+<div class="dashboard-edit-view" style="min-height: 500px">
+
+	<div class="dashboard-editor-header">
+		<div class="dashboard-editor-title">
+			<i class="fa fa-shield"></i>
+			  Account information
+		</div>
+	</div>
+
+	<div class="dashboard-editor-body">
+		<div class="row editor-row">
+			<div class="section">
+				<form name="accountForm">
+					<div>
+					<div class="tight-form">
+						<ul class="tight-form-list">
+							<li class="tight-form-item" style="width: 120px">
+								<strong>Account name</strong>
+							</li>
+							<li>
+								<input type="text" required ng-model="account.name" class="input-xlarge tight-form-input last" >
+							</li>
+						</ul>
+						<div class="clearfix"></div>
+					</div>
+					</div>
+					<br>
+					<button type="submit" class="pull-right btn btn-success" ng-click="update()">Update</button>
+				</form>
+			</div>
+		</div>
+	</div>
+</div>
+
+

+ 1 - 1
src/app/features/account/partials/apikeys.html

@@ -1,4 +1,4 @@
-<div ng-include="'app/partials/navbar.html'" ng-init="pageTitle='Account > API Keys'"></div>
+<div ng-include="'app/partials/navbar.html'" ng-init="pageTitle='Account'"></div>
 
 <div class="dashboard-edit-view" style="min-height: 500px">
 

+ 1 - 1
src/app/features/account/partials/datasources.html

@@ -1,4 +1,4 @@
-<div ng-include="'app/partials/navbar.html'" ng-init="pageTitle='Admin'"></div>
+<div ng-include="'app/partials/navbar.html'" ng-init="pageTitle='Account'"></div>
 
 <div class="dashboard-edit-view" style="min-height: 500px">
 	<div class="editor-row">

+ 2 - 2
src/app/features/account/partials/users.html

@@ -1,11 +1,11 @@
-<div ng-include="'app/partials/navbar.html'" ng-init="pageTitle='Account > Users'"></div>
+<div ng-include="'app/partials/navbar.html'" ng-init="pageTitle='Account'"></div>
 
 <div class="dashboard-edit-view" style="min-height: 500px">
 
 	<div class="dashboard-editor-header">
 		<div class="dashboard-editor-title">
 			<i class="fa fa-users"></i>
-			Account users
+			Users
 		</div>
 	</div>
 

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

@@ -12,6 +12,7 @@ define([
   './account/datasourcesCtrl',
   './account/apiKeysCtrl',
   './account/importCtrl',
+  './account/accountCtrl',
   './admin/adminUsersCtrl',
   './grafanaDatasource/datasource',
 ], function () {});

+ 43 - 0
src/app/features/dashboard/partials/panelTime.html

@@ -0,0 +1,43 @@
+<div class="editor-row">
+	<div class="section" style="margin-bottom: 20px">
+		<div class="tight-form">
+			<ul class="tight-form-list">
+				<li class="tight-form-item tight-form-item-icon">
+					<i class="fa fa-clock-o"></i>
+				</li>
+				<li class="tight-form-item" style="width: 148px">
+					<strong>Override relative time</strong>
+				</li>
+				<li class="tight-form-item" style="width: 50px">
+					Last
+				</li>
+				<li>
+					<input type="text" class="input-small tight-form-input" placeholder="1h"
+					  empty-to-null ng-model="panel.timeFrom"
+					  ng-change="get_data()" ng-model-onblur>
+				</li>
+			</ul>
+			<div class="clearfix"></div>
+		</div>
+		<div class="tight-form">
+			<ul class="tight-form-list">
+				<li class="tight-form-item tight-form-item-icon">
+					<i class="fa fa-clock-o"></i>
+				</li>
+				<li class="tight-form-item" style="width: 148px">
+					<strong>Add time shift</strong>
+				</li>
+				<li class="tight-form-item" style="width: 50px">
+					Amount
+				</li>
+				<li>
+					<input type="text" class="input-small tight-form-input" placeholder="1h"
+					empty-to-null ng-model="panel.timeShift"
+					ng-change="get_data()" ng-model-onblur>
+				</li>
+			</ul>
+			<div class="clearfix"></div>
+		</div>
+	</div>
+</div>
+

+ 12 - 3
src/app/features/dashboard/timeSrv.js

@@ -19,12 +19,23 @@ define([
       this.time = dashboard.time;
 
       this._initTimeFromUrl();
+      this._parseTime();
 
       if(this.dashboard.refresh) {
         this.set_interval(this.dashboard.refresh);
       }
     };
 
+    this._parseTime = function() {
+      // when absolute time is saved in json it is turned to a string
+      if (_.isString(this.time.from) && this.time.from.indexOf('Z') >= 0) {
+        this.time.from = new Date(this.time.from);
+      }
+      if (_.isString(this.time.to) && this.time.to.indexOf('Z') >= 0) {
+        this.time.to = new Date(this.time.to);
+      }
+    };
+
     this._parseUrlParam = function(value) {
       if (value.indexOf('now') !== -1) {
         return value;
@@ -109,9 +120,7 @@ define([
 
     this.timeRange = function(parse) {
       var _t = this.time;
-      if(_.isUndefined(_t) || _.isUndefined(_t.from)) {
-        return false;
-      }
+
       if(parse === false) {
         return {
           from: _t.from,

+ 2 - 1
src/app/features/dashboard/viewStateSrv.js

@@ -53,12 +53,13 @@ function (angular, _, $) {
       state.panelId = parseInt(state.panelId) || null;
       state.fullscreen = state.fullscreen ? true : null;
       state.edit =  (state.edit === "true" || state.edit === true) || null;
+      state.editview = state.editview || null;
       return state;
     };
 
     DashboardViewState.prototype.serializeToUrl = function() {
       var urlState = _.clone(this.state);
-      urlState.fullscreen = this.state.fullscreen ? true : null,
+      urlState.fullscreen = this.state.fullscreen ? true : null;
       urlState.edit = this.state.edit ? true : null;
       return urlState;
     };

+ 1 - 0
src/app/features/graphite/datasource.js

@@ -276,6 +276,7 @@ function (angular, _, $, config, kbn, moment) {
 
         targetValue = targets[this._seriesRefLetters[i]];
         targetValue = targetValue.replace(regex, nestedSeriesRegexReplacer);
+        targets[this._seriesRefLetters[i]] = targetValue;
 
         clean_options.push("target=" + encodeURIComponent(targetValue));
       }

+ 1 - 1
src/app/features/graphite/queryCtrl.js

@@ -293,7 +293,7 @@ function (angular, _, config, gfunc, Parser) {
     function MetricSegment(options) {
       if (options === '*' || options.value === '*') {
         this.value = '*';
-        this.html = $sce.trustAsHtml('<i class="icon-asterisk"><i>');
+        this.html = $sce.trustAsHtml('<i class="fa fa-asterisk"><i>');
         this.expandable = true;
         return;
       }

+ 2 - 9
src/app/features/influxdb/datasource.js

@@ -374,7 +374,7 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
       var fromIsAbsolute = from[from.length-1] === 's';
 
       if (until === 'now()' && !fromIsAbsolute) {
-        return 'time > now() - ' + from;
+        return 'time > ' + from;
       }
 
       return 'time > ' + from + ' and time < ' + until;
@@ -382,14 +382,7 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
 
     function getInfluxTime(date) {
       if (_.isString(date)) {
-        if (date === 'now') {
-          return 'now()';
-        }
-        else if (date.indexOf('now') >= 0) {
-          return date.substring(4);
-        }
-
-        date = kbn.parseDate(date);
+        return date.replace('now', 'now()');
       }
 
       return to_utc_epoch_seconds(date);

+ 47 - 42
src/app/features/profile/partials/profile.html

@@ -7,51 +7,53 @@
 			<div class="dashboard-editor-header">
 				<div class="dashboard-editor-title">
 					<i class="fa fa-user"></i>
-					Your info
+					Personal information
 				</div>
 			</div>
 
 			<div class="dashboard-editor-body">
+				<div class="row">
+					<form name="userForm">
+						<div>
+							<div class="tight-form">
+								<ul class="tight-form-list">
+									<li class="tight-form-item" style="width: 80px">
+										<strong>Name</strong>
+									</li>
+									<li>
+										<input type="text" required ng-model="user.name" class="input-xxlarge tight-form-input last" >
+									</li>
+								</ul>
+								<div class="clearfix"></div>
+							</div>
+							<div class="tight-form" style="margin-top: 10px">
+								<ul class="tight-form-list">
+									<li class="tight-form-item" style="width: 80px">
+										<strong>Email</strong>
+									</li>
+									<li>
+										<input type="text" required ng-model="user.email" class="input-xxlarge tight-form-input last" >
+									</li>
+								</ul>
+								<div class="clearfix"></div>
+							</div>
+							<div class="tight-form" style="margin-top: 10px">
+								<ul class="tight-form-list">
+									<li class="tight-form-item" style="width: 80px">
+										<strong>Username</strong>
+									</li>
+									<li>
+										<input type="text" required ng-model="user.login" class="input-xxlarge tight-form-input last" >
+									</li>
+								</ul>
+								<div class="clearfix"></div>
+							</div>
+						</div>
 
-				<form name="userForm">
-
-					<div class="tight-form">
-						<ul class="tight-form-list">
-							<li class="tight-form-item" style="width: 80px">
-								<strong>Name</strong>
-							</li>
-							<li>
-								<input type="text" required ng-model="user.name" class="input-xxlarge tight-form-input last" >
-							</li>
-						</ul>
-						<div class="clearfix"></div>
-					</div>
-					<div class="tight-form" style="margin-top: 10px">
-						<ul class="tight-form-list">
-							<li class="tight-form-item" style="width: 80px">
-								<strong>Email</strong>
-							</li>
-							<li>
-								<input type="text" required ng-model="user.email" class="input-xxlarge tight-form-input last" >
-							</li>
-						</ul>
-						<div class="clearfix"></div>
-					</div>
-					<div class="tight-form" style="margin-top: 10px">
-						<ul class="tight-form-list">
-							<li class="tight-form-item" style="width: 80px">
-								<strong>Username</strong>
-							</li>
-							<li>
-								<input type="text" required ng-model="user.login" class="input-xxlarge tight-form-input last" >
-							</li>
-						</ul>
-						<div class="clearfix"></div>
-					</div>
-
-					<br>
-					<button type="submit" class="btn btn-success" ng-click="update()">Update</button>
-				</form>
+						<br>
+						<button type="submit" class="pull-right btn btn-success" ng-click="update()">Update</button>
+					</form>
+				</div>
 			</div>
 		</div>
 
@@ -59,7 +61,7 @@
 			<div class="dashboard-editor-header">
 				<div class="dashboard-editor-title">
 					<i class="fa fa-cubes"></i>
-					 Your accounts
+					Your accounts
 				</div>
 			</div>
 			<br>
@@ -87,7 +89,7 @@
 			<div class="dashboard-editor-header">
 				<div class="dashboard-editor-title">
 					<i class="fa fa-plus-square"></i>
-					 Add account
+					Add account
 				</div>
 			</div>
 			<br>
@@ -111,5 +113,8 @@
 		</div>
 	</div>
 </div>
+	</div>
+</div>
+</div>
 
 

+ 2 - 3
src/app/features/profile/profileCtrl.js

@@ -33,12 +33,11 @@ function (angular) {
 
     $scope.update = function() {
       if (!$scope.userForm.$valid) { return; }
-
-      backendSrv.post('/api/user/', $scope.user);
+      backendSrv.put('/api/user/', $scope.user);
     };
 
     $scope.createAccount = function() {
-      backendSrv.put('/api/account/', $scope.newAccount).then($scope.getUserAccounts);
+      backendSrv.post('/api/account/', $scope.newAccount).then($scope.getUserAccounts);
     };
 
     $scope.init();

+ 2 - 2
src/app/features/templating/templateValuesSrv.js

@@ -81,8 +81,8 @@ function (angular, _, kbn) {
 
     this._updateNonQueryVariable = function(variable) {
       // extract options in comma seperated string
-      variable.options = _.map(variable.query.split(/[\s,]+/), function(text) {
-        return { text: text, value: text };
+      variable.options = _.map(variable.query.split(/[,]+/), function(text) {
+        return { text: text.trim(), value: text.trim() };
       });
 
       if (variable.type === 'interval') {

+ 3 - 3
src/app/panels/graph/graph.tooltip.js

@@ -71,7 +71,7 @@ function ($) {
       for (i = 0; i < seriesList.length; i++) {
         series = seriesList[i];
 
-        if (!series.data.length) {
+        if (!series.data.length || (scope.panel.legend.hideEmpty && series.allIsNull)) {
           results.push({ hidden: true });
           continue;
         }
@@ -163,7 +163,7 @@ function ($) {
           value = series.formatValue(hoverInfo.value);
 
           seriesHtml += '<div class="graph-tooltip-list-item"><div class="graph-tooltip-series-name">';
-          seriesHtml += '<i class="icon-minus" style="color:' + series.color +';"></i> ' + series.label + ':</div>';
+          seriesHtml += '<i class="fa fa-minus" style="color:' + series.color +';"></i> ' + series.label + ':</div>';
           seriesHtml += '<div class="graph-tooltip-value">' + value + '</div></div>';
           plot.highlight(i, hoverInfo.hoverIndex);
         }
@@ -174,7 +174,7 @@ function ($) {
       else if (item) {
         series = seriesList[item.seriesIndex];
         group = '<div class="graph-tooltip-list-item"><div class="graph-tooltip-series-name">';
-        group += '<i class="icon-minus" style="color:' + item.series.color +';"></i> ' + series.label + ':</div>';
+        group += '<i class="fa fa-minus" style="color:' + item.series.color +';"></i> ' + series.label + ':</div>';
 
         if (scope.panel.stack && scope.panel.tooltip.value_type === 'individual') {
           value = item.datapoint[1] - item.datapoint[2];

+ 4 - 0
src/app/panels/graph/module.html

@@ -3,6 +3,10 @@
 	<div class="graph-wrapper" ng-class="{'graph-legend-rightside': panel.legend.rightSide}">
 		<div class="graph-canvas-wrapper">
 
+			<span class="graph-time-info" ng-if="panelMeta.timeInfo">
+				<i class="fa fa-clock-o"></i> {{panelMeta.timeInfo}}
+		  </span>
+
 			<div ng-if="datapointsWarning" class="datapoints-warning">
 				<span class="small" ng-show="!datapointsCount">
 					No datapoints <tip>No datapoints returned from metric query</tip>

+ 24 - 0
src/app/panels/graph/module.js

@@ -26,6 +26,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) {
 
     $scope.panelMeta.addEditorTab('Axes & Grid', 'app/panels/graph/axisEditor.html');
     $scope.panelMeta.addEditorTab('Display Styles', 'app/panels/graph/styleEditor.html');
+    $scope.panelMeta.addEditorTab('Time range', 'app/features/dashboard/partials/panelTime.html');
 
     $scope.panelMeta.addExtendedMenuItem('Export CSV', '', 'exportCsv()');
     $scope.panelMeta.addExtendedMenuItem('Toggle legend', '', 'toggleLegend()');
@@ -88,6 +89,9 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) {
         value_type: 'cumulative',
         shared: false,
       },
+      // time overrides
+      timeFrom: null,
+      timeShift: null,
       // metric queries
       targets: [{}],
       // series color overrides
@@ -114,6 +118,26 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) {
     $scope.updateTimeRange = function () {
       $scope.range = timeSrv.timeRange();
       $scope.rangeUnparsed = timeSrv.timeRange(false);
+
+      $scope.panelMeta.timeInfo = "";
+
+      // check panel time overrrides
+      if ($scope.panel.timeFrom) {
+        if (_.isString($scope.rangeUnparsed.from)) {
+          $scope.panelMeta.timeInfo = "last " + $scope.panel.timeFrom;
+          $scope.rangeUnparsed.from = 'now-' + $scope.panel.timeFrom;
+          $scope.range.from = kbn.parseDate($scope.rangeUnparsed.from);
+        }
+      }
+
+      if ($scope.panel.timeShift) {
+        var timeShift = '-' + $scope.panel.timeShift;
+        $scope.panelMeta.timeInfo += ' timeshift ' + timeShift;
+        $scope.range.from = kbn.parseDateMath(timeShift, $scope.range.from);
+        $scope.range.to = kbn.parseDateMath(timeShift, $scope.range.to);
+        $scope.rangeUnparsed = $scope.range;
+      }
+
       if ($scope.panel.maxDataPoints) {
         $scope.resolution = $scope.panel.maxDataPoints;
       }

+ 42 - 16
src/app/panels/timepicker/editor.html

@@ -1,18 +1,44 @@
   <div class="editor-row">
-    <div class="section">
-      <div class="editor-option">
-        <label class="small">Relative time options <small>comma seperated</small></label>
-        <input type="text" array-join class="input-xlarge" ng-model="panel.time_options">
-      </div>
-      <div class="editor-option">
-        <label class="small">Auto-refresh options <small>comma seperated</small></label>
-        <input type="text" array-join class="input-xlarge" ng-model="panel.refresh_intervals">
-      </div>
+		<div class="section">
+			<div class="tight-form">
+				<ul class="tight-form-list">
+					<li class="tight-form-item" style="width: 148px">
+						<strong>Relative time options</strong></small>
+					</li>
+					<li>
+						<input type="text" class="input-xlarge tight-form-input"
+							ng-model="panel.time_options" array-join>
+				  </li>
+					<li class="tight-form-item">
+						Until
+					</li>
+					<li class="tight-form-item">
+					 now-
+					</li>
+					<li>
+						<input type="text" class="input-mini tight-form-input last"
+						  ng-model="panel.nowDelay" placeholder="0m" bs-tooltip="'Enter 1m to ignore the last minute (because it can contain incomplete metrics)'" data-placement="right">
+				  </li>
+				</ul>
+				<div class="clearfix"></div>
+			</div>
+			<div class="tight-form">
+				<ul class="tight-form-list">
+					<li class="tight-form-item" style="width: 148px">
+						<strong>Auto-refresh options</strong>
+					</li>
+					<li>
+						<input type="text" class="input-xlarge tight-form-input"
+							ng-model="panel.refresh_intervals" array-join>
+				  </li>
+				</ul>
+				<div class="clearfix"></div>
+			</div>
 
-      <p>
-        <br>
-        <i class="fa fa-info-circle"></i>
-        For these changes to fully take effect save and reload the dashboard.
-      </i>
-    </div>
-  </div>
+			<p>
+			<br>
+			<i class="fa fa-info-circle"></i>
+			For these changes to fully take effect save and reload the dashboard.
+		</i>
+	</div>
+</div>

+ 11 - 3
src/app/panels/timepicker/module.js

@@ -58,10 +58,14 @@ function (angular, app, _, moment, kbn) {
 
     $scope.init = function() {
       var time = timeSrv.timeRange(true);
-      if(time) {
-        $scope.panel.now = timeSrv.timeRange(false).to === "now" ? true : false;
-        $scope.time = getScopeTimeObj(time.from,time.to);
+      $scope.panel.now = false;
+
+      var unparsed = timeSrv.timeRange(false);
+      if (_.isString(unparsed.to) && unparsed.to.indexOf('now') === 0) {
+        $scope.panel.now = true;
       }
+
+      $scope.time = getScopeTimeObj(time.from, time.to);
     };
 
     $scope.customTime = function() {
@@ -142,6 +146,10 @@ function (angular, app, _, moment, kbn) {
         to: "now"
       };
 
+      if ($scope.panel.nowDelay) {
+        _filter.to = 'now-' + $scope.panel.nowDelay;
+      }
+
       timeSrv.setTime(_filter);
 
       $scope.time = getScopeTimeObj(kbn.parseDate(_filter.from),new Date());

+ 1 - 1
src/app/partials/dashboard.html

@@ -104,7 +104,7 @@
 
 		<div ng-show='dashboard.editable' class="row-fluid add-row-panel-hint">
 			<div class="span12" style="text-align:right;">
-				<span style="margin-right: 10px;" ng-click="add_row_default()" class="pointer btn btn-info btn-mini">
+				<span style="margin-right: 10px;" ng-click="add_row_default()" class="pointer btn btn-info btn-small">
 					<span><i class="fa fa-plus"></i> ADD ROW</span>
 				</span>
 			</div>

+ 15 - 49
src/app/partials/dashboard_topnav.html

@@ -1,15 +1,22 @@
-<div class="navbar navbar-static-top">
+<div class="navbar navbar-static-top" ng-controller='DashboardNavCtrl' ng-init="init()">
 	<div class="navbar-inner">
 		<div class="container-fluid">
+			<span class="hamburger">
+				<a class="pointer" ng-click="toggleSideMenu()">
+					<i class="fa fa-bars"></i>
+				</a>
+			</span>
 			<span class="brand">
-				<a ng-click="toggleSideMenu()">
-				  <img class="logo-icon" src="img/fav32.png" bs-tooltip="'Grafana'" data-placement="bottom"></img>
+				<img class="logo-icon" src="img/fav32.png" bs-tooltip="'Grafana'" data-placement="bottom"></img>
+				<a ng-click="openSearch()" class="page-title">
+					{{dashboard.title}}
+					<span class="small">
+						<i class="fa fa-angle-down"></i>
+					</span>
 				</a>
-				<span class="page-title">{{dashboard.title}}</span>
 			</span>
 
-			<ul class="nav pull-right" ng-controller='DashboardNavCtrl' ng-init="init()">
-
+			<ul class="nav pull-right">
 				<li ng-show="dashboardViewState.fullscreen">
 					<a ng-click="exitFullscreen()">
 						Back to dashboard
@@ -22,53 +29,12 @@
 				</li>
 
 				<li class="dropdown grafana-menu-save">
-					<a bs-tooltip="'Save'" data-placement="bottom" class="dropdown-toggle" data-toggle="dropdown" ng-click="openSaveDropdown()">
+					<a bs-tooltip="'Save'" data-placement="bottom" class="dropdown-toggle" data-toggle="dropdown" ng-click="saveDashboard()">
 						<i class='fa fa-save'></i>
 					</a>
-
-					<ul class="save-dashboard-dropdown dropdown-menu" ng-if="saveDropdownOpened">
-						<li>
-							<form class="input-prepend nomargin save-dashboard-dropdown-save-form">
-								<input class='input-medium' ng-model="dashboard.title" type="text" />
-								<button class="btn" ng-click="saveDashboard()"><i class="fa fa-save"></i></button>
-							</form>
-						</li>
-
-						<li>
-							<a class="link" ng-click="set_default()">Save as Home</a>
-						</li>
-						<li>
-							<a class="link" ng-click="purge_default()">Reset Home</a>
-						</li>
-						<li ng-show="!isFavorite">
-							<a class="link" ng-click="markAsFavorite()">Mark as favorite</a>
-						</li>
-						<li ng-show="isFavorite">
-							<a class="link" ng-click="removeAsFavorite()">Remove as favorite</a>
-						</li>
-						<li>
-							<a class="link" ng-click="editJson()">Dashboard JSON</a>
-						</li>
-						<li>
-							<a class="link" ng-click="exportDashboard()">Export dashboard</a>
-						</li>
-						<li ng-show="db.saveTemp">
-							<a bs-tooltip="'Share'" data-placement="bottom" ng-click="saveForSharing()" config-modal="app/partials/dashLoaderShare.html">
-								Share temp copy
-							</a>
-						</li>
-					</ul>
 				</li>
 
-				<li class="dropdown grafana-menu-load">
-					<a ng-click="openSearch()" bs-tooltip="'Search'" data-placement="bottom">
-						<i class='fa fa-folder-open'></i>
-					</a>
-				</li>
-
-				<li class="grafana-menu-home"><a bs-tooltip="'Goto saved default'" data-placement="bottom" href='#/'><i class='fa fa-home'></i></a></li>
-
-				<li class="grafana-menu-edit" ng-show="dashboard.editable" bs-tooltip="'Configure dashboard'" data-placement="bottom"><a class="link" dash-editor-link="app/partials/dasheditor.html"><i class='fa fa-cog pointer'></i></a></li>
+				<!-- <li class="grafana&#45;menu&#45;home"><a bs&#45;tooltip="'Goto saved default'" data&#45;placement="bottom" href='#/'><i class='fa fa&#45;home'></i></a></li> -->
 
 				<li class="grafana-menu-stop-playlist hide">
 					<a class='small' ng-click='stopPlaylist(2)'>

+ 1 - 1
src/app/partials/dasheditor.html

@@ -54,7 +54,7 @@
 							<td><i ng-click="_.move(dashboard.rows,$index,$index-1)" ng-hide="$first" class="pointer fa fa-arrow-up"></i></td>
 							<td><i ng-click="_.move(dashboard.rows,$index,$index+1)" ng-hide="$last" class="pointer fa fa-arrow-down"></i></td>
 							<td>
-								<a ng-click="dashboard.rows = _.without(dashboard.rows,row)" class="btn btn-danger btn-mini">
+								<a ng-click="dashboard.rows = _.without(dashboard.rows,row)" class="btn btn-danger btn-small">
 									<i class="fa fa-remove"></i>
 								</a>
 							</td>

+ 0 - 36
src/app/partials/loadmetrics.html

@@ -1,36 +0,0 @@
-<div ng-controller="MetricKeysCtrl" ng-init="init()">
-  <h5>Load metrics keys into elastic search</h5>
-
-  <p>
-    Work in progress...
-  </p>
-  <!-- <div class="row-fluid">
-    <div class="span12">
-      <label class="small">Load metrics recursive starting from this metric path</label>
-      <input type="text" class="input-xlarge" ng-model="metricPath"> </input>
-    </div>
-  </div>
-
-  <div class="row-fluid" style="margin-top: 15px;">
-    <div class="span12">
-      <button class="btn btn-success" ng-click="createIndex()">Clear/Create index</button>
-      <button class="btn btn-success" ng-click="loadMetricsFromPath()">Load from metric path</button>
-      <button class="btn btn-danger" ng-click="loadAll()">Load all</button>
-      <tip>Load all will fetch all metrics in one call, can be intensive for graphite and for the browser if you have a lot of metrics</tip>
-    </div>
-  </div>
-
-  <div class="row-fluid" style="margin-top: 15px;">
-    <div class="span12" ng-show="infoText" style="padding-top: 10px;">
-      {{infoText}}
-    </div>
-    <div class="span12 alert alert-error" ng-show="errorText">
-      {{errorText}}
-    </div>
-  </div>
-  <div class="row-fluid" ng-show="metricCounter">
-    <div class="span12" style="padding-top: 10px;">
-      Metrics indexed: {{metricCounter}}
-    </div>
-  </div> -->
-</div>

+ 30 - 38
src/app/partials/login.html

@@ -7,82 +7,74 @@
 		</div>
 
     <div class="login-inner-box">
-			<h1 ng-if="mode === 'login'">Login</h1>
-			<h1 ng-if="mode === 'signup'">Sign up</h1>
+			<div class="login-tab-header">
+				<button class="btn-login-tab" ng-click="loginMode = true;" ng-class="{active: loginMode}">
+					Log in
+				</button>
+				<button class="btn-login-tab" ng-click="loginMode = false;" ng-class="{active: !loginMode}">
+					Sign up
+				</button>
+			</div>
 
       <form name="loginForm" class="login-form">
-        <div class="tight-form" ng-if="mode === 'login'">
+        <div class="tight-form" ng-if="loginMode">
           <ul class="tight-form-list">
             <li class="tight-form-item" style="width: 80px">
-              User
+							<strong>User</strong>
             </li>
             <li>
-              <input type="text" class="tight-form-input last" ng-model='loginModel.user' placeholder="email or username" style="width: 246px">
+              <input type="text" class="tight-form-input last" ng-model='formModel.user' placeholder="email or username" style="width: 246px">
             </li>
           </ul>
           <div class="clearfix"></div>
         </div>
-        <div class="tight-form" ng-if="mode === 'login'">
+        <div class="tight-form" ng-if="loginMode">
           <ul class="tight-form-list">
             <li class="tight-form-item" style="width: 80px">
-              Password
+							<strong>Password</strong>
             </li>
             <li>
-              <input type="password" class="tight-form-input last" required ng-model="loginModel.password" id="inputPassword" style="width: 246px">
+							<input type="password" class="tight-form-input last" required ng-model="formModel.password" id="inputPassword" style="width: 246px" placeholder="password">
             </li>
           </ul>
           <div class="clearfix"></div>
-        </div>
+				</div>
 
-				<div class="tight-form" ng-if="mode === 'signup'">
+				<div class="tight-form" ng-if="!loginMode">
           <ul class="tight-form-list">
             <li class="tight-form-item" style="width: 80px">
-              Email
+							<strong>Email</strong>
             </li>
             <li>
-              <input type="email" class="tight-form-input last" required ng-model='newUser.email' placeholder="email" style="width: 246px">
+              <input type="email" class="tight-form-input last" required ng-model='formModel.email' placeholder="email" style="width: 246px">
             </li>
           </ul>
           <div class="clearfix"></div>
         </div>
 
-				<div class="tight-form" ng-if="mode === 'signup'">
+				<div class="tight-form" ng-if="!loginMode">
           <ul class="tight-form-list">
             <li class="tight-form-item" style="width: 80px">
-              Name
+							<strong>Password</strong>
             </li>
             <li>
-              <input type="text" class="tight-form-input last" ng-model='newUser.name' placeholder="your full name (optional)" style="width: 246px">
+							<input type="password" class="tight-form-input last" watch-change="passwordChanged(inputValue)" ng-minlength="4" required ng-model='formModel.password' placeholder="password" style="width: 246px">
             </li>
           </ul>
           <div class="clearfix"></div>
         </div>
 
-				<div class="tight-form" ng-if="mode === 'signup'">
-          <ul class="tight-form-list">
-            <li class="tight-form-item" style="width: 80px">
-              Password
-            </li>
-            <li>
-              <input type="password" class="tight-form-input last" required ng-model='newUser.password' placeholder="" style="width: 246px">
-            </li>
-          </ul>
-          <div class="clearfix"></div>
-        </div>
+				<div class="password-strength small" ng-if="!loginMode" ng-class="strengthClass">
+					<em>{{strengthText}}</em>
+				</div>
 
-				<div class="tight-form">
-          <ul class="tight-form-list">
-						<li class="tight-form-item login-signup-button" ng-class="{'login-signup-button-disable': mode === 'signup'}">
-							<a ng-click="login()">Log in</a>
-						</li>
-						<li class="tight-form-item login-signup-button last" ng-class="{'login-signup-button-disable': mode === 'login'}">
-							<a ng-click="signUp()">Sign up</a>
-						</li>
-          </ul>
-          <div class="clearfix"></div>
-        </div>
 
-				<button type="submit" ng-click="submit();" class="hidden"></button>
+				<div class="login-submit-button-row">
+					<button type="submit" class="btn" ng-click="submit();"
+						      ng-class="{'btn-inverse': !loginForm.$valid, 'btn-primary': loginForm.$valid}">
+						{{submitBtnText}}
+					</button>
+				</div>
 			</form>
 
 			<div class="clearfix"></div>

+ 6 - 3
src/app/partials/navbar.html

@@ -1,10 +1,13 @@
 <div class="navbar navbar-static-top">
 	<div class="navbar-inner">
 		<div class="container-fluid">
-			<span class="brand">
-				<a ng-click="toggleSideMenu()">
-					<img class="logo-icon" src="img/fav32.png" bs-tooltip="'Grafana'" data-placement="bottom"></img>
+			<span class="hamburger">
+				<a class="pointer" ng-click="toggleSideMenu()">
+					<i class="fa fa-bars"></i>
 				</a>
+			</span>
+			<span class="brand">
+				<img class="logo-icon" src="img/fav32.png" bs-tooltip="'Grafana'" data-placement="bottom"></img>
 				<span class="page-title">{{pageTitle}}</span>
 			</span>
 		</div>

+ 1 - 1
src/app/partials/roweditor.html

@@ -41,7 +41,7 @@
 					<td><i ng-click="_.move(row.panels,$index,$index-1)" ng-hide="$first" class="pointer fa fa-arrow-up"></i></td>
 					<td><i ng-click="_.move(row.panels,$index,$index+1)" ng-hide="$last" class="pointer fa fa-arrow-down"></i></td>
 					<td>
-						<a ng-click="row.panels = _.without(row.panels,panel)" class="btn btn-danger btn-mini">
+						<a ng-click="row.panels = _.without(row.panels,panel)" class="btn btn-danger btn-small">
 							<i class="fa fa-remove"></i>
 						</a>
 					</td>

+ 23 - 43
src/app/partials/sidemenu.html

@@ -1,45 +1,25 @@
-<section class="pro-sidemenu-items">
-	<div class="dropdown">
-		<a class="pro-sidemenu-link pointer gravatar" data-toggle="dropdown" title="{{grafana.user.email}}">
-			<span class="gravatar-missing">f</span>
-			<img ng-src="{{grafana.user.gravatarUrl}}" width="35"> <span class="gravatar-email small">{{grafana.user.login}}</span>
-		</a>
-		<ul class="dropdown-menu">
-			<li><a href="/login?logout">Logout</a></li>
-		</ul>
-	</div>
-	<a class="pro-sidemenu-link" ng-href="{{appSubUrl}}/">
-		<i class="fa fa-th-large"></i>
-		Dashboards
-	</a>
-	<a class="pro-sidemenu-link" href="account/datasources">
-		<i class="fa fa-sitemap"></i>
-		Data
-	</a>
-	<a class="pro-sidemenu-link" href="account/users">
-		<i class="fa fa-users"></i>Users
-	</a>
-	<a class="pro-sidemenu-link" href="account/apikeys">
-		<i class="fa fa-key"></i>API Keys
-	</a>
-	<a class="pro-sidemenu-link" href="account/import">
-		<i class="fa fa-download"></i>
-		Import
-	</a>
-
-	<a class="pro-sidemenu-link" href="profile">
-		<i class="fa fa-user"></i>
-		Profile
-	</a>
-
-	<a class="pro-sidemenu-link" href="admin/users" ng-if="grafana.user.isGrafanaAdmin">
-		<i class="fa fa-institution"></i>Admin
-	</a>
-
-  <a class="pro-sidemenu-link" href="login?logout">
-		<i class="fa fa-sign-out"></i>Sign out
-	</a>
-
-</section>
+<div ng-controller="SideMenuCtrl" ng-init="init()">
 
+	<ul class="sidemenu">
+		<li class="dropdown">
+			<a class="sidemenu-user pointer" data-toggle="dropdown" title="{{grafana.user.email}}">
+				<span class="gravatar-missing">f</span>
+				<img ng-src="{{grafana.user.gravatarUrl}}" width="35">
+				<span class="gravatar-email small">{{grafana.user.login}}</span>
+			</a>
+			<ul class="dropdown-menu">
+				<li><a href="{{appSubUrl}}/login?logout">Logout</a></li>
+			</ul>
+		</li>
+		<li ng-repeat-start="item in menu" ng-class="{'active': item.active}">
+			<a href="{{item.href}}" class="sidemenu-item"><i class="{{item.icon}}"></i>{{item.text}}</a>
+		</li>
+		<li ng-repeat-end ng-if="item.active">
+			<ul class="sidemenu-links">
+				<li ng-repeat="link in item.links">
+					<a href="{{link.href}}" class="sidemenu-link" ng-class="{active: link.active}"><i class="fa fa-angle-right"></i>{{link.text}}</a>
+				</li>
+			</ul>
+		</li>
+	</ul>
 </div>

+ 2 - 2
src/app/partials/templating_editor.html

@@ -31,7 +31,7 @@
 								{{variable.query}}
 							</td>
 							<td style="width: 1%">
-								<a ng-click="edit(variable)" class="btn btn-success btn-mini">
+								<a ng-click="edit(variable)" class="btn btn-success btn-small">
 									<i class="fa fa-edit"></i>
 									Edit
 								</a>
@@ -39,7 +39,7 @@
 							<td style="width: 1%"><i ng-click="_.move(variables,$index,$index-1)" ng-hide="$first" class="pointer fa fa-arrow-up"></i></td>
 							<td style="width: 1%"><i ng-click="_.move(variables,$index,$index+1)" ng-hide="$last" class="pointer fa fa-arrow-down"></i></td>
 							<td style="width: 1%">
-								<a ng-click="removeVariable(variable)" class="btn btn-danger btn-mini">
+								<a ng-click="removeVariable(variable)" class="btn btn-danger btn-small">
 									<i class="fa fa-remove"></i>
 								</a>
 							</td>

+ 4 - 0
src/app/routes/backend/all.js

@@ -30,6 +30,10 @@ define([
         controller : 'DashFromImportCtrl',
         reloadOnSearch: false,
       })
+      .when('/account', {
+        templateUrl: 'app/features/account/partials/account.html',
+        controller : 'AccountCtrl',
+      })
       .when('/account/datasources', {
         templateUrl: 'app/features/account/partials/datasources.html',
         controller : 'DataSourcesCtrl',

+ 1 - 0
src/app/routes/backend/dashboard.js

@@ -21,6 +21,7 @@ function (angular, store) {
       // do we have a previous dash
       if (prevDashPath) {
         $location.path(prevDashPath);
+        return;
       }
 
       var savedRoute = store.get('grafanaDashboardDefault');

+ 1 - 1
src/css/less/bootswatch.dark.less

@@ -363,7 +363,7 @@ div.subnav {
 	background-image: none;
 	.box-shadow(none);
 	border: none;
-	.border-radius(2px);
+	.border-radius(3px);
 	text-shadow: none;
 
 	&.disabled {

+ 1 - 1
src/css/less/bootswatch.light.less

@@ -309,7 +309,7 @@ div.subnav {
 	background-image: none;
 	.box-shadow(none);
 	border: none;
-	.border-radius(0);
+	.border-radius(3px);
 	text-shadow: none;
 
 	&.disabled {

+ 10 - 0
src/css/less/grafana.less

@@ -52,6 +52,16 @@
   float: left;
 }
 
+.navbar .brand {
+  margin-left: 0;
+}
+
+.hamburger {
+  float: left;
+  padding: 15px 0 14px 0;
+  font-size: 1.4em;
+}
+
 .page-title {
  padding: 15px 0;
  display: block;

+ 11 - 2
src/css/less/graph.less

@@ -141,7 +141,7 @@
     vertical-align: top;
     position: relative;
     left: 4px;
-    top: -20px;
+    top: -25px;
   }
 
   .graph-legend {
@@ -260,7 +260,6 @@
   transform-origin: right top;
 }
 
-
 .axisLabel {
   color: @textColor;
   font-size: @fontSizeSmall;
@@ -269,3 +268,13 @@
   font-size: 12px;
 }
 
+.graph-time-info {
+  font-weight: bold;
+  float: right;
+  margin-right: 15px;
+  color: @blue;
+  font-size: 85%;
+  position: relative;
+  top: -20px;
+}
+

+ 0 - 1
src/css/less/overrides.less

@@ -58,7 +58,6 @@
 .bgInverse {
   background: @btnInverseBackground;
   color: rgba(255,255,255,.90);
-
 }
 
 code, pre {

+ 115 - 54
src/css/less/p_pro.less

@@ -1,5 +1,8 @@
 .pro-sidemenu {
   display: none;
+  a:focus {
+    text-decoration: none;
+  }
 }
 
 .pro-sidemenu-open {
@@ -29,34 +32,40 @@
   }
 }
 
-.pro-sidemenu-items {
+.sidemenu {
+  list-style: none;
+  background: @grafanaPanelBackground;
+  margin: 0;
+  padding: 0;
 }
 
-.pro-sidemenu-link {
-  font-size: 1.0rem;
-  padding: 14px 10px 14px 20px;
+.sidemenu-links {
+  margin: 0;
+  padding: 5px 0;
+  list-style: none;
+  background: @grafanaTargetFuncBackground;
+  li {
+    display: block;
+  }
+}
+
+.sidemenu-link {
   display: block;
-  background: @grafanaPanelBackground;
-  color: @grayLight;
+  padding: 6px 0 6px 30px;
+  font-size: 15px;
+  color: @gray;
   i {
     padding-right: 15px;
   }
-  border-bottom: 1px solid black;
-}
-
-.pro-sidemenu-link:first-child {
- // border-top: 1px solid black;
-}
-
-.pro-side-menu-user {
-  padding-left: 5px;
-  img {
-    width: 49px;
-    padding-right: 10px;
+  &.active {
+    color: white;
+    font-weight: bold;
   }
 }
 
-.gravatar {
+.sidemenu-user {
+  padding: 8px 10px 7px 15px;
+  display: block;
   width: 170px;
   overflow: hidden;
   text-overflow: ellipsis;
@@ -67,29 +76,102 @@
   .gravatar-email {
     padding-left: 4px;
   }
+  img {
+    width: 35px;
+    padding-right: 10px;
+  }
+  border-bottom: 1px solid black;
+}
+
+.sidemenu-item {
+  font-size: 17px;
+  padding: 14px 10px 14px 20px;
+  display: block;
+  i {
+    padding-right: 15px;
+  }
+  border-bottom: 1px solid black;
+}
+
+
+.login-form {
+  width: 50%;
+  float: left;
+  margin-left: 25%;
+  margin-right: 25%;
+  padding-top: 50px;
+}
+
+.login-box {
+  width: 700px;
+  margin: 100px auto 0 auto;
 }
 
+.login-box-logo {
+  text-align: center;
+  padding-bottom: 50px;
+}
+
+
 .login-inner-box {
   background: @grafanaPanelBackground;
-  h1 {
-    font-size: 1.15em;
-    background: @grafanaTargetBackground;
-    text-align: center;
-    padding: 2px;
-  }
 }
 
-.login-signup-button {
-  width: 45%;
+.login-tab-header {
+  background: @grafanaTargetBackground;
   text-align: center;
+}
 
-  a {
-    font-weight: bold;
+.btn-login-tab {
+  background: transparent;
+  border: none;
+  font-size: 15px;
+  padding: 10px 10px;
+
+  &.active {
+    background: darken(@grafanaTargetBackground, 5%);
+    color: @white;
   }
-  &.login-signup-button-disable {
-    a {
-      color: darken(@linkColor, 35%);
-    }
+
+  &:focus {
+    outline: none;
+  }
+
+  font-weight: bold;
+  display: inline-block;
+  width: 170px;
+  color: @textColor;
+}
+
+.password-strength {
+  display: block;
+  width: 50px;
+  overflow: visible;
+  white-space: nowrap;
+  padding-top: 3px;
+  margin-left: 97px;
+  color: darken(@textColor, 20%);
+  border-top: 3px solid @red;
+  &.password-strength-ok {
+    width: 170px;
+    border-top: 3px solid lighten(@yellow, 10%);
+  }
+  &.password-strength-good {
+    width: 254px;
+    border-top: 3px solid lighten(@green, 10%);
+  }
+}
+
+.login-submit-button-row {
+  text-align: center;
+  margin-top: 40px;
+  button {
+    padding: 9px 7px;
+    font-size: 14px;
+    font-weight: bold;
+    width: 150px;
+    display: inline-block;
+    border: 1px solid lighten(@btnInverseBackground, 10%);
   }
 }
 
@@ -107,25 +189,4 @@
   }
 }
 
-.login-form {
-  width: 50%;
-  float: left;
-  margin-left: 25%;
-  margin-right: 25%;
-  padding-top: 30px;
-}
-
-.login-box {
-  width: 700px;
-  margin: 100px auto 0 auto;
-}
-
-.login-box-logo {
-  text-align: center;
-  padding-bottom: 50px;
-}
-
-.register-box {
-  margin-top: 100px;
-}
 

+ 2 - 2
src/css/less/panel.less

@@ -49,8 +49,8 @@
 
 .panel-loading {
   position:absolute;
-  top: 0px;
-  right: 4px;
+  top: -3px;
+  right: 0px;
   z-index: 800;
 }
 

+ 6 - 6
src/css/less/variables.dark.less

@@ -17,7 +17,7 @@
 // Accent colors
 // -------------------------
 @blue:                  #33B5E5;
-@blueDark:              #0099CC;
+@blueDark:              #0086b3;
 @green:                 #669900;
 @red:                   #CC3900;
 @yellow:                #ECBB13;
@@ -111,12 +111,12 @@
 
 // Buttons
 // -------------------------
-@btnBackground:                     @grayLight;
+@btnBackground:                     @grayDark;
 @btnBackgroundHighlight:            darken(@grayLight, 15%);
 @btnBorder:                         #bbb;
 
-@btnPrimaryBackground:              lighten(@blue, 5%);
-@btnPrimaryBackgroundHighlight:     darken(@blue, 5%);
+@btnPrimaryBackground:              lighten(@blueDark, 5%);
+@btnPrimaryBackgroundHighlight:     darken(@blueDark, 5%);
 
 @btnInfoBackground:                 lighten(@purple, 5%);
 @btnInfoBackgroundHighlight:        darken(@purple, 5%);
@@ -130,8 +130,8 @@
 @btnDangerBackground:               lighten(@red, 5%);
 @btnDangerBackgroundHighlight:      darken(@red, 5%);
 
-@btnInverseBackground:              lighten(@black, 5%);
-@btnInverseBackgroundHighlight:     darken(@black, 5%);
+@btnInverseBackground:              @grayDark;
+@btnInverseBackgroundHighlight:     lighten(@grayDark, 5%);
 
 
 // Forms

+ 1 - 0
src/test/specs/graph-tooltip-specs.js

@@ -19,6 +19,7 @@ define([
       tooltip:  {
         shared: true
       },
+      legend: { },
       stack: false
     };
 

+ 7 - 0
src/test/specs/graphiteDatasource-specs.js

@@ -74,6 +74,13 @@ define([
         expect(results[2]).to.be('target=asPercent(series1%2Cseries2)');
       });
 
+      it('should replace target placeholder when nesting query references', function() {
+        var results = ctx.ds.buildGraphiteParams({
+          targets: [{target: 'series1'}, {target: 'sumSeries(#A)'}, {target: 'asPercent(#A,#B)'}]
+        });
+        expect(results[2]).to.be('target=' + encodeURIComponent("asPercent(series1,sumSeries(series1))"));
+      });
+
       it('should fix wrong minute interval parameters', function() {
         var results = ctx.ds.buildGraphiteParams({
           targets: [{target: "summarize(prod.25m.count, '25m', 'sum')" }]

+ 3 - 3
src/test/specs/influxdb-datasource-specs.js

@@ -17,7 +17,7 @@ define([
     describe('When querying influxdb with one target using query editor target spec', function() {
       var results;
       var urlExpected = "/series?p=mupp&q=select+mean(value)+from+%22test%22"+
-                        "+where+time+%3E+now()+-+1h+group+by+time(1s)+order+asc";
+                        "+where+time+%3E+now()-1h+group+by+time(1s)+order+asc";
       var query = {
         range: { from: 'now-1h', to: 'now' },
         targets: [{ series: 'test', column: 'value', function: 'mean' }],
@@ -50,7 +50,7 @@ define([
     describe('When querying influxdb with one raw query', function() {
       var results;
       var urlExpected = "/series?p=mupp&q=select+value+from+series"+
-                        "+where+time+%3E+now()+-+1h";
+                        "+where+time+%3E+now()-1h";
       var query = {
         range: { from: 'now-1h', to: 'now' },
         targets: [{ query: "select value from series where $timeFilter", rawQuery: true }]
@@ -73,7 +73,7 @@ define([
     describe('When issuing annotation query', function() {
       var results;
       var urlExpected = "/series?p=mupp&q=select+title+from+events.backend_01"+
-                        "+where+time+%3E+now()+-+1h";
+                        "+where+time+%3E+now()-1h";
 
       var range = { from: 'now-1h', to: 'now' };
       var annotation = { query: 'select title from events.$server where $timeFilter' };

+ 11 - 0
src/test/specs/kbn-format-specs.js

@@ -69,4 +69,15 @@ define([
 
   });
 
+  describe('relative time to date parsing', function() {
+    it('should handle negative time', function() {
+      var date = kbn.parseDateMath('-2d', new Date(2014,1,5));
+      expect(date.getTime()).to.equal(new Date(2014, 1, 3).getTime());
+    });
+    it('should handle multiple math expressions', function() {
+      var date = kbn.parseDateMath('-2d-6h', new Date(2014, 1, 5));
+      expect(date.toString()).to.equal(new Date(2014, 1, 2, 18).toString());
+    });
+  });
+
 });