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

Merge branch 'master' into alerting_definitions

Torkel Ödegaard 9 лет назад
Родитель
Сommit
495404ef73

+ 3 - 0
.floo

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

+ 13 - 0
.flooignore

@@ -0,0 +1,13 @@
+#*
+*.o
+*.pyc
+*.pyo
+*~
+extern/
+node_modules/
+tmp/
+data/
+vendor/
+public_gen/
+dist/
+

+ 9 - 1
CHANGELOG.md

@@ -1,4 +1,11 @@
-# 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 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)
 * **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)
@@ -9,6 +16,7 @@
 
 
 # 3.0.1 Stable (2016-05-11)
 # 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)
 * **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)
 # 3.0.0-beta7 (2016-05-02)

+ 3 - 0
conf/defaults.ini

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

+ 7 - 0
docker/blocks/graphite/fig

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

+ 7 - 1
docker/blocks/opentsdb/fig

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

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

@@ -10,13 +10,13 @@ page_keywords: grafana, installation, debian, ubuntu, guide
 
 
 Description | Download
 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
 ## 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 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
 ## APT Repository
 
 

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

@@ -10,24 +10,24 @@ page_keywords: grafana, installation, centos, fedora, opensuse, redhat, guide
 
 
 Description | Download
 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
 ## Install Stable Release from package file
 
 
 You can install Grafana using Yum directly.
 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`.
 Or install manually using `rpm`.
 
 
 #### On CentOS / Fedora / Redhat:
 #### On CentOS / Fedora / Redhat:
 
 
     $ sudo yum install initscripts fontconfig
     $ 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:
 #### 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
 ## Install via YUM Repository
 
 

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

@@ -10,7 +10,7 @@ page_keywords: grafana, installation, windows guide
 
 
 Description | Download
 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
 ## Configure
 
 

+ 1 - 1
package.json

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

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

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

+ 2 - 0
pkg/setting/setting.go

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

+ 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,
           // 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.
           // to speed the index search we begin always on the last found hoverIndex.
           var newhoverIndex = this.findHoverIndexFromDataPoints(pos.x, series, 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 {
         } 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);
         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++) {
         for (i = 0; i < seriesHoverInfo.length; i++) {
           hoverInfo = seriesHoverInfo[i];
           hoverInfo = seriesHoverInfo[i];
 
 
@@ -150,7 +162,7 @@ function ($) {
           value = series.formatValue(hoverInfo.value);
           value = series.formatValue(hoverInfo.value);
 
 
           seriesHtml += '<div class="graph-tooltip-list-item ' + highlightClass + '"><div class="graph-tooltip-series-name">';
           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>';
           seriesHtml += '<div class="graph-tooltip-value">' + value + '</div></div>';
           plot.highlight(i, hoverInfo.hoverIndex);
           plot.highlight(i, hoverInfo.hoverIndex);
         }
         }

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

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

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

@@ -42,23 +42,29 @@
 	<div class="section gf-form-group">
 	<div class="section gf-form-group">
 		<h5 class="section-heading">Misc options</h5>
 		<h5 class="section-heading">Misc options</h5>
 		<div class="gf-form">
 		<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">
 			<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>
 				<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>
 		</div>
 		<div class="gf-form">
 		<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">
 			<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>
 				<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>
 		</div>
 		<div class="gf-form">
 		<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">
 			<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>
 				<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>
 		</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>
 
 
 	<div class="section gf-form-group">
 	<div class="section gf-form-group">

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

@@ -204,35 +204,3 @@
 		</div>
 		</div>
 	</div>
 	</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 valuea 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: [
     valueMaps: [
       { value: 'null', op: '=', text: 'N/A' }
       { 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',
     nullPointMode: 'connected',
     valueName: 'avg',
     valueName: 'avg',
     prefixFontSize: '50%',
     prefixFontSize: '50%',
@@ -73,6 +81,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
   onInitEditMode() {
   onInitEditMode() {
     this.fontSizes = ['20%', '30%','50%','70%','80%','100%', '110%', '120%', '150%', '170%', '200%'];
     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('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();
     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;
           data.valueFormated = map.text;
           return;
           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: '' });
     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) {
   link(scope, elem, attrs, ctrl) {
     var $location = this.$location;
     var $location = this.$location;
     var linkSrv = this.linkSrv;
     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');
       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');
+    });
+  });
+
 });
 });