Bladeren bron

Merge branch 'master' into export-dashboard

Conflicts:
	.floo
	.flooignore
Torkel Ödegaard 9 jaren geleden
bovenliggende
commit
6632f883c0
42 gewijzigde bestanden met toevoegingen van 337 en 128 verwijderingen
  1. 2 2
      .floo
  2. 5 2
      .flooignore
  3. 13 1
      CHANGELOG.md
  4. 1 0
      README.md
  5. 3 0
      conf/defaults.ini
  6. 3 0
      conf/sample.ini
  7. 7 0
      docker/blocks/graphite/fig
  8. 8 0
      docker/blocks/influxdb/fig
  9. 7 1
      docker/blocks/opentsdb/fig
  10. 10 0
      docker/blocks/prometheus/fig
  11. 3 3
      docs/sources/datasources/opentsdb.md
  12. 3 3
      docs/sources/installation/debian.md
  13. 4 4
      docs/sources/installation/rpm.md
  14. 1 1
      docs/sources/installation/windows.md
  15. 2 2
      latest.json
  16. 1 1
      package.json
  17. 1 1
      packaging/deb/default/grafana-server
  18. 1 1
      packaging/rpm/sysconfig/grafana-server
  19. 1 0
      pkg/api/frontendsettings.go
  20. 7 8
      pkg/cmd/grafana-cli/commands/upgrade_command.go
  21. 15 7
      pkg/cmd/grafana-cli/services/services.go
  22. 10 1
      pkg/plugins/queries.go
  23. 3 1
      pkg/services/sqlstore/preferences.go
  24. 2 0
      pkg/setting/setting.go
  25. 3 1
      public/app/app.ts
  26. 2 0
      public/app/core/utils/kbn.js
  27. 1 1
      public/app/features/panel/metrics_ds_selector.ts
  28. 7 3
      public/app/plugins/datasource/influxdb/datasource.ts
  29. 1 1
      public/app/plugins/datasource/influxdb/partials/query.options.html
  30. 2 1
      public/app/plugins/datasource/opentsdb/config_ctrl.ts
  31. 23 17
      public/app/plugins/datasource/opentsdb/datasource.js
  32. 3 3
      public/app/plugins/datasource/opentsdb/partials/query.editor.html
  33. 4 4
      public/app/plugins/datasource/prometheus/datasource.ts
  34. 1 1
      public/app/plugins/datasource/prometheus/query_ctrl.ts
  35. 16 7
      public/app/plugins/panel/graph/graph.js
  36. 15 3
      public/app/plugins/panel/graph/graph_tooltip.js
  37. 1 0
      public/app/plugins/panel/graph/module.ts
  38. 9 3
      public/app/plugins/panel/graph/tab_display.html
  39. 0 32
      public/app/plugins/panel/singlestat/editor.html
  40. 58 0
      public/app/plugins/panel/singlestat/mappings.html
  41. 53 12
      public/app/plugins/panel/singlestat/module.ts
  42. 25 0
      public/app/plugins/panel/singlestat/specs/singlestat-specs.ts

+ 2 - 2
.floo

@@ -1,3 +1,3 @@
 {
-    "url": "https://floobits.com/raintank/grafana"
-}
+  "url": "https://floobits.com/raintank/grafana"
+}

+ 5 - 2
.flooignore

@@ -5,5 +5,8 @@
 *~
 extern/
 node_modules/
-tmp
-vendor/
+tmp/
+data/
+vendor/
+public_gen/
+dist/

+ 13 - 1
CHANGELOG.md

@@ -1,10 +1,22 @@
-# 3.0.2 Stable (unreleased)
+# 3.1.0
+
+### Enhancements
+* **Singlestat**: Add support for range to text mappings, closes [#1319](https://github.com/grafana/grafana/issues/1319)
+* **Graph**: Adds sort order options for graph tooltip, closes  [#1189](https://github.com/grafana/grafana/issues/1189)
+* **Theme**: Add default theme to config file [#5011](https://github.com/grafana/grafana/pull/5011)
+
+# 3.0.2 Stable (2016-05-16)
 
 * **Templating**: Fixed issue mixing row repeat and panel repeats, fixes [#4988](https://github.com/grafana/grafana/issues/4988)
 * **Templating**: Fixed issue detecting dependencies in nested variables, fixes [#4987](https://github.com/grafana/grafana/issues/4987), fixes [#4986](https://github.com/grafana/grafana/issues/4986)
+* **Graph**: Fixed broken PNG rendering in graph panel, fixes [#5025](https://github.com/grafana/grafana/issues/5025)
+* **Graph**: Fixed broken xaxis on graph panel, fixes [#5024](https://github.com/grafana/grafana/issues/5024)
+
+* **Influxdb**: Fixes crash when hiding middle serie, fixes [#5005](https://github.com/grafana/grafana/issues/5005)
 
 # 3.0.1 Stable (2016-05-11)
 
+### Bug fixes
 * **Templating**: Fixed issue with new data source variable not persisting current selected value, fixes [#4934](https://github.com/grafana/grafana/issues/4934)
 
 # 3.0.0-beta7 (2016-05-02)

+ 1 - 0
README.md

@@ -16,6 +16,7 @@ Graphite, Elasticsearch, OpenTSDB, Prometheus and InfluxDB.
 - [What's New in Grafana 2.0](http://docs.grafana.org/guides/whats-new-in-v2/)
 - [What's New in Grafana 2.1](http://docs.grafana.org/guides/whats-new-in-v2-1/)
 - [What's New in Grafana 2.5](http://docs.grafana.org/guides/whats-new-in-v2-5/)
+- [What's New in Grafana 3.0](http://docs.grafana.org/guides/whats-new-in-v3/)
 
 ## Features
 ### Graphite Target Editor

+ 3 - 0
conf/defaults.ini

@@ -172,6 +172,9 @@ verify_email_enabled = false
 # Background text for the user field on the login page
 login_hint = email or username
 
+# Default UI theme ("dark" or "light")
+default_theme = dark
+
 #################################### Anonymous Auth ##########################
 [auth.anonymous]
 # enable anonymous access

+ 3 - 0
conf/sample.ini

@@ -155,6 +155,9 @@ check_for_updates = true
 # Background text for the user field on the login page
 ;login_hint = email or username
 
+# Default UI theme ("dark" or "light")
+;default_theme = dark
+
 #################################### Anonymous Auth ##########################
 [auth.anonymous]
 # enable anonymous access

+ 7 - 0
docker/blocks/graphite/fig

@@ -8,3 +8,10 @@ graphite:
     - /etc/localtime:/etc/localtime:ro
     - /etc/timezone:/etc/timezone:ro
 
+fake-data-gen:
+  image: grafana/fake-data-gen
+  net: bridge
+  environment:
+    FD_DATASOURCE: graphite
+    FD_PORT: 2003
+

+ 8 - 0
docker/blocks/influxdb/fig

@@ -4,3 +4,11 @@ influxdb:
     - "2004:2004"
     - "8083:8083"
     - "8086:8086"
+
+fake-data-gen:
+  image: grafana/fake-data-gen
+  net: bridge
+  environment:
+    FD_DATASOURCE: influxdb
+    FD_PORT: 8086
+

+ 7 - 1
docker/blocks/opentsdb/fig

@@ -2,4 +2,10 @@ opentsdb:
   image: opower/opentsdb:latest
   ports:
     - "4242:4242"
-    
+
+fake-data-gen:
+  image: grafana/fake-data-gen
+  net: bridge
+  environment:
+    FD_DATASOURCE: opentsdb
+

+ 10 - 0
docker/blocks/prometheus/fig

@@ -1,6 +1,16 @@
 prometheus:
   build: blocks/prometheus
+  net: bridge
   ports:
     - "9090:9090"
   volumes:
     - /var/docker/prometheus:/prometheus-data
+
+fake-data-gen:
+  image: grafana/fake-data-gen
+  net: bridge
+  ports:
+    - "9091:9091"
+  environment:
+    FD_DATASOURCE: prom
+

+ 3 - 3
docs/sources/datasources/opentsdb.md

@@ -7,10 +7,10 @@ page_keywords: grafana, opentsdb, documentation
 # OpenTSDB Guide
 The newest release of Grafana adds additional functionality when using an OpenTSDB Data source.
 
-![](/img/v2/add_OpenTSDB.jpg)
+![](/img/v2/add_OpenTSDB.png)
 
-1. Open the side menu by clicking the the Grafana icon in the top header. 
-2. In the side menu under the `Dashboards` link you should find a link named `Data Sources`.    
+1. Open the side menu by clicking the the Grafana icon in the top header.
+2. In the side menu under the `Dashboards` link you should find a link named `Data Sources`.
 
     > NOTE: If this link is missing in the side menu it means that your current user does not have the `Admin` role for the current organization.
 

+ 3 - 3
docs/sources/installation/debian.md

@@ -10,13 +10,13 @@ page_keywords: grafana, installation, debian, ubuntu, guide
 
 Description | Download
 ------------ | -------------
-Stable .deb for Debian-based Linux | [grafana_3.0.1_amd64.deb](https://grafanarel.s3.amazonaws.com/builds/grafana_3.0.1_amd64.deb)
+Stable .deb for Debian-based Linux | [grafana_3.0.2-1463383025_amd64.deb](https://grafanarel.s3.amazonaws.com/builds/grafana_3.0.2-1463383025_amd64.deb)
 
 ## Install Stable
 
-    $ wget https://grafanarel.s3.amazonaws.com/builds/grafana_3.0.1_amd64.deb
+    $ wget https://grafanarel.s3.amazonaws.com/builds/grafana_3.0.2-1463383025_amd64.deb
     $ sudo apt-get install -y adduser libfontconfig
-    $ sudo dpkg -i grafana_3.0.1_amd64.deb
+    $ sudo dpkg -i grafana_3.0.2-1463383025_amd64.deb
 
 ## APT Repository
 

+ 4 - 4
docs/sources/installation/rpm.md

@@ -10,24 +10,24 @@ page_keywords: grafana, installation, centos, fedora, opensuse, redhat, guide
 
 Description | Download
 ------------ | -------------
-Stable .RPM for CentOS / Fedora / OpenSuse / Redhat Linux | [grafana-3.0.1-1.x86_64.rpm](https://grafanarel.s3.amazonaws.com/builds/grafana-3.0.1-1.x86_64.rpm)
+Stable .RPM for CentOS / Fedora / OpenSuse / Redhat Linux | [grafana-3.0.2-1463383025.x86_64.rpm](https://grafanarel.s3.amazonaws.com/builds/grafana-3.0.2-1463383025.x86_64.rpm)
 
 ## Install Stable Release from package file
 
 You can install Grafana using Yum directly.
 
-    $ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-3.0.1-1.x86_64.rpm
+    $ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-3.0.2-1463383025.x86_64.rpm
 
 Or install manually using `rpm`.
 
 #### On CentOS / Fedora / Redhat:
 
     $ sudo yum install initscripts fontconfig
-    $ sudo rpm -Uvh grafana-3.0.1-1.x86_64.rpm
+    $ sudo rpm -Uvh grafana-3.0.2-1463383025.x86_64.rpm
 
 #### On OpenSuse:
 
-    $ sudo rpm -i --nodeps grafana-3.0.1-1.x86_64.rpm
+    $ sudo rpm -i --nodeps grafana-3.0.2-1463383025.x86_64.rpm
 
 ## Install via YUM Repository
 

+ 1 - 1
docs/sources/installation/windows.md

@@ -10,7 +10,7 @@ page_keywords: grafana, installation, windows guide
 
 Description | Download
 ------------ | -------------
-Stable Zip package for Windows | [grafana.2.6.0.windows-x64.zip](https://grafanarel.s3.amazonaws.com/winbuilds/dist/grafana-2.5.0.windows-x64.zip)
+Stable Zip package for Windows | [grafana.3.0.2.windows-x64.zip](https://grafanarel.s3.amazonaws.com/winbuilds/dist/grafana-3.0.2.windows-x64.zip)
 
 ## Configure
 

+ 2 - 2
latest.json

@@ -1,4 +1,4 @@
 {
-  "stable": "3.0.1",
-	"testing": "3.0.1"
+  "stable": "3.0.2",
+	"testing": "3.0.2"
 }

+ 1 - 1
package.json

@@ -4,7 +4,7 @@
     "company": "Coding Instinct AB"
   },
   "name": "grafana",
-  "version": "3.0.2",
+  "version": "3.1.0",
   "repository": {
     "type": "git",
     "url": "http://github.com/grafana/grafana.git"

+ 1 - 1
packaging/deb/default/grafana-server

@@ -14,6 +14,6 @@ CONF_DIR=/etc/grafana
 
 CONF_FILE=/etc/grafana/grafana.ini
 
-RESTART_ON_UPGRADE=true
+RESTART_ON_UPGRADE=false
 
 PLUGINS_DIR=/var/lib/grafana/plugins

+ 1 - 1
packaging/rpm/sysconfig/grafana-server

@@ -14,6 +14,6 @@ CONF_DIR=/etc/grafana
 
 CONF_FILE=/etc/grafana/grafana.ini
 
-RESTART_ON_UPGRADE=true
+RESTART_ON_UPGRADE=false
 
 PLUGINS_DIR=/var/lib/grafana/plugins

+ 1 - 0
pkg/api/frontendsettings.go

@@ -142,6 +142,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
 			"buildstamp":    setting.BuildStamp,
 			"latestVersion": plugins.GrafanaLatestVersion,
 			"hasUpdate":     plugins.GrafanaHasUpdate,
+			"env":           setting.Env,
 		},
 	}
 

+ 7 - 8
pkg/cmd/grafana-cli/commands/upgrade_command.go

@@ -1,6 +1,8 @@
 package commands
 
 import (
+	"github.com/fatih/color"
+	"github.com/grafana/grafana/pkg/cmd/grafana-cli/log"
 	s "github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
 )
 
@@ -14,20 +16,17 @@ func upgradeCommand(c CommandLine) error {
 		return err
 	}
 
-	remotePlugins, err2 := s.ListAllPlugins(c.GlobalString("repo"))
+	v, err2 := s.GetPlugin(localPlugin.Id, c.GlobalString("repo"))
 
 	if err2 != nil {
 		return err2
 	}
 
-	for _, v := range remotePlugins.Plugins {
-		if localPlugin.Id == v.Id {
-			if ShouldUpgrade(localPlugin.Info.Version, v) {
-				s.RemoveInstalledPlugin(pluginsDir, pluginName)
-				return InstallPlugin(localPlugin.Id, "", c)
-			}
-		}
+	if ShouldUpgrade(localPlugin.Info.Version, v) {
+		s.RemoveInstalledPlugin(pluginsDir, pluginName)
+		return InstallPlugin(localPlugin.Id, "", c)
 	}
 
+	log.Infof("%s %s is up to date \n", color.GreenString("✔"), localPlugin.Id)
 	return nil
 }

+ 15 - 7
pkg/cmd/grafana-cli/services/services.go

@@ -44,7 +44,7 @@ func ReadPlugin(pluginDir, pluginName string) (m.InstalledPlugin, error) {
 	}
 
 	if res.Id == "" {
-		return m.InstalledPlugin{}, errors.New("could not read find plugin " + pluginName)
+		return m.InstalledPlugin{}, errors.New("could not find plugin " + pluginName + " in " + pluginDir)
 	}
 
 	return res, nil
@@ -69,13 +69,21 @@ func RemoveInstalledPlugin(pluginPath, id string) error {
 }
 
 func GetPlugin(pluginId, repoUrl string) (m.Plugin, error) {
-	resp, _ := ListAllPlugins(repoUrl)
+	fullUrl := repoUrl + "/repo/" + pluginId
 
-	for _, i := range resp.Plugins {
-		if i.Id == pluginId {
-			return i, nil
-		}
+	res, err := goreq.Request{Uri: fullUrl, MaxRedirects: 3}.Do()
+	if err != nil {
+		return m.Plugin{}, err
+	}
+	if res.StatusCode != 200 {
+		return m.Plugin{}, fmt.Errorf("Could not access %s statuscode %v", fullUrl, res.StatusCode)
 	}
 
-	return m.Plugin{}, errors.New("could not find plugin named \"" + pluginId + "\"")
+	var resp m.Plugin
+	err = res.Body.FromJsonTo(&resp)
+	if err != nil {
+		return m.Plugin{}, errors.New("Could not load plugin data")
+	}
+
+	return resp, nil
 }

+ 10 - 1
pkg/plugins/queries.go

@@ -24,7 +24,16 @@ func GetPluginSettings(orgId int64) (map[string]*m.PluginSettingInfoDTO, error)
 		}
 
 		// default to enabled true
-		opt := &m.PluginSettingInfoDTO{Enabled: true}
+		opt := &m.PluginSettingInfoDTO{
+			PluginId: pluginDef.Id,
+			OrgId:    orgId,
+			Enabled:  true,
+		}
+
+		// apps are disabled by default
+		if pluginDef.Type == PluginTypeApp {
+			opt.Enabled = false
+		}
 
 		// if it's included in app check app settings
 		if pluginDef.IncludedInAppId != "" {

+ 3 - 1
pkg/services/sqlstore/preferences.go

@@ -5,6 +5,8 @@ import (
 
 	"github.com/grafana/grafana/pkg/bus"
 	m "github.com/grafana/grafana/pkg/models"
+
+	"github.com/grafana/grafana/pkg/setting"
 )
 
 func init() {
@@ -26,7 +28,7 @@ func GetPreferencesWithDefaults(query *m.GetPreferencesWithDefaultsQuery) error
 	}
 
 	res := &m.Preferences{
-		Theme:           "dark",
+		Theme:           setting.DefaultTheme,
 		Timezone:        "browser",
 		HomeDashboardId: 0,
 	}

+ 2 - 0
pkg/setting/setting.go

@@ -88,6 +88,7 @@ var (
 	AutoAssignOrgRole  string
 	VerifyEmailEnabled bool
 	LoginHint          string
+	DefaultTheme       string
 
 	// Http auth
 	AdminUser     string
@@ -454,6 +455,7 @@ func NewConfigContext(args *CommandLineArgs) error {
 	AutoAssignOrgRole = users.Key("auto_assign_org_role").In("Editor", []string{"Editor", "Admin", "Read Only Editor", "Viewer"})
 	VerifyEmailEnabled = users.Key("verify_email_enabled").MustBool(false)
 	LoginHint = users.Key("login_hint").String()
+	DefaultTheme = users.Key("default_theme").String()
 
 	// anonymous access
 	AnonymousEnabled = Cfg.Section("auth.anonymous").Key("enabled").MustBool(false)

+ 3 - 1
public/app/app.ts

@@ -42,7 +42,9 @@ export class GrafanaApp {
     app.constant('grafanaVersion', "@grafanaVersion@");
 
     app.config(($locationProvider, $controllerProvider, $compileProvider, $filterProvider, $provide) => {
-      //$compileProvider.debugInfoEnabled(false);
+      if (config.buildInfo.env !== 'development') {
+        $compileProvider.debugInfoEnabled(false);
+      }
 
       this.registerFunctions.controller = $controllerProvider.register;
       this.registerFunctions.directive  = $compileProvider.directive;

+ 2 - 0
public/app/core/utils/kbn.js

@@ -396,6 +396,7 @@ function($, _) {
   kbn.valueFormats.ev           = kbn.formatBuilders.decimalSIPrefix('eV');
   kbn.valueFormats.amp          = kbn.formatBuilders.decimalSIPrefix('A');
   kbn.valueFormats.volt         = kbn.formatBuilders.decimalSIPrefix('V');
+  kbn.valueFormats.dBm          = kbn.formatBuilders.decimalSIPrefix('dBm');
 
   // Temperature
   kbn.valueFormats.celsius   = kbn.formatBuilders.fixedUnit('°C');
@@ -677,6 +678,7 @@ function($, _) {
           {text: 'electron volt (eV)',         value: 'ev'          },
           {text: 'Ampere (A)',                 value: 'amp'         },
           {text: 'Volt (V)',                   value: 'volt'        },
+          {text: 'Decibel-milliwatt (dBm)',    value: 'dBm'         },
         ]
       },
       {

+ 1 - 1
public/app/features/panel/metrics_ds_selector.ts

@@ -10,7 +10,7 @@ var template = `
   <div class="gf-form-inline">
     <div class="gf-form">
       <label class="gf-form-label">
-        <i class="icon-gf icon-gf-datasource"></i>
+        <i class="icon-gf icon-gf-datasources"></i>
       </label>
       <label class="gf-form-label">
         Panel data source

+ 7 - 3
public/app/plugins/datasource/influxdb/datasource.ts

@@ -45,7 +45,7 @@ export default class InfluxDatasource {
     var i, y;
 
     var allQueries = _.map(options.targets, (target) => {
-      if (target.hide) { return []; }
+      if (target.hide) { return ""; }
 
       queryTargets.push(target);
 
@@ -54,8 +54,12 @@ export default class InfluxDatasource {
       var query =  queryModel.render(true);
       query = query.replace(/\$interval/g, (target.interval || options.interval));
       return query;
-
-    }).join(";");
+    }).reduce((acc, current) => {
+      if (current !== "") {
+        acc += ";" + current;
+      }
+      return acc;
+    });
 
     // replace grafana variables
     allQueries = allQueries.replace(/\$timeFilter/g, timeFilter);

+ 1 - 1
public/app/plugins/datasource/influxdb/partials/query.options.html

@@ -45,7 +45,7 @@
 			<ul>
 				<li>$m = replaced with measurement name</li>
 				<li>$measurement = replaced with measurement name</li>
-				<li>$1 - $9 = replaced with part of measurement name (if you seperate your measurement name with dots)</li>
+				<li>$1 - $9 = replaced with part of measurement name (if you separate your measurement name with dots)</li>
 				<li>$col = replaced with column name</li>
 				<li>$tag_hostname = replaced with the value of the hostname tag</li>
 				<li>You can also use [[tag_hostname]] pattern replacement syntax</li>

+ 2 - 1
public/app/plugins/datasource/opentsdb/config_ctrl.ts

@@ -16,7 +16,8 @@ export class OpenTsConfigCtrl {
 
   tsdbVersions = [
     {name: '<=2.1', value: 1},
-    {name: '>=2.2', value: 2},
+    {name: '==2.2', value: 2},
+    {name: '==2.3', value: 3},
   ];
 
   tsdbResolutions = [

+ 23 - 17
public/app/plugins/datasource/opentsdb/datasource.js

@@ -54,13 +54,12 @@ function (angular, _, dateMath) {
       });
 
       return this.performTimeSeriesQuery(queries, start, end).then(function(response) {
-        var metricToTargetMapping = mapMetricsToTargets(response.data, options);
+        var metricToTargetMapping = mapMetricsToTargets(response.data, options, this.tsdbVersion);
         var result = _.map(response.data, function(metricData, index) {
           index = metricToTargetMapping[index];
           if (index === -1) {
             index = 0;
           }
-
           this._saveTagKeys(metricData);
 
           return transformMetricData(metricData, groupByTags, options.targets[index], options, this.tsdbResolution);
@@ -114,6 +113,9 @@ function (angular, _, dateMath) {
         msResolution: msResolution,
         globalAnnotations: true
       };
+      if (this.tsdbVersion === 3) {
+        reqBody.showQuery = true;
+      }
 
       // Relative queries (e.g. last hour) don't include an end time
       if (end) {
@@ -393,23 +395,27 @@ function (angular, _, dateMath) {
       return query;
     }
 
-    function mapMetricsToTargets(metrics, options) {
+    function mapMetricsToTargets(metrics, options, tsdbVersion) {
       var interpolatedTagValue;
       return _.map(metrics, function(metricData) {
-        return _.findIndex(options.targets, function(target) {
-          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, 'pipe');
-              return metricData.tags[tagK] === interpolatedTagValue || interpolatedTagValue === "*";
-            });
-          }
-        });
+        if (tsdbVersion === 3) {
+          return metricData.query.index;
+        } else {
+          return _.findIndex(options.targets, function(target) {
+            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, 'pipe');
+                return metricData.tags[tagK] === interpolatedTagValue || interpolatedTagValue === "*";
+              });
+            }
+          });
+        }
       });
     }
 

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

@@ -69,7 +69,7 @@
 			</div>
 		</div>
 
-		<div class="gf-form" ng-if="ctrl.tsdbVersion == 2">
+		<div class="gf-form" ng-if="ctrl.tsdbVersion >= 2">
 			<label class="gf-form-label query-keyword width-6">Fill</label>
 			<div class="gf-form-select-wrapper">
 				<select ng-model="ctrl.target.downsampleFillPolicy" class="gf-form-input"
@@ -91,7 +91,7 @@
 		</div>
 	</div>
 
-	<div class="gf-form-inline" ng-if="ctrl.tsdbVersion == 2">
+	<div class="gf-form-inline" ng-if="ctrl.tsdbVersion >= 2">
 		<div class="gf-form">
 
 			<label class="gf-form-label query-keyword width-8">
@@ -170,7 +170,7 @@
 		<div class="gf-form">
 			<label class="gf-form-label query-keyword width-8">
 				Tags
-				<info-popover mode="right-normal" ng-if="ctrl.tsdbVersion == 2">
+				<info-popover mode="right-normal" ng-if="ctrl.tsdbVersion >= 2">
 					Please use filters, tags are deprecated in opentsdb 2.2
 				</info-popover>
 			</label>

+ 4 - 4
public/app/plugins/datasource/prometheus/datasource.ts

@@ -43,7 +43,7 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
     return value.replace(/[\\^$*+?.()|[\]{}]/g, '\\\\$&');
   }
 
-  function interpolateQueryExpr(value, variable, defaultFormatFn) {
+  this.interpolateQueryExpr = function(value, variable, defaultFormatFn) {
     // if no multi or include all do not regexEscape
     if (!variable.multi && !variable.includeAll) {
       return value;
@@ -59,6 +59,7 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
 
   // Called once per panel (graph)
   this.query = function(options) {
+    var self = this;
     var start = getPrometheusTime(options.range.from, false);
     var end = getPrometheusTime(options.range.to, true);
 
@@ -73,7 +74,7 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
       activeTargets.push(target);
 
       var query: any = {};
-      query.expr = templateSrv.replace(target.expr, options.scopedVars, interpolateQueryExpr);
+      query.expr = templateSrv.replace(target.expr, options.scopedVars, self.interpolateQueryExpr);
 
       var interval = target.interval || options.interval;
       var intervalFactor = target.intervalFactor || 1;
@@ -99,7 +100,6 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
       return this.performTimeSeriesQuery(query, start, end);
     }, this));
 
-    var self = this;
     return $q.all(allQueryPromise)
     .then(function(allResponse) {
       var result = [];
@@ -160,7 +160,7 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
 
     var interpolated;
     try {
-      interpolated = templateSrv.replace(expr, {}, interpolateQueryExpr);
+      interpolated = templateSrv.replace(expr, {}, this.interpolateQueryExpr);
     } catch (err) {
       return $q.reject(err);
     }

+ 1 - 1
public/app/plugins/datasource/prometheus/query_ctrl.ts

@@ -61,7 +61,7 @@ class PrometheusQueryCtrl extends QueryCtrl {
     var rangeDiff = Math.ceil((range.to.valueOf() - range.from.valueOf()) / 1000);
     var endTime = range.to.utc().format('YYYY-MM-DD HH:mm');
     var expr = {
-      expr: this.templateSrv.replace(this.target.expr, this.panelCtrl.panel.scopedVars),
+      expr: this.templateSrv.replace(this.target.expr, this.panelCtrl.panel.scopedVars, this.datasource.interpolateQueryExpr),
       range_input: rangeDiff + 's',
       end_input: endTime,
       step_input: '',

+ 16 - 7
public/app/plugins/panel/graph/graph.js

@@ -282,7 +282,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) {
 
           options.xaxis = {
             timezone: dashboard.getTimezone(),
-            show: panel['x-axis'],
+            show: panel.xaxis.show,
             mode: "time",
             min: min,
             max: max,
@@ -452,12 +452,21 @@ function (angular, $, moment, _, kbn, GraphTooltip) {
           url += panel.fill !== 0 ? ('&areaAlpha=' + (panel.fill/10).toFixed(1)) : '';
           url += panel.linewidth !== 0 ? '&lineWidth=' + panel.linewidth : '';
           url += panel.legend.show ? '&hideLegend=false' : '&hideLegend=true';
-          url += panel.grid.leftMin !== null ? '&yMin=' + panel.grid.leftMin : '';
-          url += panel.grid.leftMax !== null ? '&yMax=' + panel.grid.leftMax : '';
-          url += panel.grid.rightMin !== null ? '&yMin=' + panel.grid.rightMin : '';
-          url += panel.grid.rightMax !== null ? '&yMax=' + panel.grid.rightMax : '';
-          url += panel['x-axis'] ? '' : '&hideAxes=true';
-          url += panel['y-axis'] ? '' : '&hideYAxis=true';
+
+          if (panel.yaxes && panel.yaxes.length > 0) {
+            var showYaxis = false;
+            for(var i = 0; panel.yaxes.length > i; i++) {
+              if (panel.yaxes[i].show) {
+                url += (panel.yaxes[i].min !== null && panel.yaxes[i].min !== undefined) ? '&yMin=' + panel.yaxes[i].min : '';
+                url += (panel.yaxes[i].max !== null && panel.yaxes[i].max !== undefined) ? '&yMax=' + panel.yaxes[i].max : '';
+                showYaxis = true;
+                break;
+              }
+            }
+            url += showYaxis ? '' : '&hideYAxis=true';
+          }
+
+          url += panel.xaxis.show ? '' : '&hideAxes=true';
 
           switch(panel.yaxes[0].format) {
             case 'bytes':

+ 15 - 3
public/app/plugins/panel/graph/graph_tooltip.js

@@ -81,9 +81,9 @@ function ($) {
           // Stacked series can increase its length on each new stacked serie if null points found,
           // to speed the index search we begin always on the last found hoverIndex.
           var newhoverIndex = this.findHoverIndexFromDataPoints(pos.x, series, hoverIndex);
-          results.push({ value: value, hoverIndex: newhoverIndex });
+          results.push({ value: value, hoverIndex: newhoverIndex, color: series.color, label: series.label });
         } else {
-          results.push({ value: value, hoverIndex: hoverIndex });
+          results.push({ value: value, hoverIndex: hoverIndex, color: series.color, label: series.label });
         }
       }
 
@@ -133,6 +133,18 @@ function ($) {
 
         absoluteTime = dashboard.formatDate(seriesHoverInfo.time, tooltipFormat);
 
+        // Dynamically reorder the hovercard for the current time point if the
+        // option is enabled.
+        if (panel.tooltip.ordering === 'decreasing') {
+          seriesHoverInfo.sort(function(a, b) {
+            return parseFloat(b.value) - parseFloat(a.value);
+          });
+        } else if (panel.tooltip.ordering === 'increasing') {
+          seriesHoverInfo.sort(function(a, b) {
+            return parseFloat(a.value) - parseFloat(b.value);
+          });
+        }
+
         for (i = 0; i < seriesHoverInfo.length; i++) {
           hoverInfo = seriesHoverInfo[i];
 
@@ -150,7 +162,7 @@ function ($) {
           value = series.formatValue(hoverInfo.value);
 
           seriesHtml += '<div class="graph-tooltip-list-item ' + highlightClass + '"><div class="graph-tooltip-series-name">';
-          seriesHtml += '<i class="fa fa-minus" style="color:' + series.color +';"></i> ' + series.label + ':</div>';
+          seriesHtml += '<i class="fa fa-minus" style="color:' + hoverInfo.color +';"></i> ' + hoverInfo.label + ':</div>';
           seriesHtml += '<div class="graph-tooltip-value">' + value + '</div></div>';
           plot.highlight(i, hoverInfo.hoverIndex);
         }

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

@@ -92,6 +92,7 @@ class GraphCtrl extends MetricsPanelCtrl {
     tooltip       : {
       value_type: 'cumulative',
       shared: true,
+      ordering: 'alphabetical',
       msResolution: false,
     },
     // time overrides

+ 9 - 3
public/app/plugins/panel/graph/tab_display.html

@@ -42,23 +42,29 @@
 	<div class="section gf-form-group">
 		<h5 class="section-heading">Misc options</h5>
 		<div class="gf-form">
-			<label class="gf-form-label width-7">Null value</label>
+			<label class="gf-form-label width-9">Null value</label>
 			<div class="gf-form-select-wrapper">
 				<select class="gf-form-input max-width-8" ng-model="ctrl.panel.nullPointMode" ng-options="f for f in ['connected', 'null', 'null as zero']" ng-change="ctrl.render()"></select>
 			</div>
 		</div>
 		<div class="gf-form">
-			<label class="gf-form-label width-7">Renderer</label>
+			<label class="gf-form-label width-9">Renderer</label>
 			<div class="gf-form-select-wrapper max-width-8">
 				<select class="gf-form-input" ng-model="ctrl.panel.renderer" ng-options="f for f in ['flot', 'png']" ng-change="ctrl.render()"></select>
 			</div>
 		</div>
 		<div class="gf-form">
-			<label class="gf-form-label width-7">Tooltip mode</label>
+			<label class="gf-form-label width-9">Tooltip mode</label>
 			<div class="gf-form-select-wrapper max-width-8">
 				<select class="gf-form-input" ng-model="ctrl.panel.tooltip.shared" ng-options="f.value as f.text for f in [{text: 'All series', value: true}, {text: 'Single', value: false}]" ng-change="ctrl.render()"></select>
 			</div>
 		</div>
+		<div class="gf-form">
+			<label class="gf-form-label width-9">Tooltip ordering<tip>The ordering from top to bottom</tip></label>
+			<div class="gf-form-select-wrapper max-width-8">
+				<select class="gf-form-input" ng-model="ctrl.panel.tooltip.ordering" ng-options="f.value as f.text for f in [{text: 'Alphabetical', value: 'alphabetical'}, {text: 'Increasing', value: 'increasing'}, {text: 'Decreasing', value: 'decreasing'}]" ng-change="ctrl.render()"></select>
+			</div>
+		</div>
 	</div>
 
 	<div class="section gf-form-group">

+ 0 - 32
public/app/plugins/panel/singlestat/editor.html

@@ -204,35 +204,3 @@
 		</div>
 	</div>
 </div>
-
-<div class="editor-row">
-	<div class="section" style="margin-bottom: 20px">
-		<div class="tight-form last">
-			<ul class="tight-form-list">
-				<li class="tight-form-item">
-					<strong>Value to text mapping</strong>
-				</li>
-				<li class="tight-form-item"  ng-repeat-start="map in ctrl.panel.valueMaps">
-					<i class="fa fa-remove pointer" ng-click="ctrl.removeValueMap(map)"></i>
-				</li>
-				<li>
-					<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="map.text" class="input-mini tight-form-input" ng-blur="ctrl.render()">
-				</li>
-
-				<li>
-					<a class="pointer tight-form-item last" ng-click="ctrl.addValueMap();">
-						<i class="fa fa-plus"></i>
-					</a>
-				</li>
-
-			</ul>
-			<div class="clearfix"></div>
-		</div>
-	</div>
-</div>

+ 58 - 0
public/app/plugins/panel/singlestat/mappings.html

@@ -0,0 +1,58 @@
+<div class="editor-row">
+  <div class="gf-form-group">
+    <div class="gf-form">
+        <span class="gf-form-label">
+          Type
+        </span>
+        <div class="gf-form-select-wrapper">
+          <select class="gf-form-input" ng-model="ctrl.panel.mappingType"
+                                                        ng-options="f.value as f.name for f in ctrl.panel.mappingTypes" ng-change="ctrl.render()"></select>
+        </div>
+    </div>
+  </div>
+</div>
+<div class="editor-row" ng-if="ctrl.panel.mappingType==1">
+  <h5 class="page-heading">Set value mappings</h5>
+  <div class="gf-form-group">
+    <div class="gf-form" ng-repeat="map in ctrl.panel.valueMaps">
+      <span class="gf-form-label">
+        <i class="fa fa-remove pointer" ng-click="ctrl.removeValueMap(map)"></i>
+      </span>
+      <input type="text" ng-model="map.value" placeholder="value" class="gf-form-input max-width-6" ng-blur="ctrl.render()">
+      <span class="gf-form-label">
+        <i class="fa fa-arrow-right"></i>
+      </span>
+      <input type="text" placeholder="text" ng-model="map.text" class="gf-form-input max-width-8" ng-blur="ctrl.render()">
+    </div>
+
+    <div class="gf-form-button-row">
+      <button class="btn btn-inverse" ng-click="ctrl.addValueMap();">
+        <i class="fa fa-plus"></i>
+        Add a value mapping
+      </button>
+    </div>
+  </div>
+</div>
+<div class="editor-row" ng-if="ctrl.panel.mappingType==2">
+  <h5 class="page-heading">Set range mappings</h5>
+  <div class="gf-form-group">
+    <div class="gf-form" ng-repeat="rangeMap in ctrl.panel.rangeMaps">
+        <span class="gf-form-label">
+          <i class="fa fa-remove pointer" ng-click="ctrl.removeRangeMap(rangeMap)"></i>
+        </span>
+        <span class="gf-form-label">From</span>
+        <input type="text" ng-model="rangeMap.from" class="gf-form-input max-width-6" ng-blur="ctrl.render()">
+        <span class="gf-form-label">To</span>
+        <input type="text" ng-model="rangeMap.to" class="gf-form-input max-width-6" ng-blur="ctrl.render()">
+        <span class="gf-form-label">Text</span>
+        <input type="text" ng-model="rangeMap.text" class="gf-form-input max-width-8" ng-blur="ctrl.render()">
+    </div>
+
+    <div class="gf-form-button-row">
+      <button class="btn btn-inverse" ng-click="ctrl.addRangeMap()">
+        <i class="fa fa-plus"></i>
+        Add a range mapping
+      </button>
+    </div>
+  </div>
+</div>

+ 53 - 12
public/app/plugins/panel/singlestat/module.ts

@@ -35,6 +35,14 @@ class SingleStatCtrl extends MetricsPanelCtrl {
     valueMaps: [
       { value: 'null', op: '=', text: 'N/A' }
     ],
+    mappingTypes: [
+      {name: 'value to text', value: 1},
+      {name: 'range to text', value: 2},
+    ],
+    rangeMaps: [
+      { from: 'null', to: 'null', text: 'N/A' }
+    ],
+    mappingType: 1,
     nullPointMode: 'connected',
     valueName: 'avg',
     prefixFontSize: '50%',
@@ -73,6 +81,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
   onInitEditMode() {
     this.fontSizes = ['20%', '30%','50%','70%','80%','100%', '110%', '120%', '150%', '170%', '200%'];
     this.addEditorTab('Options', 'public/app/plugins/panel/singlestat/editor.html', 2);
+    this.addEditorTab('Value Mappings', 'public/app/plugins/panel/singlestat/mappings.html', 3);
     this.unitFormats = kbn.getUnitFormats();
   }
 
@@ -192,23 +201,45 @@ class SingleStatCtrl extends MetricsPanelCtrl {
       }
     }
 
-    // check value to text mappings
-    for (var i = 0; i < this.panel.valueMaps.length; i++) {
-      var map = this.panel.valueMaps[i];
-      // special null case
-      if (map.value === 'null') {
-        if (data.value === null || data.value === void 0) {
+    // check value to text mappings if its enabled
+    if (this.panel.mappingType === 1) {
+      for (var i = 0; i < this.panel.valueMaps.length; i++) {
+        var map = this.panel.valueMaps[i];
+        // special null case
+        if (map.value === 'null') {
+          if (data.value === null || data.value === void 0) {
+            data.valueFormated = map.text;
+            return;
+          }
+          continue;
+        }
+
+        // value/number to text mapping
+        var value = parseFloat(map.value);
+        if (value === data.valueRounded) {
           data.valueFormated = map.text;
           return;
         }
-        continue;
       }
+    } else if (this.panel.mappingType === 2) {
+      for (var i = 0; i < this.panel.rangeMaps.length; i++) {
+        var map = this.panel.rangeMaps[i];
+        // special null case
+        if (map.from === 'null' && map.to === 'null') {
+          if (data.value === null || data.value === void 0) {
+            data.valueFormated = map.text;
+            return;
+          }
+          continue;
+        }
 
-      // value/number to text mapping
-      var value = parseFloat(map.value);
-      if (value === data.valueRounded) {
-        data.valueFormated = map.text;
-        return;
+        // value/number to range mapping
+        var from = parseFloat(map.from);
+        var to = parseFloat(map.to);
+        if (to >= data.valueRounded && from <= data.valueRounded) {
+          data.valueFormated = map.text;
+          return;
+        }
       }
     }
 
@@ -227,6 +258,16 @@ class SingleStatCtrl extends MetricsPanelCtrl {
     this.panel.valueMaps.push({value: '', op: '=', text: '' });
   }
 
+  removeRangeMap(rangeMap) {
+    var index = _.indexOf(this.panel.rangeMaps, rangeMap);
+    this.panel.rangeMaps.splice(index, 1);
+    this.render();
+  };
+
+  addRangeMap() {
+    this.panel.rangeMaps.push({from: '', to: '', text: ''});
+  }
+
   link(scope, elem, attrs, ctrl) {
     var $location = this.$location;
     var linkSrv = this.linkSrv;

+ 25 - 0
public/app/plugins/panel/singlestat/specs/singlestat-specs.ts

@@ -84,4 +84,29 @@ describe('SingleStatCtrl', function() {
       expect(ctx.data.valueFormated).to.be('OK');
     });
   });
+
+  singleStatScenario('When range to text mapping is specifiedfor first range', function(ctx) {
+    ctx.setup(function() {
+      ctx.datapoints = [[41,50]];
+      ctx.ctrl.panel.mappingType = 2;
+      ctx.ctrl.panel.rangeMaps = [{from: '10', to: '50', text: 'OK'},{from: '51', to: '100', text: 'NOT OK'}];
+    });
+
+    it('Should replace value with text OK', function() {
+      expect(ctx.data.valueFormated).to.be('OK');
+    });
+  });
+
+  singleStatScenario('When range to text mapping is specified for other ranges', function(ctx) {
+    ctx.setup(function() {
+      ctx.datapoints = [[65,75]];
+      ctx.ctrl.panel.mappingType = 2;
+      ctx.ctrl.panel.rangeMaps = [{from: '10', to: '50', text: 'OK'},{from: '51', to: '100', text: 'NOT OK'}];
+    });
+
+    it('Should replace value with text NOT OK', function() {
+      expect(ctx.data.valueFormated).to.be('NOT OK');
+    });
+  });
+
 });