Merge branch 'master' into export-dashboard

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

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

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

@ -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)

@ -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

@ -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

@ -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

@ -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

@ -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

@ -3,3 +3,9 @@ opentsdb:
ports:
- "4242:4242"
fake-data-gen:
image: grafana/fake-data-gen
net: bridge
environment:
FD_DATASOURCE: opentsdb

@ -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

@ -7,7 +7,7 @@ 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`.

@ -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

@ -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

@ -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

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

@ -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"

@ -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

@ -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

@ -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,
},
}

@ -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
}

@ -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
}

@ -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 != "" {

@ -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,
}

@ -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)

@ -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;

@ -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' },
]
},
{

@ -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

@ -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);

@ -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>

@ -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 = [

@ -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 === "*";
});
}
});
}
});
}

@ -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>

@ -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);
}

@ -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: '',

@ -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':

@ -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);
}

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

@ -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">

@ -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>

@ -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>

@ -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;

@ -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');
});
});
});

Loading…
Cancel
Save