Merge branch 'master' into export-dashboard

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

@ -5,5 +5,8 @@
*~ *~
extern/ extern/
node_modules/ node_modules/
tmp tmp/
data/
vendor/ 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 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)
* **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) # 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)

@ -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.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.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 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 ## Features
### Graphite Target Editor ### Graphite Target Editor

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

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

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

@ -4,3 +4,11 @@ influxdb:
- "2004:2004" - "2004:2004"
- "8083:8083" - "8083:8083"
- "8086:8086" - "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: ports:
- "4242:4242" - "4242:4242"
fake-data-gen:
image: grafana/fake-data-gen
net: bridge
environment:
FD_DATASOURCE: opentsdb

@ -1,6 +1,16 @@
prometheus: prometheus:
build: blocks/prometheus build: blocks/prometheus
net: bridge
ports: ports:
- "9090:9090" - "9090:9090"
volumes: volumes:
- /var/docker/prometheus:/prometheus-data - /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 # OpenTSDB Guide
The newest release of Grafana adds additional functionality when using an OpenTSDB Data source. 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. 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`. 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 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

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

@ -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,4 +1,4 @@
{ {
"stable": "3.0.1", "stable": "3.0.2",
"testing": "3.0.1" "testing": "3.0.2"
} }

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

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

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

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

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

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

@ -24,7 +24,16 @@ func GetPluginSettings(orgId int64) (map[string]*m.PluginSettingInfoDTO, error)
} }
// default to enabled true // 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 it's included in app check app settings
if pluginDef.IncludedInAppId != "" { if pluginDef.IncludedInAppId != "" {

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

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

@ -42,7 +42,9 @@ export class GrafanaApp {
app.constant('grafanaVersion', "@grafanaVersion@"); app.constant('grafanaVersion', "@grafanaVersion@");
app.config(($locationProvider, $controllerProvider, $compileProvider, $filterProvider, $provide) => { 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.controller = $controllerProvider.register;
this.registerFunctions.directive = $compileProvider.directive; this.registerFunctions.directive = $compileProvider.directive;

@ -396,6 +396,7 @@ function($, _) {
kbn.valueFormats.ev = kbn.formatBuilders.decimalSIPrefix('eV'); kbn.valueFormats.ev = kbn.formatBuilders.decimalSIPrefix('eV');
kbn.valueFormats.amp = kbn.formatBuilders.decimalSIPrefix('A'); kbn.valueFormats.amp = kbn.formatBuilders.decimalSIPrefix('A');
kbn.valueFormats.volt = kbn.formatBuilders.decimalSIPrefix('V'); kbn.valueFormats.volt = kbn.formatBuilders.decimalSIPrefix('V');
kbn.valueFormats.dBm = kbn.formatBuilders.decimalSIPrefix('dBm');
// Temperature // Temperature
kbn.valueFormats.celsius = kbn.formatBuilders.fixedUnit('°C'); kbn.valueFormats.celsius = kbn.formatBuilders.fixedUnit('°C');
@ -677,6 +678,7 @@ function($, _) {
{text: 'electron volt (eV)', value: 'ev' }, {text: 'electron volt (eV)', value: 'ev' },
{text: 'Ampere (A)', value: 'amp' }, {text: 'Ampere (A)', value: 'amp' },
{text: 'Volt (V)', value: 'volt' }, {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-inline">
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label"> <label class="gf-form-label">
<i class="icon-gf icon-gf-datasource"></i> <i class="icon-gf icon-gf-datasources"></i>
</label> </label>
<label class="gf-form-label"> <label class="gf-form-label">
Panel data source Panel data source

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

@ -45,7 +45,7 @@
<ul> <ul>
<li>$m = replaced with measurement name</li> <li>$m = replaced with measurement name</li>
<li>$measurement = 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>$col = replaced with column name</li>
<li>$tag_hostname = replaced with the value of the hostname tag</li> <li>$tag_hostname = replaced with the value of the hostname tag</li>
<li>You can also use [[tag_hostname]] pattern replacement syntax</li> <li>You can also use [[tag_hostname]] pattern replacement syntax</li>

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

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

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

@ -43,7 +43,7 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
return value.replace(/[\\^$*+?.()|[\]{}]/g, '\\\\$&'); 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 no multi or include all do not regexEscape
if (!variable.multi && !variable.includeAll) { if (!variable.multi && !variable.includeAll) {
return value; return value;
@ -59,6 +59,7 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
// Called once per panel (graph) // Called once per panel (graph)
this.query = function(options) { this.query = function(options) {
var self = this;
var start = getPrometheusTime(options.range.from, false); var start = getPrometheusTime(options.range.from, false);
var end = getPrometheusTime(options.range.to, true); var end = getPrometheusTime(options.range.to, true);
@ -73,7 +74,7 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
activeTargets.push(target); activeTargets.push(target);
var query: any = {}; 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 interval = target.interval || options.interval;
var intervalFactor = target.intervalFactor || 1; var intervalFactor = target.intervalFactor || 1;
@ -99,7 +100,6 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
return this.performTimeSeriesQuery(query, start, end); return this.performTimeSeriesQuery(query, start, end);
}, this)); }, this));
var self = this;
return $q.all(allQueryPromise) return $q.all(allQueryPromise)
.then(function(allResponse) { .then(function(allResponse) {
var result = []; var result = [];
@ -160,7 +160,7 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
var interpolated; var interpolated;
try { try {
interpolated = templateSrv.replace(expr, {}, interpolateQueryExpr); interpolated = templateSrv.replace(expr, {}, this.interpolateQueryExpr);
} catch (err) { } catch (err) {
return $q.reject(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 rangeDiff = Math.ceil((range.to.valueOf() - range.from.valueOf()) / 1000);
var endTime = range.to.utc().format('YYYY-MM-DD HH:mm'); var endTime = range.to.utc().format('YYYY-MM-DD HH:mm');
var expr = { 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', range_input: rangeDiff + 's',
end_input: endTime, end_input: endTime,
step_input: '', step_input: '',

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

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

@ -92,6 +92,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

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

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

@ -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: [ 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,7 +201,8 @@ class SingleStatCtrl extends MetricsPanelCtrl {
} }
} }
// check value to text mappings // check value to text mappings if its enabled
if (this.panel.mappingType === 1) {
for (var i = 0; i < this.panel.valueMaps.length; i++) { for (var i = 0; i < this.panel.valueMaps.length; i++) {
var map = this.panel.valueMaps[i]; var map = this.panel.valueMaps[i];
// special null case // special null case
@ -211,6 +221,27 @@ class SingleStatCtrl extends MetricsPanelCtrl {
return; return;
} }
} }
} 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 range mapping
var from = parseFloat(map.from);
var to = parseFloat(map.to);
if (to >= data.valueRounded && from <= data.valueRounded) {
data.valueFormated = map.text;
return;
}
}
}
if (data.value === null || data.value === void 0) { if (data.value === null || data.value === void 0) {
data.valueFormated = "no value"; data.valueFormated = "no value";
@ -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;

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

Loading…
Cancel
Save