Explorar el Código

Merge branch 'master' into variable-value-formatting-rethink

Torkel Ödegaard hace 9 años
padre
commit
f44d6e063c
Se han modificado 49 ficheros con 587 adiciones y 120 borrados
  1. 2 0
      CHANGELOG.md
  2. 2 0
      docker/blocks/elastic/elasticsearch.yml
  3. 0 1
      docker/blocks/elastic/elasticsearch/config/.placeholder
  4. 2 0
      docker/blocks/elastic/fig
  5. 1 0
      docs/sources/datasources/opentsdb.md
  6. 3 2
      pkg/api/api.go
  7. 2 2
      pkg/api/index.go
  8. 1 9
      pkg/services/sqlstore/sqlstore.go
  9. 3 0
      public/app/core/directives/ng_model_on_blur.js
  10. 35 0
      public/app/core/utils/file_export.ts
  11. 42 15
      public/app/core/utils/kbn.js
  12. 1 4
      public/app/features/annotations/editor_ctrl.js
  13. 1 1
      public/app/features/annotations/partials/editor.html
  14. 1 0
      public/app/features/dashboard/all.js
  15. 17 8
      public/app/features/dashboard/dashboardLoaderSrv.js
  16. 5 1
      public/app/features/dashboard/dashboardSrv.js
  17. 41 0
      public/app/features/dashboard/impressionStore.ts
  18. 1 1
      public/app/features/dashboard/submenu/submenu.html
  19. 5 1
      public/app/features/datasources/partials/list.html
  20. 7 3
      public/app/features/panel/metrics_panel_ctrl.ts
  21. 0 1
      public/app/features/panel/panel_directive.ts
  22. 3 4
      public/app/features/plugins/page_ctrl.ts
  23. 1 1
      public/app/features/plugins/partials/edit.html
  24. 1 1
      public/app/features/plugins/partials/page.html
  25. 18 6
      public/app/features/templating/partials/editor.html
  26. 1 1
      public/app/features/templating/templateValuesSrv.js
  27. 4 4
      public/app/partials/inspector.html
  28. 1 1
      public/app/partials/metrics.html
  29. 21 0
      public/app/plugins/datasource/opentsdb/config_ctrl.ts
  30. 45 12
      public/app/plugins/datasource/opentsdb/datasource.js
  31. 1 4
      public/app/plugins/datasource/opentsdb/module.ts
  32. 11 0
      public/app/plugins/datasource/opentsdb/partials/config.html
  33. 72 10
      public/app/plugins/datasource/opentsdb/partials/query.editor.html
  34. 88 2
      public/app/plugins/datasource/opentsdb/query_ctrl.ts
  35. 1 1
      public/app/plugins/datasource/opentsdb/specs/datasource-specs.ts
  36. 86 0
      public/app/plugins/datasource/opentsdb/specs/query-ctrl-specs.ts
  37. 3 3
      public/app/plugins/datasource/prometheus/metric_find_query.js
  38. 16 0
      public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts
  39. 1 1
      public/app/plugins/panel/dashlist/module.html
  40. 15 1
      public/app/plugins/panel/dashlist/module.ts
  41. 6 1
      public/app/plugins/panel/graph/module.ts
  42. 2 2
      public/app/plugins/panel/singlestat/editor.html
  43. 2 2
      public/app/plugins/panel/singlestat/module.ts
  44. 9 3
      public/app/plugins/panel/singlestat/specs/singlestat-specs.ts
  45. 2 2
      public/dashboards/home.json
  46. 1 5
      public/sass/base/_fonts.scss
  47. 1 1
      public/sass/base/_type.scss
  48. 1 1
      public/sass/components/_color_picker.scss
  49. 2 2
      public/test/core/utils/kbn_specs.js

+ 2 - 0
CHANGELOG.md

@@ -8,6 +8,7 @@
 * **Prometheus**: Prometheus annotation support, closes[#2883](https://github.com/grafana/grafana/pull/2883)
 * **Cli**: New cli tool for downloading and updating plugins
 * **Annotations**: Annotations can now contain links that can be clicked (you can navigate on to annotation popovers), closes [#1588](https://github.com/grafana/grafana/issues/1588)
+* **Opentsdb**: Opentsdb 2.2 filters support, closes[#3077](https://github.com/grafana/grafana/issues/3077)
 
 ### Breaking changes
 * **Plugin API**: Both datasource and panel plugin api (and plugin.json schema) have been updated, requiring an update to plugins. See [plugin api](https://github.com/grafana/grafana/blob/master/public/app/plugins/plugin_api.md) for more info.
@@ -21,6 +22,7 @@
 * **Admin**: Admin can now have global overview of Grafana setup, closes [#3812](https://github.com/grafana/grafana/issues/3812)
 * **graph**: Right side legend height is now fixed at row height, closes [#1277](https://github.com/grafana/grafana/issues/1277)
 * **Table**: All content in table panel is now html escaped, closes [#3673](https://github.com/grafana/grafana/issues/3673)
+* **graph**: Template variables can now be used in TimeShift and TimeFrom, closes[#1960](https://github.com/grafana/grafana/issues/1960)
 
 ### Bug fixes
 * **Playlist**: Fix for memory leak when running a playlist, closes [#3794](https://github.com/grafana/grafana/pull/3794)

+ 2 - 0
docker/blocks/elastic/elasticsearch.yml

@@ -0,0 +1,2 @@
+script.inline: on
+script.indexed: on

+ 0 - 1
docker/blocks/elastic/elasticsearch/config/.placeholder

@@ -1 +0,0 @@
-Ensure the existence of the parent folder.

+ 2 - 0
docker/blocks/elastic/fig

@@ -4,3 +4,5 @@ elasticsearch:
   ports:
     - "9200:9200"
     - "9300:9300"
+  volumes:
+    - ./blocks/elastic/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml

+ 1 - 0
docs/sources/datasources/opentsdb.md

@@ -23,6 +23,7 @@ Name | The data source name, important that this is the same as in Grafana v1.x
 Default | Default data source means that it will be pre-selected for new panels.
 Url | The http protocol, ip and port of you opentsdb server (default port is usually 4242)
 Access | Proxy = access via Grafana backend, Direct = access directory from browser.
+Version | Version = opentsdb version, either <=2.1 or 2.2
 
 ## Query editor
 Open a graph in edit mode by click the title.

+ 3 - 2
pkg/api/api.go

@@ -43,8 +43,9 @@ func Register(r *macaron.Macaron) {
 	r.Get("/admin/orgs/edit/:id", reqGrafanaAdmin, Index)
 	r.Get("/admin/stats", reqGrafanaAdmin, Index)
 
-	r.Get("/apps", reqSignedIn, Index)
-	r.Get("/apps/edit/*", reqSignedIn, Index)
+	r.Get("/plugins", reqSignedIn, Index)
+	r.Get("/plugins/:id/edit", reqSignedIn, Index)
+	r.Get("/plugins/:id/page/:page", reqSignedIn, Index)
 
 	r.Get("/dashboard/*", reqSignedIn, Index)
 	r.Get("/dashboard-solo/*", reqSignedIn, Index)

+ 2 - 2
pkg/api/index.go

@@ -85,13 +85,13 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
 		if plugin.Pinned {
 			pageLink := &dtos.NavLink{
 				Text: plugin.Name,
-				Url:  setting.AppSubUrl + "/apps/" + plugin.Id + "/edit",
+				Url:  setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit",
 				Img:  plugin.Info.Logos.Small,
 			}
 
 			for _, page := range plugin.Pages {
 				pageLink.Children = append(pageLink.Children, &dtos.NavLink{
-					Url:  setting.AppSubUrl + "/apps/" + plugin.Id + "/page/" + page.Slug,
+					Url:  setting.AppSubUrl + "/plugins/" + plugin.Id + "/page/" + page.Slug,
 					Text: page.Name,
 				})
 			}

+ 1 - 9
pkg/services/sqlstore/sqlstore.go

@@ -77,7 +77,7 @@ func NewEngine() {
 		log.Fatal(3, "Sqlstore: Fail to connect to database: %v", err)
 	}
 
-	err = SetEngine(x, true)
+	err = SetEngine(x, setting.Env == setting.DEV)
 
 	if err != nil {
 		log.Fatal(3, "fail to initialize orm engine: %v", err)
@@ -105,14 +105,6 @@ func SetEngine(engine *xorm.Engine, enableLog bool) (err error) {
 			return fmt.Errorf("sqlstore.init(fail to create xorm.log): %v", err)
 		}
 		x.Logger = xorm.NewSimpleLogger(f)
-
-		if setting.Env == setting.DEV {
-			x.ShowSQL = false
-			x.ShowInfo = false
-			x.ShowDebug = false
-			x.ShowErr = true
-			x.ShowWarn = true
-		}
 	}
 
 	return nil

+ 3 - 0
public/app/core/directives/ng_model_on_blur.js

@@ -47,6 +47,9 @@ function (coreModule, kbn, rangeUtil) {
           if (ctrl.$isEmpty(modelValue)) {
             return true;
           }
+          if (viewValue.indexOf('$') === 0) {
+            return true; // allow template variable
+          }
           var info = rangeUtil.describeTextRange(viewValue);
           return info.invalid !== true;
         };

+ 35 - 0
public/app/core/utils/file_export.ts

@@ -14,6 +14,41 @@ export function exportSeriesListToCsv(seriesList) {
     saveSaveBlob(text, 'grafana_data_export.csv');
 };
 
+export function exportSeriesListToCsvColumns(seriesList) {
+    var text = 'Time;';
+    // add header
+    _.each(seriesList, function(series) {
+        text += series.alias + ';';
+    });
+    text = text.substring(0,text.length-1);
+    text += '\n';
+
+    // process data
+    var dataArr = [[]];
+    var sIndex = 1;
+    _.each(seriesList, function(series) {
+        var cIndex = 0;
+        dataArr.push([]);
+        _.each(series.datapoints, function(dp) {
+            dataArr[0][cIndex] = new Date(dp[1]).toISOString();
+            dataArr[sIndex][cIndex] = dp[0];
+            cIndex++;
+        });
+        sIndex++;
+    });
+
+    // make text
+    for (var i = 0; i < dataArr[0].length; i++) {
+        text += dataArr[0][i] + ';';
+        for (var j = 1; j < dataArr.length; j++) {
+            text += dataArr[j][i] + ';';
+        }
+        text = text.substring(0,text.length-1);
+        text += '\n';
+    }
+    saveSaveBlob(text, 'grafana_data_export.csv');
+};
+
 export function exportTableDataToCsv(table) {
     var text = '';
     // add header

+ 42 - 15
public/app/core/utils/kbn.js

@@ -12,39 +12,66 @@ function($, _) {
 
   kbn.round_interval = function(interval) {
     switch (true) {
-    // 0.5s
-    case (interval <= 500):
+    // 0.3s
+    case (interval <= 300):
       return 100;       // 0.1s
-    // 5s
-    case (interval <= 5000):
+    // 0.75s
+    case (interval <= 750):
+      return 500;       // 0.5s
+    // 1.5s
+    case (interval <= 1500):
       return 1000;      // 1s
+    // 3.5s
+    case (interval <= 3500):
+      return 2000;      // 2s
     // 7.5s
     case (interval <= 7500):
       return 5000;      // 5s
-    // 15s
-    case (interval <= 15000):
+    // 12.5s
+    case (interval <= 12500):
       return 10000;     // 10s
+    // 17.5s
+    case (interval <= 17500):
+      return 15000;     // 15s
+    // 25s
+    case (interval <= 25000):
+      return 20000;     // 20s
     // 45s
     case (interval <= 45000):
       return 30000;     // 30s
-    // 3m
-    case (interval <= 180000):
+    // 1.5m
+    case (interval <= 90000):
       return 60000;     // 1m
-    // 9m
+    // 3.5m
+    case (interval <= 210000):
+      return 120000;    // 2m
+    // 7.5m
     case (interval <= 450000):
       return 300000;    // 5m
-    // 20m
-    case (interval <= 1200000):
+    // 12.5m
+    case (interval <= 750000):
       return 600000;    // 10m
+    // 12.5m
+    case (interval <= 1050000):
+      return 900000;    // 15m
+    // 25m
+    case (interval <= 1500000):
+      return 1200000;   // 20m
     // 45m
     case (interval <= 2700000):
       return 1800000;   // 30m
-    // 2h
-    case (interval <= 7200000):
+    // 1.5h
+    case (interval <= 5400000):
       return 3600000;   // 1h
-    // 6h
-    case (interval <= 21600000):
+    // 2.5h
+    case (interval <= 9000000):
+      return 7200000;   // 2h
+    // 4.5h
+    case (interval <= 16200000):
       return 10800000;  // 3h
+    // 9h
+    case (interval <= 32400000):
+      return 21600000;  // 6h
     // 24h
     case (interval <= 86400000):
       return 43200000;  // 12h

+ 1 - 4
public/app/features/annotations/editor_ctrl.js

@@ -12,10 +12,7 @@ function (angular, _, $) {
     var annotationDefaults = {
       name: '',
       datasource: null,
-      showLine: true,
-      iconColor: '#C0C6BE',
-      lineColor: 'rgba(255, 96, 96, 0.592157)',
-      iconSize: 13,
+      iconColor: 'rgba(255, 96, 96, 1)',
       enable: true
     };
 

+ 1 - 1
public/app/features/annotations/partials/editor.html

@@ -76,8 +76,8 @@
 					<div class="gf-form">
 						<label class="gf-form-label">
 							<span>Color</span>
-							<spectrum-picker ng-model="currentAnnotation.iconColor"></spectrum-picker>
 						</label>
+						<spectrum-picker class="gf-form-input" ng-model="currentAnnotation.iconColor"></spectrum-picker>
 					</div>
 				</div>
 			</div>

+ 1 - 0
public/app/features/dashboard/all.js

@@ -16,4 +16,5 @@ define([
   './graphiteImportCtrl',
   './dynamicDashboardSrv',
   './importCtrl',
+  './impressionStore',
 ], function () {});

+ 17 - 8
public/app/features/dashboard/dashboardLoaderSrv.js

@@ -5,8 +5,9 @@ define([
   'jquery',
   'app/core/utils/kbn',
   'app/core/utils/datemath',
+  './impressionStore',
 ],
-function (angular, moment, _, $, kbn, dateMath) {
+function (angular, moment, _, $, kbn, dateMath, impressionStore) {
   'use strict';
 
   var module = angular.module('grafana.services');
@@ -24,19 +25,27 @@ function (angular, moment, _, $, kbn, dateMath) {
     };
 
     this.loadDashboard = function(type, slug) {
-      if (type === 'script') {
-        return this._loadScriptedDashboard(slug);
-      }
+      var promise;
 
-      if (type === 'snapshot') {
-        return backendSrv.get('/api/snapshots/' + $routeParams.slug).catch(function() {
+      if (type === 'script') {
+        promise = this._loadScriptedDashboard(slug);
+      } else if (type === 'snapshot') {
+        promise = backendSrv.get('/api/snapshots/' + $routeParams.slug).catch(function() {
           return {meta:{isSnapshot: true, canSave: false, canEdit: false}, dashboard: {title: 'Snapshot not found'}};
         });
+      } else {
+        promise = backendSrv.getDashboard($routeParams.type, $routeParams.slug)
+          .catch(function() {
+            return self._dashboardLoadFailed("Not found");
+          });
       }
 
-      return backendSrv.getDashboard($routeParams.type, $routeParams.slug).catch(function() {
-        return self._dashboardLoadFailed("Not found");
+      promise.then(function(result) {
+        impressionStore.impressions.addDashboardImpression(slug);
+        return result;
       });
+
+      return promise;
     };
 
     this._loadScriptedDashboard = function(file) {

+ 5 - 1
public/app/features/dashboard/dashboardSrv.js

@@ -140,7 +140,11 @@ function (angular, $, _, moment) {
     };
 
     p.isSubmenuFeaturesEnabled = function() {
-      return this.templating.list.length > 0 || this.annotations.list.length > 0 || this.links.length > 0;
+      var visableTemplates = _.filter(this.templating.list, function(template) {
+        return template.hideVariable === undefined || template.hideVariable === false;
+      });
+
+      return visableTemplates.length > 0 || this.annotations.list.length > 0 || this.links.length > 0;
     };
 
     p.getPanelInfoById = function(panelId) {

+ 41 - 0
public/app/features/dashboard/impressionStore.ts

@@ -0,0 +1,41 @@
+///<reference path="../../headers/common.d.ts" />
+
+import store from 'app/core/store';
+import _ from 'lodash';
+
+export class ImpressionsStore {
+  constructor() {}
+
+  addDashboardImpression(slug) {
+    var impressions = [];
+    if (store.exists("dashboard_impressions")) {
+      impressions = JSON.parse(store.get("dashboard_impressions"));
+      if (!_.isArray(impressions)) {
+        impressions = [];
+      }
+    }
+
+    var exists = impressions.indexOf(slug);
+    if (exists >= 0) {
+      impressions.splice(exists, 1);
+    }
+
+    impressions.unshift(slug);
+
+    if (impressions.length > 20) {
+      impressions.shift();
+    }
+    store.set("dashboard_impressions", JSON.stringify(impressions));
+  }
+
+  getDashboardOpened() {
+    var k = store.get("dashboard_impressions");
+    return JSON.parse(k);
+  }
+}
+
+var impressions = new ImpressionsStore();
+
+export {
+  impressions
+};

+ 1 - 1
public/app/features/dashboard/submenu/submenu.html

@@ -1,6 +1,6 @@
 <div class="submenu-controls">
 	<ul ng-if="ctrl.dashboard.templating.list.length > 0">
-		<li ng-repeat="variable in ctrl.variables" class="submenu-item">
+		<li ng-repeat="variable in ctrl.variables" ng-show="!variable.hideVariable" class="submenu-item">
 			<span class="submenu-item-label template-variable " ng-show="!variable.hideLabel">
 				{{variable.label || variable.name}}:
 			</span>

+ 5 - 1
public/app/features/datasources/partials/list.html

@@ -23,6 +23,7 @@
 			<thead>
 				<tr>
 					<th><strong>name</strong></th>
+					<th><strong>type</strong></th>
 					<th><strong>url</strong></th>
 					<th style="width: 60px;"></th>
 					<th style="width: 85px;"></th>
@@ -37,7 +38,10 @@
 						</a>
 					</td>
 					<td>
-						<span class="ellipsis">{{ds.url}}</span>
+						<span>{{ds.type}}</span>
+					</td>
+					<td>
+						<span>{{ds.url}}</span>
 					</td>
 					<td class="text-center">
 						<span ng-if="ds.isDefault">

+ 7 - 3
public/app/features/panel/metrics_panel_ctrl.ts

@@ -17,6 +17,7 @@ class MetricsPanelCtrl extends PanelCtrl {
   $timeout: any;
   datasourceSrv: any;
   timeSrv: any;
+  templateSrv: any;
   timing: any;
   range: any;
   rangeRaw: any;
@@ -34,6 +35,7 @@ class MetricsPanelCtrl extends PanelCtrl {
     this.$q = $injector.get('$q');
     this.datasourceSrv = $injector.get('datasourceSrv');
     this.timeSrv = $injector.get('timeSrv');
+    this.templateSrv = $injector.get('templateSrv');
 
     if (!this.panel.targets) {
       this.panel.targets = [{}];
@@ -119,7 +121,8 @@ class MetricsPanelCtrl extends PanelCtrl {
 
     // check panel time overrrides
     if (this.panel.timeFrom) {
-      var timeFromInfo = rangeUtil.describeTextRange(this.panel.timeFrom);
+      var timeFromInterpolated = this.templateSrv.replace(this.panel.timeFrom, this.panel.scopedVars);
+      var timeFromInfo = rangeUtil.describeTextRange(timeFromInterpolated);
       if (timeFromInfo.invalid) {
         this.timeInfo = 'invalid time override';
         return;
@@ -136,13 +139,14 @@ class MetricsPanelCtrl extends PanelCtrl {
     }
 
     if (this.panel.timeShift) {
-      var timeShiftInfo = rangeUtil.describeTextRange(this.panel.timeShift);
+      var timeShiftInterpolated = this.templateSrv.replace(this.panel.timeShift, this.panel.scopedVars);
+      var timeShiftInfo = rangeUtil.describeTextRange(timeShiftInterpolated);
       if (timeShiftInfo.invalid) {
         this.timeInfo = 'invalid timeshift';
         return;
       }
 
-      var timeShift = '-' + this.panel.timeShift;
+      var timeShift = '-' + timeShiftInterpolated;
       this.timeInfo += ' timeshift ' + timeShift;
       this.range.from = dateMath.parseDateMath(timeShift, this.range.from, false);
       this.range.to = dateMath.parseDateMath(timeShift, this.range.to, true);

+ 0 - 1
public/app/features/panel/panel_directive.ts

@@ -31,7 +31,6 @@ var panelTemplate = `
     <div class="tabbed-view tabbed-view--panel-edit">
       <div class="tabbed-view-header">
         <h2 class="tabbed-view-title">
-          <i ng-class="ctrl.icon"></i>
           {{ctrl.pluginName}}
         </h2>
 

+ 3 - 4
public/app/features/plugins/page_ctrl.ts

@@ -5,14 +5,13 @@ import _ from 'lodash';
 
 export class AppPageCtrl {
   page: any;
-  appId: any;
+  pluginId: any;
   appModel: any;
 
   /** @ngInject */
   constructor(private backendSrv, private $routeParams: any, private $rootScope) {
-    this.appId = $routeParams.appId;
-
-    this.backendSrv.get(`/api/org/apps/${this.appId}/settings`).then(app => {
+    this.pluginId = $routeParams.pluginId;
+    this.backendSrv.get(`/api/org/plugins/${this.pluginId}/settings`).then(app => {
       this.appModel = app;
       this.page = _.findWhere(app.pages, {slug: this.$routeParams.slug});
       if (!this.page) {

+ 1 - 1
public/app/features/plugins/partials/edit.html

@@ -72,7 +72,7 @@
 						{{ds.name}}
 					</li>
 					<li ng-repeat="page in ctrl.model.pages">
-						<a href="apps/{{ctrl.appId}}/page/{{page.slug}}" class="external-link">{{page.name}}</a>
+						<a href="plugins/{{ctrl.pluginId}}/page/{{page.slug}}" class="external-link">{{page.name}}</a>
 					</li>
 				</ul>
 			</section>

+ 1 - 1
public/app/features/plugins/partials/page.html

@@ -1,4 +1,4 @@
-<navbar icon="icon-gf icon-gf-apps" title="{{ctrl.appModel.name}}" title-url="apps/{{ctrl.appId}}/edit">
+<navbar icon="icon-gf icon-gf-apps" title="{{ctrl.appModel.name}}" title-url="plugins/{{ctrl.pluginId}}/edit">
 </navbar>
 
 <div class="page-container">

+ 18 - 6
public/app/features/templating/partials/editor.html

@@ -91,11 +91,17 @@
 						</div>
 					</div>
 				</div>
-				<div class="gf-form">
-					<span class="gf-form-label width-7">Label</span>
-					<input type="text" class="gf-form-input max-width-14" ng-model='current.label' placeholder="optional display name"></input>
-					<editor-checkbox class="width-13" text="Hide label" model="current.hideLabel" change="runQuery()"></editor-checkbox>
+				<div class="gf-form-inline">
+					<div class="gf-form">
+						<span class="gf-form-label width-7">Label</span>
+						<input type="text" class="gf-form-input max-width-14" ng-model='current.label' placeholder="optional display name"></input>
+					</div>
+					<div class="gf-form">
+						<editor-checkbox class="width-10" text="Hide label" model="current.hideLabel" change="runQuery()"></editor-checkbox>
+						<editor-checkbox class="width-11" text="Hide variable" model="current.hideVariable" change="runQuery()"></editor-checkbox>
+					</div>
 				</div>
+
 			</div>
 
 			<h5 class="section-heading">Value Options</h5>
@@ -109,10 +115,16 @@
 					<span class="gf-form-label" ng-show="current.auto">
 						Auto interval steps <tip>How many times should the current time range be divided to calculate the value</tip>
 					</span>
-					<div class="gf-form-select-wrapper max-width-10">
-						<select class="gf-form-input" ng-model="current.auto_count" ng-options="f for f in [3,5,10,30,50,100,200]" ng-change="runQuery()"></select>
+					<div class="gf-form-select-wrapper max-width-10" ng-show="current.auto">
+						<select class="gf-form-input" ng-model="current.auto_count" ng-options="f for f in [2,3,4,5,10,20,30,40,50,100,200,300,400,500]" ng-change="runQuery()"></select>
 					</div>
 				</div>
+				<div class="gf-form">
+					<span class="gf-form-label" ng-show="current.auto">
+						Auto interval min value <tip>The calculated value will not go below this threshold</tip>
+					</span>
+					<input type="text" class="gf-form-input max-width-10" ng-show="current.auto" ng-model="current.auto_min" ng-change="runQuery()"></input>
+				</div>
 			</div>
 
 			<div ng-show="current.type === 'custom'" class="gf-form-group">

+ 1 - 1
public/app/features/templating/templateValuesSrv.js

@@ -60,7 +60,7 @@ function (angular, _, kbn) {
         variable.options.unshift({ text: 'auto', value: '$__auto_interval' });
       }
 
-      var interval = kbn.calculateInterval(timeSrv.timeRange(), variable.auto_count);
+      var interval = kbn.calculateInterval(timeSrv.timeRange(), variable.auto_count, (variable.auto_min ? ">"+variable.auto_min : null));
       templateSrv.setGrafanaVariable('$__auto_interval', interval);
     };
 

+ 4 - 4
public/app/partials/inspector.html

@@ -18,8 +18,8 @@
 	<div class="gf-box-body">
 
 		<div ng-if="editor.index == 0">
-			<h5>Request details</h5>
-			<table class="table table-striped small inspector-request-table">
+			<h5 class="section-heading">Request details</h5>
+			<table class="filter-table gf-form-group">
 				<tr>
 					<td>Url</td>
 					<td>{{inspector.error.config.url}}</td>
@@ -38,8 +38,8 @@
 				</tr>
 			</table>
 
-			<h5>Request parameters</h5>
-			<table class="table table-striped small inspector-request-table">
+			<h5 class="section-heading">Request parameters</h5>
+			<table class="filter-table">
 				<tr ng-repeat="param in request_parameters">
 					<td>
 						{{param.key}}

+ 1 - 1
public/app/partials/metrics.html

@@ -37,7 +37,7 @@
 
 </div>
 
-<div class="editor-row" style="margin-top: 30px">
+<div class="editor-row">
 
 	<div class="pull-right dropdown" style="margin-right: 10px;">
 		<button class="btn btn-inverse dropdown-toggle" data-toggle="dropdown" bs-tooltip="'Datasource'">

+ 21 - 0
public/app/plugins/datasource/opentsdb/config_ctrl.ts

@@ -0,0 +1,21 @@
+///<reference path="../../../headers/common.d.ts" />
+
+import angular from 'angular';
+import _ from 'lodash';
+
+export class OpenTsConfigCtrl {
+  static templateUrl = 'public/app/plugins/datasource/opentsdb/partials/config.html';
+  current: any;
+
+  /** @ngInject */
+  constructor($scope) {
+    this.current.jsonData = this.current.jsonData || {};
+    this.current.jsonData.tsdbVersion = this.current.jsonData.tsdbVersion || 1;
+  }
+
+  tsdbVersions = [
+    {name: '<=2.1', value: 1},
+    {name: '2.2', value: 2},
+  ];
+
+}

+ 45 - 12
public/app/plugins/datasource/opentsdb/datasource.js

@@ -14,6 +14,8 @@ function (angular, _, dateMath) {
     this.name = instanceSettings.name;
     this.withCredentials = instanceSettings.withCredentials;
     this.basicAuth = instanceSettings.basicAuth;
+    instanceSettings.jsonData = instanceSettings.jsonData || {};
+    this.tsdbVersion = instanceSettings.jsonData.tsdbVersion || 1;
     this.supportMetrics = true;
     this.tagKeys = {};
 
@@ -39,9 +41,15 @@ function (angular, _, dateMath) {
 
       var groupByTags = {};
       _.each(queries, function(query) {
-        _.each(query.tags, function(val, key) {
-          groupByTags[key] = true;
-        });
+        if (query.filters && query.filters.length > 0) {
+          _.each(query.filters, function(val) {
+            groupByTags[val.tagk] = true;
+          });
+        } else {
+          _.each(query.tags, function(val, key) {
+            groupByTags[key] = true;
+          });
+        }
       });
 
       return this.performTimeSeriesQuery(queries, start, end).then(function(response) {
@@ -88,6 +96,7 @@ function (angular, _, dateMath) {
       // In case the backend is 3rd-party hosted and does not suport OPTIONS, urlencoded requests
       // go as POST rather than OPTIONS+POST
       options.headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
+
       return backendSrv.datasourceRequest(options);
     };
 
@@ -215,7 +224,7 @@ function (angular, _, dateMath) {
     this.getAggregators = function() {
       if (aggregatorsPromise) { return aggregatorsPromise; }
 
-      aggregatorsPromise =  this._get('/api/aggregators').then(function(result) {
+      aggregatorsPromise = this._get('/api/aggregators').then(function(result) {
         if (result.data && _.isArray(result.data)) {
           return result.data.sort();
         }
@@ -224,6 +233,19 @@ function (angular, _, dateMath) {
       return aggregatorsPromise;
     };
 
+    var filterTypesPromise = null;
+    this.getFilterTypes = function() {
+      if (filterTypesPromise) { return filterTypesPromise; }
+
+      filterTypesPromise = this._get('/api/config/filters').then(function(result) {
+        if (result.data) {
+          return Object.keys(result.data).sort();
+        }
+        return [];
+      });
+      return filterTypesPromise;
+    };
+
     function transformMetricData(md, groupByTags, target, options) {
       var metricLabel = createMetricLabel(md, target, groupByTags, options);
       var dps = [];
@@ -307,10 +329,14 @@ function (angular, _, dateMath) {
         }
       }
 
-      query.tags = angular.copy(target.tags);
-      if(query.tags){
-        for(var key in query.tags){
-          query.tags[key] = templateSrv.replace(query.tags[key], options.scopedVars);
+      if (target.filters && target.filters.length > 0) {
+        query.filters = angular.copy(target.filters);
+      } else {
+        query.tags = angular.copy(target.tags);
+        if(query.tags){
+          for(var key in query.tags){
+            query.tags[key] = templateSrv.replace(query.tags[key], options.scopedVars);
+          }
         }
       }
 
@@ -321,11 +347,18 @@ function (angular, _, dateMath) {
       var interpolatedTagValue;
       return _.map(metrics, function(metricData) {
         return _.findIndex(options.targets, function(target) {
-          return target.metric === metricData.metric &&
+          if (target.filters && target.filters.length > 0) {
+            return target.metric === metricData.metric &&
+            _.all(target.filters, function(filter) {
+              return filter.tagk === interpolatedTagValue === "*";
+            });
+          } else {
+            return target.metric === metricData.metric &&
             _.all(target.tags, function(tagV, tagK) {
-            interpolatedTagValue = templateSrv.replace(tagV, options.scopedVars);
-            return metricData.tags[tagK] === interpolatedTagValue || interpolatedTagValue === "*";
-          });
+              interpolatedTagValue = templateSrv.replace(tagV, options.scopedVars);
+              return metricData.tags[tagK] === interpolatedTagValue || interpolatedTagValue === "*";
+            });
+          }
         });
       });
     }

+ 1 - 4
public/app/plugins/datasource/opentsdb/module.ts

@@ -1,9 +1,6 @@
 import {OpenTsDatasource} from './datasource';
 import {OpenTsQueryCtrl} from './query_ctrl';
-
-class OpenTsConfigCtrl {
-  static templateUrl = 'partials/config.html';
-}
+import {OpenTsConfigCtrl} from './config_ctrl';
 
 export {
   OpenTsDatasource as Datasource,

+ 11 - 0
public/app/plugins/datasource/opentsdb/partials/config.html

@@ -1,2 +1,13 @@
 <datasource-http-settings current="ctrl.current"></datasource-http-settings>
 
+<br>
+<h5>Opentsdb settings</h5>
+<div class="gf-form">
+  <span class="gf-form-label width-7">
+    Version
+  </span>
+  <span class="gf-form-select-wrapper">
+    <select class="gf-form-input gf-size-auto" ng-model="ctrl.current.jsonData.tsdbVersion" ng-options="v.value as v.name for v in ctrl.tsdbVersions"></select>
+  </span>
+  <div class="clearfix"></div>
+</div>

+ 72 - 10
public/app/plugins/datasource/opentsdb/partials/query.editor.html

@@ -63,12 +63,11 @@
 			</select>
 		</li>
 
-		<li class="tight-form-item query-keyword" style="width: 59px">
+		<li class="tight-form-item query-keyword" style="width: 59px" ng-if="ctrl.tsdbVersion == 2">
 			Fill
-			<tip>Available since OpenTSDB 2.2</tip>
 		</li>
 
-		<li>
+		<li ng-if="ctrl.tsdbVersion == 2">
 			<select ng-model="ctrl.target.downsampleFillPolicy" class="tight-form-input input-small"
 				ng-options="agg for agg in ctrl.fillPolicies"
 				ng-change="ctrl.targetBlur()">
@@ -83,10 +82,67 @@
 	<div class="clearfix"></div>
 </div>
 
+<div class="tight-form" ng-if="ctrl.tsdbVersion == 2">
+  <ul class="tight-form-list" role="menu">
+    <li class="tight-form-item tight-form-align query-keyword" style="width: 100px">
+      Filters
+      <tip ng-if="ctrl.tsdbVersion == 2">Filters does not work with tags, either of the two will work but not both.</tip>
+    </li>
+    <li ng-repeat="fil in ctrl.target.filters track by $index" class="tight-form-item">
+      {{fil.tagk}}&nbsp;=&nbsp;{{fil.type}}&#40;{{fil.filter}}&#41;&nbsp;&#44&nbsp;groupBy&nbsp;=&nbsp;{{fil.groupBy}}
+      <a ng-click="ctrl.editFilter(fil, $index)">
+        <i class="fa fa-pencil"></i>
+      </a>
+      <a ng-click="ctrl.removeFilter($index)">
+        <i class="fa fa-remove"></i>
+      </a>
+    </li>
+    <li class="tight-form-item query-keyword" ng-hide="ctrl.addFilterMode">
+      <a ng-click="ctrl.addFilter()">
+        <i class="fa fa-plus"></i>
+      </a>
+    </li>
+
+    <li class="query-keyword" ng-show="ctrl.addFilterMode">
+      <input type="text" class="input-small tight-form-input" spellcheck='false'
+      bs-typeahead="ctrl.suggestTagKeys" data-min-length=0 data-items=100
+      ng-model="ctrl.target.currentFilterKey" placeholder="key"></input>
+
+      Type <select ng-model="ctrl.target.currentFilterType"
+      class="tight-form-input input-small"
+      ng-options="filType for filType in ctrl.filterTypes">
+      </select>
+ 
+      <input type="text" class="input-small tight-form-input"
+      spellcheck='false' bs-typeahead="ctrl.suggestTagValues"
+      data-min-length=0 data-items=100 ng-model="ctrl.target.currentFilterValue" placeholder="filter">
+      </input>
+
+      groupBy <editor-checkbox text="" model="ctrl.target.currentFilterGroupBy"></editor-checkbox>
+
+      <a bs-tooltip="ctrl.errors.filters"
+        style="color: rgb(229, 189, 28)"
+        ng-show="ctrl.errors.filters">
+        <i class="fa fa-warning"></i>
+      </a>
+ 
+      <a ng-click="ctrl.addFilter()" ng-hide="ctrl.errors.filters">
+        add filter
+      </a>
+      <a ng-click="ctrl.closeAddFilterMode()">
+        <i class="fa fa-remove"></i>
+      </a>
+
+    </li>
+  </ul>
+  <div class="clearfix"></div>
+</div>
+
 <div class="tight-form">
 	<ul class="tight-form-list" role="menu">
 		<li class="tight-form-item tight-form-align query-keyword" style="width: 100px">
 			Tags
+      <tip ng-if="ctrl.tsdbVersion == 2">Please use filters, tags are deprecated in opentsdb 2.2</tip>
 		</li>
 		<li ng-repeat="(key, value) in ctrl.target.tags track by $index" class="tight-form-item">
 			{{key}}&nbsp;=&nbsp;{{value}}
@@ -113,15 +169,21 @@
 			spellcheck='false' bs-typeahead="ctrl.suggestTagValues"
 			data-min-length=0 data-items=100 ng-model="ctrl.target.currentTagValue" placeholder="value">
 			</input>
-			<a ng-click="ctrl.addTag()">
+
+      <a bs-tooltip="ctrl.errors.tags"
+        style="color: rgb(229, 189, 28)"
+        ng-show="ctrl.errors.tags">
+        <i class="fa fa-warning"></i>
+      </a>
+
+			<a ng-click="ctrl.addTag()" ng-hide="ctrl.errors.tags">
 				add tag
 			</a>
-			<a bs-tooltip="ctrl.errors.tags"
-				style="color: rgb(229, 189, 28)"
-				ng-show="target.errors.tags">
-				<i class="fa fa-warning"></i>
-			</a>
-		</li>
+      <a ng-click="ctrl.closeAddTagMode()">
+        <i class="fa fa-remove"></i>
+      </a>
+		
+    </li>
 	</ul>
 	<div class="clearfix"></div>
 </div>

+ 88 - 2
public/app/plugins/datasource/opentsdb/query_ctrl.ts

@@ -8,6 +8,8 @@ export class OpenTsQueryCtrl extends QueryCtrl {
   static templateUrl = 'partials/query.editor.html';
   aggregators: any;
   fillPolicies: any;
+  filterTypes: any;
+  tsdbVersion: any;
   aggregator: any;
   downsampleInterval: any;
   downsampleAggregator: any;
@@ -17,6 +19,7 @@ export class OpenTsQueryCtrl extends QueryCtrl {
   suggestTagKeys: any;
   suggestTagValues: any;
   addTagMode: boolean;
+  addFilterMode: boolean;
 
   /** @ngInject **/
   constructor($scope, $injector) {
@@ -25,6 +28,9 @@ export class OpenTsQueryCtrl extends QueryCtrl {
     this.errors = this.validateTarget();
     this.aggregators = ['avg', 'sum', 'min', 'max', 'dev', 'zimsum', 'mimmin', 'mimmax'];
     this.fillPolicies = ['none', 'nan', 'null', 'zero'];
+    this.filterTypes = ['wildcard','iliteral_or','not_iliteral_or','not_literal_or','iwildcard','literal_or','regexp'];
+
+    this.tsdbVersion = this.datasource.tsdbVersion;
 
     if (!this.target.aggregator) {
       this.target.aggregator = 'sum';
@@ -38,8 +44,16 @@ export class OpenTsQueryCtrl extends QueryCtrl {
       this.target.downsampleFillPolicy = 'none';
     }
 
-    this.datasource.getAggregators().then(function(aggs) {
-      this.aggregators = aggs;
+    this.datasource.getAggregators().then((aggs) => {
+      if (aggs.length !== 0) {
+        this.aggregators = aggs;
+      }
+    });
+
+    this.datasource.getFilterTypes().then((filterTypes) => {
+      if (filterTypes.length !== 0) {
+        this.filterTypes = filterTypes;
+      }
     });
 
     // needs to be defined here as it is called from typeahead
@@ -70,6 +84,11 @@ export class OpenTsQueryCtrl extends QueryCtrl {
   }
 
   addTag() {
+
+    if (this.target.filters && this.target.filters.length > 0) {
+      this.errors.tags = "Please remove filters to use tags, tags and filters are mutually exclusive.";
+    }
+
     if (!this.addTagMode) {
       this.addTagMode = true;
       return;
@@ -103,6 +122,73 @@ export class OpenTsQueryCtrl extends QueryCtrl {
     this.addTag();
   }
 
+  closeAddTagMode() {
+    this.addTagMode = false;
+    return;
+  }
+
+  addFilter() {
+
+    if (this.target.tags && _.size(this.target.tags) > 0) {
+      this.errors.filters = "Please remove tags to use filters, tags and filters are mutually exclusive.";
+    }
+
+    if (!this.addFilterMode) {
+      this.addFilterMode = true;
+      return;
+    }
+
+    if (!this.target.filters) {
+      this.target.filters = [];
+    }
+
+    if (!this.target.currentFilterType) {
+      this.target.currentFilterType = 'iliteral_or';
+    }
+
+    if (!this.target.currentFilterGroupBy) {
+      this.target.currentFilterGroupBy = false;
+    }
+
+    this.errors = this.validateTarget();
+
+    if (!this.errors.filters) {
+      var currentFilter = {
+        type:    this.target.currentFilterType,
+        tagk:     this.target.currentFilterKey,
+        filter:   this.target.currentFilterValue,
+        groupBy: this.target.currentFilterGroupBy
+      };
+      this.target.filters.push(currentFilter);
+      this.target.currentFilterType = 'literal_or';
+      this.target.currentFilterKey = '';
+      this.target.currentFilterValue = '';
+      this.target.currentFilterGroupBy = false;
+      this.targetBlur();
+    }
+
+    this.addFilterMode = false;
+  }
+
+  removeFilter(index) {
+    this.target.filters.splice(index, 1);
+    this.targetBlur();
+  }
+
+  editFilter(fil, index) {
+    this.removeFilter(index);
+    this.target.currentFilterKey = fil.tagk;
+    this.target.currentFilterValue = fil.filter;
+    this.target.currentFilterType = fil.type;
+    this.target.currentFilterGroupBy = fil.groupBy;
+    this.addFilter();
+  }
+
+  closeAddFilterMode() {
+    this.addFilterMode = false;
+    return;
+  }
+
   validateTarget() {
     var errs: any = {};
 

+ 1 - 1
public/app/plugins/datasource/opentsdb/specs/datasource-specs.ts

@@ -4,7 +4,7 @@ import {OpenTsDatasource} from "../datasource";
 
 describe('opentsdb', function() {
   var ctx = new helpers.ServiceTestContext();
-  var instanceSettings = {url: '' };
+  var instanceSettings = {url: '', jsonData: { tsdbVersion: 1 }};
 
   beforeEach(angularMocks.module('grafana.core'));
   beforeEach(angularMocks.module('grafana.services'));

+ 86 - 0
public/app/plugins/datasource/opentsdb/specs/query-ctrl-specs.ts

@@ -0,0 +1,86 @@
+import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
+import helpers from 'test/specs/helpers';
+import {OpenTsQueryCtrl} from "../query_ctrl";
+
+describe('OpenTsQueryCtrl', function() {
+  var ctx = new helpers.ControllerTestContext();
+
+  beforeEach(angularMocks.module('grafana.core'));
+  beforeEach(angularMocks.module('grafana.services'));
+  beforeEach(ctx.providePhase(['backendSrv','templateSrv']));
+
+  beforeEach(ctx.providePhase());
+  beforeEach(angularMocks.inject(($rootScope, $controller, $q) => {
+    ctx.$q = $q;
+    ctx.scope = $rootScope.$new();
+    ctx.target = {target: ''};
+    ctx.panelCtrl = {panel: {}};
+    ctx.panelCtrl.refresh = sinon.spy();
+    ctx.datasource.getAggregators = sinon.stub().returns(ctx.$q.when([]));
+    ctx.datasource.getFilterTypes = sinon.stub().returns(ctx.$q.when([]));
+
+    ctx.ctrl = $controller(OpenTsQueryCtrl, {$scope: ctx.scope}, {
+      panelCtrl: ctx.panelCtrl,
+      datasource: ctx.datasource,
+      target: ctx.target,
+    });
+    ctx.scope.$digest();
+  }));
+
+  describe('init query_ctrl variables', function() {
+
+    it('filter types should be initialized', function() {
+      expect(ctx.ctrl.filterTypes.length).to.be(7);
+    });
+
+    it('aggregators should be initialized', function() {
+      expect(ctx.ctrl.aggregators.length).to.be(8);
+    });
+
+    it('fill policy options should be initialized', function() {
+      expect(ctx.ctrl.fillPolicies.length).to.be(4);
+    });
+
+  });
+
+  describe('when adding filters and tags', function() {
+
+    it('addTagMode should be false when closed', function() {
+      ctx.ctrl.addTagMode = true;
+      ctx.ctrl.closeAddTagMode();
+      expect(ctx.ctrl.addTagMode).to.be(false);
+    });
+
+    it('addFilterMode should be false when closed', function() {
+      ctx.ctrl.addFilterMode = true;
+      ctx.ctrl.closeAddFilterMode();
+      expect(ctx.ctrl.addFilterMode).to.be(false);
+    });
+
+    it('removing a tag from the tags list', function() {
+      ctx.ctrl.target.tags = {"tagk": "tag_key", "tagk2": "tag_value2"};
+      ctx.ctrl.removeTag("tagk");
+      expect(Object.keys(ctx.ctrl.target.tags).length).to.be(1);
+    });
+
+    it('removing a filter from the filters list', function() {
+      ctx.ctrl.target.filters = [{"tagk": "tag_key", "filter": "tag_value2", "type": "wildcard", "groupBy": true}];
+      ctx.ctrl.removeFilter(0);
+      expect(ctx.ctrl.target.filters.length).to.be(0);
+    });
+
+    it('adding a filter when tags exist should generate error', function() {
+      ctx.ctrl.target.tags = {"tagk": "tag_key", "tagk2": "tag_value2"};
+      ctx.ctrl.addFilter();
+      expect(ctx.ctrl.errors.filters).to.be('Please remove tags to use filters, tags and filters are mutually exclusive.');
+    });
+
+    it('adding a tag when filters exist should generate error', function() {
+      ctx.ctrl.target.filters = [{"tagk": "tag_key", "filter": "tag_value2", "type": "wildcard", "groupBy": true}];
+      ctx.ctrl.addTag();
+      expect(ctx.ctrl.errors.tags).to.be('Please remove filters to use tags, tags and filters are mutually exclusive.');
+    });
+
+  });
+
+});

+ 3 - 3
public/app/plugins/datasource/prometheus/metric_find_query.js

@@ -11,16 +11,16 @@ function (_, moment) {
   }
 
   PrometheusMetricFindQuery.prototype.process = function() {
-    var label_values_regex = /^label_values\(([^,]+)(?:,\s*(.+))?\)$/;
+    var label_values_regex = /^label_values\((?:(.+),\s*)?([a-zA-Z_][a-zA-Z0-9_]+)\)$/;
     var metric_names_regex = /^metrics\((.+)\)$/;
     var query_result_regex = /^query_result\((.+)\)$/;
 
     var label_values_query = this.query.match(label_values_regex);
     if (label_values_query) {
-      if (label_values_query[2]) {
+      if (label_values_query[1]) {
         return this.labelValuesQuery(label_values_query[2], label_values_query[1]);
       } else {
-        return this.labelValuesQuery(label_values_query[1], null);
+        return this.labelValuesQuery(label_values_query[2], null);
       }
     }
 

+ 16 - 0
public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts

@@ -48,6 +48,22 @@ describe('PrometheusMetricFindQuery', function() {
       ctx.$rootScope.$apply();
       expect(results.length).to.be(3);
     });
+    it('label_values(metric{label1="foo", label2="bar", label3="baz"}, resource) should generate series query', function() {
+      response = {
+        status: "success",
+        data: [
+          {__name__: "metric", resource: "value1"},
+          {__name__: "metric", resource: "value2"},
+          {__name__: "metric", resource: "value3"}
+        ]
+      };
+      ctx.$httpBackend.expect('GET', 'proxied/api/v1/series?match[]=metric').respond(response);
+      var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(metric, resource)');
+      pm.process().then(function(data) { results = data; });
+      ctx.$httpBackend.flush();
+      ctx.$rootScope.$apply();
+      expect(results.length).to.be(3);
+    });
     it('metrics(metric.*) should generate metric name query', function() {
       response = {
         status: "success",

+ 1 - 1
public/app/plugins/panel/dashlist/module.html

@@ -5,7 +5,7 @@
 				{{dash.title}}
 			</span>
 			<span class="dashlist-star">
-				<i class="fa" ng-class="{'fa-star': dash.isStarred, 'fa-star-o': !dash.isStarred}"></i>
+				<i class="fa" ng-class="{'fa-star': dash.isStarred, 'fa-star-o': dash.isStarred === false}"></i>
 			</span>
 		</a>
 	</div>

+ 15 - 1
public/app/plugins/panel/dashlist/module.ts

@@ -3,6 +3,7 @@
 import _ from 'lodash';
 import config from 'app/core/config';
 import {PanelCtrl} from 'app/plugins/sdk';
+import {impressions} from 'app/features/dashboard/impressionStore';
 
  // Set and populate defaults
 var panelDefaults = {
@@ -31,7 +32,7 @@ class DashListCtrl extends PanelCtrl {
 
   initEditMode() {
     super.initEditMode();
-    this.modes = ['starred', 'search'];
+    this.modes = ['starred', 'search', 'last viewed'];
     this.icon = "fa fa-star";
     this.addEditorTab('Options', () => {
       return {templateUrl: 'public/app/plugins/panel/dashlist/editor.html'};
@@ -41,6 +42,19 @@ class DashListCtrl extends PanelCtrl {
   refresh() {
     var params: any = {limit: this.panel.limit};
 
+    if (this.panel.mode === 'last viewed') {
+      var dashListNames = _.first(impressions.getDashboardOpened(), this.panel.limit).map((dashboard) => {
+        return {
+          title: dashboard,
+          uri: 'db/' + dashboard
+        };
+      });
+
+      this.dashList = dashListNames;
+      this.renderingCompleted();
+      return;
+    }
+
     if (this.panel.mode === 'starred') {
       params.starred = "true";
     } else {

+ 6 - 1
public/app/plugins/panel/graph/module.ts

@@ -125,7 +125,8 @@ class GraphCtrl extends MetricsPanelCtrl {
 
   getExtendedMenu() {
     var menu = super.getExtendedMenu();
-    menu.push({text: 'Export CSV', click: 'ctrl.exportCsv()'});
+    menu.push({text: 'Export CSV (series as rows)', click: 'ctrl.exportCsv()'});
+    menu.push({text: 'Export CSV (series as columns)', click: 'ctrl.exportCsvColumns()'});
     menu.push({text: 'Toggle legend', click: 'ctrl.toggleLegend()'});
     return menu;
   }
@@ -295,6 +296,10 @@ class GraphCtrl extends MetricsPanelCtrl {
   exportCsv() {
     fileExport.exportSeriesListToCsv(this.seriesList);
   }
+
+  exportCsvColumns() {
+    fileExport.exportSeriesListToCsvColumns(this.seriesList);
+  }
 }
 
 export {GraphCtrl, GraphCtrl as PanelCtrl}

+ 2 - 2
public/app/plugins/panel/singlestat/editor.html

@@ -167,13 +167,13 @@
 					<i class="fa fa-remove pointer" ng-click="ctrl.removeValueMap(map)"></i>
 				</li>
 				<li>
-					<input type="text" ng-model="ctrl.map.value" placeholder="value" class="input-mini tight-form-input" ng-blur="ctrl.render()">
+					<input type="text" ng-model="map.value" placeholder="value" class="input-mini tight-form-input" ng-blur="ctrl.render()">
 				</li>
 				<li class="tight-form-item">
 					<i class="fa fa-arrow-right"></i>
 				</li>
 				<li ng-repeat-end>
-					<input type="text" placeholder="text" ng-model="ctrl.map.text" class="input-mini tight-form-input" ng-blur="ctrl.render()">
+					<input type="text" placeholder="text" ng-model="map.text" class="input-mini tight-form-input" ng-blur="ctrl.render()">
 				</li>
 
 				<li>

+ 2 - 2
public/app/plugins/panel/singlestat/module.ts

@@ -50,7 +50,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
   unitFormats: any[];
 
   /** @ngInject */
-  constructor($scope, $injector, private $location, private linkSrv, private templateSrv) {
+  constructor($scope, $injector, private $location, private linkSrv) {
     super($scope, $injector);
     _.defaults(this.panel, panelDefaults);
   }
@@ -213,7 +213,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
 
       // value/number to text mapping
       var value = parseFloat(map.value);
-      if (value === data.value) {
+      if (value === data.valueRounded) {
         data.valueFormated = map.text;
         return;
       }

+ 9 - 3
public/app/plugins/panel/singlestat/specs/singlestat-specs.ts

@@ -69,14 +69,20 @@ describe('SingleStatCtrl', function() {
 
   singleStatScenario('When value to text mapping is specified', function(ctx) {
     ctx.setup(function() {
-      ctx.datapoints = [[10,1]];
+      ctx.datapoints = [[9.9,1]];
       ctx.ctrl.panel.valueMaps = [{value: '10', text: 'OK'}];
     });
 
+    it('value should remain', function() {
+      expect(ctx.data.value).to.be(9.9);
+    });
+
+    it('round should be rounded up', function() {
+      expect(ctx.data.valueRounded).to.be(10);
+    });
+
     it('Should replace value with text', function() {
-      expect(ctx.data.value).to.be(10);
       expect(ctx.data.valueFormated).to.be('OK');
     });
-
   });
 });

+ 2 - 2
public/dashboards/home.json

@@ -47,11 +47,11 @@
         {
           "id": 3,
           "limit": 10,
-          "mode": "search",
+          "mode": "last viewed",
           "query": "",
           "span": 6,
           "tags": [],
-          "title": "Dashboards",
+          "title": "Last 10 viewed dashboards",
           "type": "dashlist"
         }
       ],

+ 1 - 5
public/sass/base/_fonts.scss

@@ -8,7 +8,7 @@
     font-weight: normal;
     font-style: normal;
 }
- 
+
 .icon-gf {
     /* use !important to prevent issues with browser extensions that change fonts */
     font-family: 'grafana-icons' !important;
@@ -28,10 +28,6 @@
     vertical-align: middle;
 }
 
-
-
-
-
 .icon-gf-raintank_wordmark:before {
     content: "\e600";
 }

+ 1 - 1
public/sass/base/_type.scss

@@ -131,7 +131,7 @@ mark,
 // Unordered and Ordered lists
 ul, ol {
   padding: 0;
-  margin: 0 0 $line-height-base / 2 25px;
+  padding-left: $spacer;
 }
 ul ul,
 ul ol,

+ 1 - 1
public/sass/components/_color_picker.scss

@@ -31,7 +31,7 @@
   width: 15px;
   height: 15px;
   border: none;
-  margin-right: 5px;
+  margin: 0;
   float: left;
   z-index: 0;
 }

+ 2 - 2
public/test/core/utils/kbn_specs.js

@@ -127,7 +127,7 @@ define([
     it('10m 1600 resolution', function() {
       var range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') };
       var str = kbn.calculateInterval(range, 1600, null);
-      expect(str).to.be('100ms');
+      expect(str).to.be('500ms');
     });
 
     it('fixed user interval', function() {
@@ -145,7 +145,7 @@ define([
     it('large time range and user low limit', function() {
       var range = { from: dateMath.parse('now-14d'), to: dateMath.parse('now') };
       var str = kbn.calculateInterval(range, 1000, '>10s');
-      expect(str).to.be('30m');
+      expect(str).to.be('20m');
     });
   });
 });