Commit ecba23e8 by bergquist

Merge branch 'master' into influxdb_alias_seriename

parents f0a0e647 70b9ba25
# 4.0-pre (unreleased) # 4.0-beta2 (unrelased)
### Bugfixes
* **Graph Panel**: Bar width if bars was only used in series override, [#6528](https://github.com/grafana/grafana/issues/6528)
# 4.0-beta1 (2016-11-09)
### Enhancements ### Enhancements
* **Login**: Adds option to disable username/password logins, closes [#4674](https://github.com/grafana/grafana/issues/4674) * **Login**: Adds option to disable username/password logins, closes [#4674](https://github.com/grafana/grafana/issues/4674)
...@@ -18,12 +23,13 @@ ...@@ -18,12 +23,13 @@
* **Background Tasks**: Now support automatic purging of old rendered images, closes [#2172](https://github.com/grafana/grafana/issues/2172) * **Background Tasks**: Now support automatic purging of old rendered images, closes [#2172](https://github.com/grafana/grafana/issues/2172)
* **Dashboard**: After inactivity hide nav/row actions, fade to nice clean view, can be toggled with `d v`, also added kiosk mode, toggled via `d k` [#6476](https://github.com/grafana/grafana/issues/6476) * **Dashboard**: After inactivity hide nav/row actions, fade to nice clean view, can be toggled with `d v`, also added kiosk mode, toggled via `d k` [#6476](https://github.com/grafana/grafana/issues/6476)
* **Dashboard**: Improved dashboard row menu & add panel UX [#6442](https://github.com/grafana/grafana/issues/6442) * **Dashboard**: Improved dashboard row menu & add panel UX [#6442](https://github.com/grafana/grafana/issues/6442)
* **Graph Panel**: Support for stacking null values [#2912](https://github.com/grafana/grafana/issues/2912), [#6287](https://github.com/grafana/grafana/issues/6287), thanks @benrubson!
### Breaking changes ### Breaking changes
* **SystemD**: Change systemd description, closes [#5971](https://github.com/grafana/grafana/pull/5971) * **SystemD**: Change systemd description, closes [#5971](https://github.com/grafana/grafana/pull/5971)
* **lodash upgrade**: Upgraded lodash from 2.4.2 to 4.15.0, this contains a number of breaking changes that could effect plugins. closes [#6021](https://github.com/grafana/grafana/pull/6021) * **lodash upgrade**: Upgraded lodash from 2.4.2 to 4.15.0, this contains a number of breaking changes that could effect plugins. closes [#6021](https://github.com/grafana/grafana/pull/6021)
### Bugfixes ### Bug fixes
* **Table Panel**: Fixed problem when switching to Mixed datasource in metrics tab, fixes [#5999](https://github.com/grafana/grafana/pull/5999) * **Table Panel**: Fixed problem when switching to Mixed datasource in metrics tab, fixes [#5999](https://github.com/grafana/grafana/pull/5999)
* **Playlist**: Fixed problem with play order not matching order defined in playlist, fixes [#5467](https://github.com/grafana/grafana/pull/5467) * **Playlist**: Fixed problem with play order not matching order defined in playlist, fixes [#5467](https://github.com/grafana/grafana/pull/5467)
* **Graph panel**: Fixed problem with auto decimals on y axis when datamin=datamax, fixes [#6070](https://github.com/grafana/grafana/pull/6070) * **Graph panel**: Fixed problem with auto decimals on y axis when datamin=datamax, fixes [#6070](https://github.com/grafana/grafana/pull/6070)
......
...@@ -5,7 +5,7 @@ machine: ...@@ -5,7 +5,7 @@ machine:
GOPATH: "/home/ubuntu/.go_workspace" GOPATH: "/home/ubuntu/.go_workspace"
ORG_PATH: "github.com/grafana" ORG_PATH: "github.com/grafana"
REPO_PATH: "${ORG_PATH}/grafana" REPO_PATH: "${ORG_PATH}/grafana"
GODIST: "go1.7.1.linux-amd64.tar.gz" GODIST: "go1.7.3.linux-amd64.tar.gz"
post: post:
- mkdir -p download - mkdir -p download
- test -e download/$GODIST || curl -o download/$GODIST https://storage.googleapis.com/golang/$GODIST - test -e download/$GODIST || curl -o download/$GODIST https://storage.googleapis.com/golang/$GODIST
......
...@@ -25,31 +25,12 @@ to add and configure a `notification` object. This is done from the Alerting/Not ...@@ -25,31 +25,12 @@ to add and configure a `notification` object. This is done from the Alerting/Not
On the notifications list page hit the `New Notification` button to go the the page where you On the notifications list page hit the `New Notification` button to go the the page where you
can configure and setup a new notification. can configure and setup a new notification.
You you specify name and type, and type specific options. You can also test the notification to make You specify name and type, and type specific options. You can also test the notification to make
sure it's working and setup correctly. sure it's working and setup correctly.
<!-- You can reach this page from the Alerting submenu or Alert List page header. -->
<!-- When you configure a notification you can have it be a global notifiations, meaning -->
<!-- it will be sent for all alerts within Grafana. This is useful to make sure you won’t miss to configure -->
<!-- notifications for an alert. You can find the alert notification page in the main menu under alerting. -->
<!-- -->
<!-- ## Add a notifications to an Alert -->
<!-- You can add and remove notifications from an alert by going to the `Notifications` sub menu in the alerting tab. -->
<!-- -->
<!-- -->
<!-- <img class="no-shadow" src="/img/docs/v4/alerttab_notifications_submenu.png"> -->
<!-- -->
<!-- -->
<!-- Click the `+` button to add a new notification and the `x` to remove. Notifications with a blue backgrounds are enabled by default for all alerts and cannot be modified from this view. -->
<!-- -->
<!-- -->
<!-- <img class="no-shadow" src="/img/docs/v4/add_remove_notifications.png"> -->
<!-- -->
### Send on all alerts ### Send on all alerts
This option will make this notification used for all alert rules, existing and new. When checked this option will make this notification used for all alert rules, existing and new.
## Supported notification types ## Supported notification types
...@@ -61,12 +42,25 @@ To enable email notification you have to setup [SMTP settings](/installation/con ...@@ -61,12 +42,25 @@ To enable email notification you have to setup [SMTP settings](/installation/con
in the Grafana config. Email notification will upload an image of the alert graph to an in the Grafana config. Email notification will upload an image of the alert graph to an
external image destination if available or fallback on attaching the image in the email. external image destination if available or fallback on attaching the image in the email.
### Slack
{{< imgbox max-width="40%" img="/img/docs/v4/slack_notification.png" caption="Alerting Slack Notification" >}}
To set up slack you need to configure an incoming webhook url at slack. You can follow their guide for how
to do that https://api.slack.com/incoming-webhooks If you want to include screenshots of the firing alerts
in the slack messages you have to configure the [external image destination](#external-image-store) in Grafana.
Setting | Description
---------- | -----------
Recipient | allows you to override the slack recipient.
Mention | make it possible to include a mention in the slack notification sent by Grafana. Ex @here or @channel
### Webhook ### Webhook
The webhook notification is a simple way to send information about an state change over HTTP to a custom endpoint. The webhook notification is a simple way to send information about an state change over HTTP to a custom endpoint.
Using this notification you could integrated Grafana into any system you choose, by yourself. Using this notification you could integrated Grafana into any system you choose, by yourself.
Example json schema: Example json body:
```json ```json
{ {
"title": "My alert", "title": "My alert",
...@@ -85,19 +79,6 @@ Example json schema: ...@@ -85,19 +79,6 @@ Example json schema:
} }
``` ```
### Slack
{{< imgbox max-width="40%" img="/img/docs/v4/slack_notification.png" caption="Alerting Slack Notification" >}}
To set up slack you need to configure an incoming webhook url at slack. You can follow their guide for how
to do that https://api.slack.com/incoming-webhooks If you want to include screenshots of the firing alerts
in the slack messages you have to configure the [external image destination](#external-image-store) in Grafana.
Setting | Description
---------- | -----------
Recipient | allows you to override the slack recipient.
Mention | make it possible to include a mention in the slack notification sent by Grafana. Ex @here or @channel
### PagerDuty ### PagerDuty
To set up PagerDuty, all you have to do is to provide an api key. To set up PagerDuty, all you have to do is to provide an api key.
...@@ -120,7 +101,3 @@ config file. ...@@ -120,7 +101,3 @@ config file.
This is not an optional requirement, you can get slack and email notifications without setting this up. This is not an optional requirement, you can get slack and email notifications without setting this up.
+++ +++
title = "Alerting Engine Rules Guide" title = "Alerting Engine & Rules Guide"
description = "Configuring Alert Rules" description = "Configuring Alert Rules"
keywords = ["grafana", "alerting", "guide", "rules"] keywords = ["grafana", "alerting", "guide", "rules"]
type = "docs" type = "docs"
...@@ -73,7 +73,7 @@ in the scenario below. ...@@ -73,7 +73,7 @@ in the scenario below.
- No new notifications are sent as the alert rule is already in state `Alerting`. - No new notifications are sent as the alert rule is already in state `Alerting`.
So as you can see from the above scenario Grafana will not send out notifications when other series cause the alert So as you can see from the above scenario Grafana will not send out notifications when other series cause the alert
to fire if the rule already is in state ´Alerting`. To improve support for queries that return multiple series to fire if the rule already is in state `Alerting`. To improve support for queries that return multiple series
we plan to track state **per series** in a future release. we plan to track state **per series** in a future release.
### No Data / Null values ### No Data / Null values
...@@ -107,6 +107,12 @@ The message can contain anything, information about how you might solve the issu ...@@ -107,6 +107,12 @@ The message can contain anything, information about how you might solve the issu
The actual notifications are configured and shared between multiple alerts. Read the The actual notifications are configured and shared between multiple alerts. Read the
[Notifications]({{< relref "notifications.md" >}}) guide for how to configure and setup notifications. [Notifications]({{< relref "notifications.md" >}}) guide for how to configure and setup notifications.
## Alert State History & Annotations
Alert state changes are recorded in the internal annotation table in Grafana's database. The state changes
are visualized as annotations in the alert rule's graph panel. You can also go into the `State history`
submenu in the alert tab to view & clear state history.
## Troubleshooting ## Troubleshooting
{{< imgbox max-width="40%" img="/img/docs/v4/alert_test_rule.png" caption="Test Rule" >}} {{< imgbox max-width="40%" img="/img/docs/v4/alert_test_rule.png" caption="Test Rule" >}}
......
...@@ -14,13 +14,22 @@ weight = 1 ...@@ -14,13 +14,22 @@ weight = 1
Description | Download Description | Download
------------ | ------------- ------------ | -------------
Stable .deb for Debian-based Linux | [3.1.1 (x86-64 deb)](https://grafanarel.s3.amazonaws.com/builds/grafana_3.1.1-1470047149_amd64.deb) Stable for Debian-based Linux | [3.1.1 (x86-64 deb)](https://grafanarel.s3.amazonaws.com/builds/grafana_3.1.1-1470047149_amd64.deb)
Latest Beta for Debian-based Linux | [4.0.0-beta1 (x86-64 deb)](https://grafanarel.s3.amazonaws.com/builds/grafana_4.0.0-1478693311beta1_amd64.deb)
## Install Stable ## Install Stable
$ wget https://grafanarel.s3.amazonaws.com/builds/grafana_3.1.1-1470047149_amd64.deb ```
$ wget https://grafanarel.s3.amazonaws.com/builds/grafana_3.1.1-1470047149_amd64.deb
$ sudo apt-get install -y adduser libfontconfig
$ sudo dpkg -i grafana_3.1.1-1470047149_amd64.deb
```
## Install Latest Beta
$ wget https://grafanarel.s3.amazonaws.com/builds/grafana_4.0.0-1478693311beta1_amd64.deb
$ sudo apt-get install -y adduser libfontconfig $ sudo apt-get install -y adduser libfontconfig
$ sudo dpkg -i grafana_3.1.1-1470047149_amd64.deb $ sudo dpkg -i grafana_4.0.0-1478693311beta1_amd64.deb
## APT Repository ## APT Repository
......
...@@ -14,7 +14,8 @@ weight = 2 ...@@ -14,7 +14,8 @@ weight = 2
Description | Download Description | Download
------------ | ------------- ------------ | -------------
Stable .RPM for CentOS / Fedora / OpenSuse / Redhat Linux | [3.1.1 (x86-64 rpm)](https://grafanarel.s3.amazonaws.com/builds/grafana-3.1.1-1470047149.x86_64.rpm) Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [3.1.1 (x86-64 rpm)](https://grafanarel.s3.amazonaws.com/builds/grafana-3.1.1-1470047149.x86_64.rpm)
Latest Beta for CentOS / Fedora / OpenSuse / Redhat Linux | [4.0.0-beta1 (x86-64 rpm)](https://grafanarel.s3.amazonaws.com/builds/grafana-4.0.0-1478693311beta1.x86_64.rpm)
## Install Stable ## Install Stable
...@@ -33,6 +34,21 @@ Or install manually using `rpm`. ...@@ -33,6 +34,21 @@ Or install manually using `rpm`.
$ sudo rpm -i --nodeps grafana-3.1.1-1470047149.x86_64.rpm $ sudo rpm -i --nodeps grafana-3.1.1-1470047149.x86_64.rpm
## Or Install Latest Beta
$ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-4.0.0-1478693311beta1.x86_64.rpm
Or install manually using `rpm`.
#### On CentOS / Fedora / Redhat:
$ sudo yum install initscripts fontconfig
$ sudo rpm -Uvh grafana-4.0.0-1478693311beta1.x86_64.rpm
#### On OpenSuse:
$ sudo rpm -i --nodeps grafana-4.0.0-1478693311beta1.x86_64.rpm
## Install via YUM Repository ## Install via YUM Repository
Add the following to a new file at `/etc/yum.repos.d/grafana.repo` Add the following to a new file at `/etc/yum.repos.d/grafana.repo`
......
...@@ -13,7 +13,9 @@ weight = 3 ...@@ -13,7 +13,9 @@ weight = 3
Description | Download Description | Download
------------ | ------------- ------------ | -------------
Stable Zip package for Windows | [grafana.3.1.1.windows-x64.zip](https://grafanarel.s3.amazonaws.com/winbuilds/dist/grafana-3.1.1.windows-x64.zip) Latest stable package for Windows | [grafana.3.1.1.windows-x64.zip](https://grafanarel.s3.amazonaws.com/winbuilds/dist/grafana-3.1.1.windows-x64.zip)
Latest beta package for Windows | [grafana.4.0.0-beta1.windows-x64.zip](https://grafanarel.s3.amazonaws.com/builds/grafana-4.0.0-beta1.windows-x64.zip)
## Configure ## Configure
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
"company": "Coding Instinct AB" "company": "Coding Instinct AB"
}, },
"name": "grafana", "name": "grafana",
"version": "4.0.0-pre1", "version": "4.0.0-beta1",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "http://github.com/grafana/grafana.git" "url": "http://github.com/grafana/grafana.git"
......
...@@ -57,7 +57,7 @@ func (e *InfluxDBExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, ...@@ -57,7 +57,7 @@ func (e *InfluxDBExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice,
} }
if setting.Env == setting.DEV { if setting.Env == setting.DEV {
glog.Debug("Influxdb query", "raw query", query) glog.Debug("Influxdb query", "raw query", rawQuery)
} }
req, err := e.createRequest(rawQuery) req, err := e.createRequest(rawQuery)
......
...@@ -12,7 +12,6 @@ type InfluxdbQueryParser struct{} ...@@ -12,7 +12,6 @@ type InfluxdbQueryParser struct{}
func (qp *InfluxdbQueryParser) Parse(model *simplejson.Json, dsInfo *tsdb.DataSourceInfo) (*Query, error) { func (qp *InfluxdbQueryParser) Parse(model *simplejson.Json, dsInfo *tsdb.DataSourceInfo) (*Query, error) {
policy := model.Get("policy").MustString("default") policy := model.Get("policy").MustString("default")
rawQuery := model.Get("query").MustString("") rawQuery := model.Get("query").MustString("")
interval := model.Get("interval").MustString("")
alias := model.Get("alias").MustString("") alias := model.Get("alias").MustString("")
measurement := model.Get("measurement").MustString("") measurement := model.Get("measurement").MustString("")
...@@ -37,7 +36,8 @@ func (qp *InfluxdbQueryParser) Parse(model *simplejson.Json, dsInfo *tsdb.DataSo ...@@ -37,7 +36,8 @@ func (qp *InfluxdbQueryParser) Parse(model *simplejson.Json, dsInfo *tsdb.DataSo
return nil, err return nil, err
} }
if interval == "" { interval := model.Get("interval").MustString("")
if interval == "" && dsInfo.JsonData != nil {
dsInterval := dsInfo.JsonData.Get("timeInterval").MustString("") dsInterval := dsInfo.JsonData.Get("timeInterval").MustString("")
if dsInterval != "" { if dsInterval != "" {
interval = dsInterval interval = dsInterval
......
...@@ -5,9 +5,15 @@ import ( ...@@ -5,9 +5,15 @@ import (
"strconv" "strconv"
"strings" "strings"
"regexp"
"github.com/grafana/grafana/pkg/tsdb" "github.com/grafana/grafana/pkg/tsdb"
) )
var (
regexpOperatorPattern *regexp.Regexp = regexp.MustCompile(`^\/.*\/$`)
)
type QueryBuilder struct{} type QueryBuilder struct{}
func (qb *QueryBuilder) Build(query *Query, queryContext *tsdb.QueryContext) (string, error) { func (qb *QueryBuilder) Build(query *Query, queryContext *tsdb.QueryContext) (string, error) {
...@@ -43,18 +49,28 @@ func (qb *QueryBuilder) renderTags(query *Query) []string { ...@@ -43,18 +49,28 @@ func (qb *QueryBuilder) renderTags(query *Query) []string {
str += " " str += " "
} }
value := tag.Value //If the operator is missing we fall back to sensible defaults
nValue, err := strconv.ParseFloat(tag.Value, 64) if tag.Operator == "" {
if regexpOperatorPattern.Match([]byte(tag.Value)) {
tag.Operator = "=~"
} else {
tag.Operator = "="
}
}
textValue := ""
numericValue, err := strconv.ParseFloat(tag.Value, 64)
// quote value unless regex or number
if tag.Operator == "=~" || tag.Operator == "!~" { if tag.Operator == "=~" || tag.Operator == "!~" {
value = fmt.Sprintf("%s", value) textValue = tag.Value
} else if err == nil { } else if err == nil {
value = fmt.Sprintf("%v", nValue) textValue = fmt.Sprintf("%v", numericValue)
} else { } else {
value = fmt.Sprintf("'%s'", value) textValue = fmt.Sprintf("'%s'", tag.Value)
} }
res = append(res, fmt.Sprintf(`%s"%s" %s %s`, str, tag.Key, tag.Operator, value)) res = append(res, fmt.Sprintf(`%s"%s" %s %s`, str, tag.Key, tag.Operator, textValue))
} }
return res return res
......
...@@ -86,16 +86,34 @@ func TestInfluxdbQueryBuilder(t *testing.T) { ...@@ -86,16 +86,34 @@ func TestInfluxdbQueryBuilder(t *testing.T) {
So(rawQuery, ShouldEqual, `Raw query`) So(rawQuery, ShouldEqual, `Raw query`)
}) })
Convey("can render normal tags without operator", func() {
query := &Query{Tags: []*Tag{&Tag{Operator: "", Value: `value`, Key: "key"}}}
So(strings.Join(builder.renderTags(query), ""), ShouldEqual, `"key" = 'value'`)
})
Convey("can render regex tags without operator", func() {
query := &Query{Tags: []*Tag{&Tag{Operator: "", Value: `/value/`, Key: "key"}}}
So(strings.Join(builder.renderTags(query), ""), ShouldEqual, `"key" =~ /value/`)
})
Convey("can render regex tags", func() { Convey("can render regex tags", func() {
query := &Query{Tags: []*Tag{&Tag{Operator: "=~", Value: "value", Key: "key"}}} query := &Query{Tags: []*Tag{&Tag{Operator: "=~", Value: `/value/`, Key: "key"}}}
So(strings.Join(builder.renderTags(query), ""), ShouldEqual, `"key" =~ value`) So(strings.Join(builder.renderTags(query), ""), ShouldEqual, `"key" =~ /value/`)
}) })
Convey("can render number tags", func() { Convey("can render number tags", func() {
query := &Query{Tags: []*Tag{&Tag{Operator: "=", Value: "1", Key: "key"}}} query := &Query{Tags: []*Tag{&Tag{Operator: "=", Value: "10001", Key: "key"}}}
So(strings.Join(builder.renderTags(query), ""), ShouldEqual, `"key" = 10001`)
})
Convey("can render number tags with decimals", func() {
query := &Query{Tags: []*Tag{&Tag{Operator: "=", Value: "10001.1", Key: "key"}}}
So(strings.Join(builder.renderTags(query), ""), ShouldEqual, `"key" = 1`) So(strings.Join(builder.renderTags(query), ""), ShouldEqual, `"key" = 10001.1`)
}) })
Convey("can render string tags", func() { Convey("can render string tags", func() {
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
<p class="small" style="position: absolute; top: 48px; right: 10px"> <p class="small" style="position: absolute; top: 48px; right: 10px">
<span class="shortcut-table-key">mod</span> = <span class="shortcut-table-key">mod</span> =
<span class="muted">CTRL on windows, CMD key on Mac</span> <span class="muted">CTRL on windows or linux and CMD key on Mac</span>
</p> </p>
<div ng-repeat="(category, shortcuts) in ctrl.shortcuts" class="shortcut-category"> <div ng-repeat="(category, shortcuts) in ctrl.shortcuts" class="shortcut-category">
......
...@@ -102,6 +102,7 @@ export default class TimeSeries { ...@@ -102,6 +102,7 @@ export default class TimeSeries {
this.stats.min = Number.MAX_VALUE; this.stats.min = Number.MAX_VALUE;
this.stats.avg = null; this.stats.avg = null;
this.stats.current = null; this.stats.current = null;
this.stats.timeStep = Number.MAX_VALUE;
this.allIsNull = true; this.allIsNull = true;
this.allIsZero = true; this.allIsZero = true;
...@@ -110,11 +111,22 @@ export default class TimeSeries { ...@@ -110,11 +111,22 @@ export default class TimeSeries {
var currentTime; var currentTime;
var currentValue; var currentValue;
var nonNulls = 0; var nonNulls = 0;
var previousTime;
for (var i = 0; i < this.datapoints.length; i++) { for (var i = 0; i < this.datapoints.length; i++) {
currentValue = this.datapoints[i][0]; currentValue = this.datapoints[i][0];
currentTime = this.datapoints[i][1]; currentTime = this.datapoints[i][1];
// Due to missing values we could have different timeStep all along the series
// so we have to find the minimum one (could occur with aggregators such as ZimSum)
if (previousTime !== undefined) {
let timeStep = currentTime - previousTime;
if (timeStep < this.stats.timeStep) {
this.stats.timeStep = timeStep;
}
}
previousTime = currentTime;
if (currentValue === null) { if (currentValue === null) {
if (ignoreNulls) { continue; } if (ignoreNulls) { continue; }
if (nullAsZero) { if (nullAsZero) {
...@@ -145,10 +157,6 @@ export default class TimeSeries { ...@@ -145,10 +157,6 @@ export default class TimeSeries {
result.push([currentTime, currentValue]); result.push([currentTime, currentValue]);
} }
if (this.datapoints.length >= 2) {
this.stats.timeStep = this.datapoints[1][1] - this.datapoints[0][1];
}
if (this.stats.max === -Number.MAX_VALUE) { this.stats.max = null; } if (this.stats.max === -Number.MAX_VALUE) { this.stats.max = null; }
if (this.stats.min === Number.MAX_VALUE) { this.stats.min = null; } if (this.stats.min === Number.MAX_VALUE) { this.stats.min = null; }
......
///<reference path="../../../headers/common.d.ts" /> ///<reference path="../../../headers/common.d.ts" />
import _ from 'lodash';
class GrafanaDatasource { class GrafanaDatasource {
/** @ngInject */ /** @ngInject */
constructor(private backendSrv) {} constructor(private backendSrv) {}
query(options) { query(options) {
return this.backendSrv.get('/api/metrics/test', { return this.backendSrv.post('/api/tsdb/query', {
from: options.range.from.valueOf(), from: options.range.from.valueOf().toString(),
to: options.range.to.valueOf(), to: options.range.to.valueOf().toString(),
scenario: 'random_walk', queries: [
interval: options.intervalMs, {
maxDataPoints: options.maxDataPoints "refId": "A",
"scenarioId": "random_walk",
"intervalMs": options.intervalMs,
"maxDataPoints": options.maxDataPoints,
}
]
}).then(res => {
var data = [];
if (res.results) {
_.forEach(res.results, queryRes => {
for (let series of queryRes.series) {
data.push({
target: series.name,
datapoints: series.points
});
}
});
}
return {data: data};
}); });
} }
......
...@@ -183,6 +183,24 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) { ...@@ -183,6 +183,24 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) {
} }
} }
// Series could have different timeSteps,
// let's find the smallest one so that bars are correctly rendered.
function getMinTimeStepOfSeries(data) {
var min = Number.MAX_VALUE;
for (let i = 0; i < data.length; i++) {
if (!data[i].stats.timeStep) {
continue;
}
if (data[i].stats.timeStep < min) {
min = data[i].stats.timeStep;
}
}
return min;
}
// Function for rendering panel // Function for rendering panel
function render_panel() { function render_panel() {
panelWidth = elem.width(); panelWidth = elem.width();
...@@ -279,9 +297,7 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) { ...@@ -279,9 +297,7 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) {
break; break;
} }
default: { default: {
if (data.length && data[0].stats.timeStep) { options.series.bars.barWidth = getMinTimeStepOfSeries(data) / 1.5;
options.series.bars.barWidth = data[0].stats.timeStep / 1.5;
}
addTimeAxis(options); addTimeAxis(options);
break; break;
} }
......
...@@ -2,7 +2,7 @@ define([ ...@@ -2,7 +2,7 @@ define([
'jquery', 'jquery',
'lodash' 'lodash'
], ],
function ($, _) { function ($) {
'use strict'; 'use strict';
function GraphTooltip(elem, dashboard, scope, getSeriesFn) { function GraphTooltip(elem, dashboard, scope, getSeriesFn) {
...@@ -21,7 +21,10 @@ function ($, _) { ...@@ -21,7 +21,10 @@ function ($, _) {
var initial = last*ps; var initial = last*ps;
var len = series.datapoints.points.length; var len = series.datapoints.points.length;
for (var j = initial; j < len; j += ps) { for (var j = initial; j < len; j += ps) {
if (series.datapoints.points[j] > posX) { // Special case of a non stepped line, highlight the very last point just before a null point
if ((series.datapoints.points[initial] != null && series.datapoints.points[j] == null && ! series.lines.steps)
//normal case
|| series.datapoints.points[j] > posX) {
return Math.max(j - ps, 0)/ps; return Math.max(j - ps, 0)/ps;
} }
} }
...@@ -51,23 +54,35 @@ function ($, _) { ...@@ -51,23 +54,35 @@ function ($, _) {
//now we know the current X (j) position for X and Y values //now we know the current X (j) position for X and Y values
var last_value = 0; //needed for stacked values var last_value = 0; //needed for stacked values
var minDistance, minTime;
for (i = 0; i < seriesList.length; i++) { for (i = 0; i < seriesList.length; i++) {
series = seriesList[i]; series = seriesList[i];
if (!series.data.length || (panel.legend.hideEmpty && series.allIsNull)) { if (!series.data.length || (panel.legend.hideEmpty && series.allIsNull)) {
// Init value & yaxis so that it does not brake series sorting
results.push({ hidden: true, value: 0, yaxis: 0 }); results.push({ hidden: true, value: 0, yaxis: 0 });
continue; continue;
} }
if (!series.data.length || (panel.legend.hideZero && series.allIsZero)) { if (!series.data.length || (panel.legend.hideZero && series.allIsZero)) {
// Init value & yaxis so that it does not brake series sorting
results.push({ hidden: true, value: 0, yaxis: 0 }); results.push({ hidden: true, value: 0, yaxis: 0 });
continue; continue;
} }
hoverIndex = this.findHoverIndexFromData(pos.x, series); hoverIndex = this.findHoverIndexFromData(pos.x, series);
hoverDistance = Math.abs(pos.x - series.data[hoverIndex][0]); hoverDistance = pos.x - series.data[hoverIndex][0];
pointTime = series.data[hoverIndex][0]; pointTime = series.data[hoverIndex][0];
// Take the closest point before the cursor, or if it does not exist, the closest after
if (! minDistance
|| (hoverDistance >=0 && (hoverDistance < minDistance || minDistance < 0))
|| (hoverDistance < 0 && hoverDistance > minDistance)) {
minDistance = hoverDistance;
minTime = pointTime;
}
if (series.stack) { if (series.stack) {
if (panel.tooltip.value_type === 'individual') { if (panel.tooltip.value_type === 'individual') {
value = series.data[hoverIndex][1]; value = series.data[hoverIndex][1];
...@@ -89,6 +104,7 @@ function ($, _) { ...@@ -89,6 +104,7 @@ function ($, _) {
hoverIndex = this.findHoverIndexFromDataPoints(pos.x, series, hoverIndex); hoverIndex = this.findHoverIndexFromDataPoints(pos.x, series, hoverIndex);
} }
// Be sure we have a yaxis so that it does not brake series sorting
yaxis = 0; yaxis = 0;
if (series.yaxis) { if (series.yaxis) {
yaxis = series.yaxis.n; yaxis = series.yaxis.n;
...@@ -106,8 +122,8 @@ function ($, _) { ...@@ -106,8 +122,8 @@ function ($, _) {
}); });
} }
// Find point which closer to pointer // Time of the point closer to pointer
results.time = _.min(results, 'distance').time; results.time = minTime;
return results; return results;
}; };
...@@ -153,6 +169,8 @@ function ($, _) { ...@@ -153,6 +169,8 @@ function ($, _) {
seriesHtml = ''; seriesHtml = '';
absoluteTime = dashboard.formatDate(seriesHoverInfo.time, tooltipFormat);
// Dynamically reorder the hovercard for the current time point if the // Dynamically reorder the hovercard for the current time point if the
// option is enabled, sort by yaxis by default. // option is enabled, sort by yaxis by default.
if (panel.tooltip.sort === 2) { if (panel.tooltip.sort === 2) {
...@@ -169,8 +187,6 @@ function ($, _) { ...@@ -169,8 +187,6 @@ function ($, _) {
}); });
} }
var distance, time;
for (i = 0; i < seriesHoverInfo.length; i++) { for (i = 0; i < seriesHoverInfo.length; i++) {
hoverInfo = seriesHoverInfo[i]; hoverInfo = seriesHoverInfo[i];
...@@ -178,11 +194,6 @@ function ($, _) { ...@@ -178,11 +194,6 @@ function ($, _) {
continue; continue;
} }
if (! distance || hoverInfo.distance < distance) {
distance = hoverInfo.distance;
time = hoverInfo.time;
}
var highlightClass = ''; var highlightClass = '';
if (item && i === item.seriesIndex) { if (item && i === item.seriesIndex) {
highlightClass = 'graph-tooltip-list-item--highlight'; highlightClass = 'graph-tooltip-list-item--highlight';
...@@ -198,7 +209,6 @@ function ($, _) { ...@@ -198,7 +209,6 @@ function ($, _) {
plot.highlight(hoverInfo.index, hoverInfo.hoverIndex); plot.highlight(hoverInfo.index, hoverInfo.hoverIndex);
} }
absoluteTime = dashboard.formatDate(time, tooltipFormat);
self.showTooltip(absoluteTime, seriesHtml, pos); self.showTooltip(absoluteTime, seriesHtml, pos);
} }
// single series tooltip // single series tooltip
......
...@@ -66,7 +66,7 @@ class GraphCtrl extends MetricsPanelCtrl { ...@@ -66,7 +66,7 @@ class GraphCtrl extends MetricsPanelCtrl {
// fill factor // fill factor
fill : 1, fill : 1,
// line width in pixels // line width in pixels
linewidth : 2, linewidth : 1,
// show hide points // show hide points
points : false, points : false,
// point radius in pixels // point radius in pixels
......
...@@ -135,7 +135,7 @@ describe('grafanaGraph', function() { ...@@ -135,7 +135,7 @@ describe('grafanaGraph', function() {
}); });
it('should set barWidth', function() { it('should set barWidth', function() {
expect(ctx.plotOptions.series.bars.barWidth).to.be(10/1.5); expect(ctx.plotOptions.series.bars.barWidth).to.be(1/1.5);
}); });
}); });
......
...@@ -59,34 +59,34 @@ ...@@ -59,34 +59,34 @@
.dashnav-action-icons, .dashnav-action-icons,
.dashnav-move-timeframe { .dashnav-move-timeframe {
opacity: 0; opacity: 0;
transition: opacity 1.5s ease-in-out; transition: all 1.5s ease-in-out 1s;
} }
// navbar buttons // navbar buttons
.navbar-brand-btn, .navbar-brand-btn,
.navbar-inner { .navbar-inner {
border: none; border-color: transparent;
background: transparent; background: transparent;
transition: background 1.5s ease-in-out; transition: all 1.5s ease-in-out 1s;
.fa { .fa {
opacity: 0; opacity: 0;
transition: opacity 1.5s ease-in-out; transition: all 1.5s ease-in-out 1s;
} }
} }
.navbar-page-btn { .navbar-page-btn {
border: none; border-color: transparent;
transform: translate3d(-50px, 0, 0);
background: transparent; background: transparent;
transition: transform 1.5s ease-in-out; transform: translate3d(-50px, 0, 0);
transition: all 1.5s ease-in-out 1s;
.icon-gf { .icon-gf {
opacity: 0; opacity: 0;
transition: opacity 1.5s ease-in-out; transition: all 1.5s ease-in-out 1s;
} }
} }
.gf-timepicker-nav-btn { .gf-timepicker-nav-btn {
transform: translate3d(40px, 0, 0); transform: translate3d(40px, 0, 0);
transition: transform 1.5s ease-in-out; transition: transform 1.5s ease-in-out 1s;
} }
} }
...@@ -50,7 +50,6 @@ ...@@ -50,7 +50,6 @@
.panel-alert-state { .panel-alert-state {
&--alerting { &--alerting {
background-color: mix($critical,$panel-bg, 3%);
animation: alerting-panel 1.6s cubic-bezier(1,.1,.73,1) 0s infinite alternate; animation: alerting-panel 1.6s cubic-bezier(1,.1,.73,1) 0s infinite alternate;
box-shadow: 0 0 10px rgba($critical,0.5); box-shadow: 0 0 10px rgba($critical,0.5);
opacity: 1; opacity: 1;
......
...@@ -1201,24 +1201,21 @@ Licensed under the MIT license. ...@@ -1201,24 +1201,21 @@ Licensed under the MIT license.
points[k + m] = null; points[k + m] = null;
} }
} }
else {
// a little bit of line specific stuff that if (insertSteps && k > 0 && (!nullify || points[k - ps] != null)) {
// perhaps shouldn't be here, but lacking // copy the point to make room for a middle point
// better means... for (m = 0; m < ps; ++m)
if (insertSteps && k > 0 points[k + ps + m] = points[k + m];
&& points[k - ps] != null
&& points[k - ps] != points[k] // middle point has same y
&& points[k - ps + 1] != points[k + 1]) { points[k + 1] = points[k - ps + 1] || 0;
// copy the point to make room for a middle point
for (m = 0; m < ps; ++m) // if series has null values, let's give the last !null value a nice step
points[k + ps + m] = points[k + m]; if(nullify)
points[k] = p[0];
// middle point has same y
points[k + 1] = points[k - ps + 1]; // we've added a point, better reflect that
k += ps;
// we've added a point, better reflect that
k += ps;
}
} }
} }
} }
......
...@@ -78,41 +78,46 @@ charts or filled areas). ...@@ -78,41 +78,46 @@ charts or filled areas).
i = 0, j = 0, l, m; i = 0, j = 0, l, m;
while (true) { while (true) {
if (i >= points.length) // browse all points from the current series and from the previous series
if (i >= points.length && j >= otherpoints.length)
break; break;
// newpoints will replace current series with
// as many points as different timestamps we have in the 2 (current & previous) series
l = newpoints.length; l = newpoints.length;
px = points[i + keyOffset];
if (points[i] == null) { py = points[i + accumulateOffset];
// copy gaps qx = otherpoints[j + keyOffset];
for (m = 0; m < ps; ++m) qy = otherpoints[j + accumulateOffset];
newpoints.push(points[i + m]); bottom = 0;
if (i < points.length && px == null) {
// let's ignore null points from current series, nothing to do with them
i += ps; i += ps;
} }
else if (j >= otherpoints.length) { else if (j < otherpoints.length && qx == null) {
// for lines, we can't use the rest of the points // let's ignore null points from previous series, nothing to do with them
if (!withlines) { j += otherps;
for (m = 0; m < ps; ++m)
newpoints.push(points[i + m]);
}
i += ps;
} }
else if (otherpoints[j] == null) { else if (i >= points.length) {
// oops, got a gap // no more points in the current series, simply take the remaining points
// from the previous series so that next series will correctly stack
for (m = 0; m < ps; ++m) for (m = 0; m < ps; ++m)
newpoints.push(null); newpoints.push(otherpoints[j + m]);
fromgap = true; bottom = qy;
j += otherps; j += otherps;
} }
else if (j >= otherpoints.length) {
// no more points in the previous series, of course let's take
// the remaining points from the current series
for (m = 0; m < ps; ++m)
newpoints.push(points[i + m]);
i += ps;
}
else { else {
// cases where we actually got two points // next available points from current and previous series have the same timestamp
px = points[i + keyOffset];
py = points[i + accumulateOffset];
qx = otherpoints[j + keyOffset];
qy = otherpoints[j + accumulateOffset];
bottom = 0;
if (px == qx) { if (px == qx) {
// so take the point from the current series and skip the previous' one
for (m = 0; m < ps; ++m) for (m = 0; m < ps; ++m)
newpoints.push(points[i + m]); newpoints.push(points[i + m]);
...@@ -122,55 +127,39 @@ charts or filled areas). ...@@ -122,55 +127,39 @@ charts or filled areas).
i += ps; i += ps;
j += otherps; j += otherps;
} }
// next available point with the smallest timestamp is from the previous series
else if (px > qx) { else if (px > qx) {
// we got past point below, might need to // so take the point from the previous series so that next series will correctly stack
// insert interpolated extra point for (m = 0; m < ps; ++m)
if (withlines && i > 0 && points[i - ps] != null) { newpoints.push(otherpoints[j + m]);
intery = py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px);
newpoints.push(qx); // we might be able to interpolate
newpoints.push(intery + qy); if (i > 0 && points[i - ps] != null)
for (m = 2; m < ps; ++m) newpoints[l + accumulateOffset] += py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px);
newpoints.push(points[i + m]);
bottom = qy; bottom = qy;
}
j += otherps; j += otherps;
} }
else { // px < qx // (px < qx) next available point with the smallest timestamp is from the current series
if (fromgap && withlines) { else {
// if we come from a gap, we just skip this point // so of course let's take the point from the current series
i += ps;
continue;
}
for (m = 0; m < ps; ++m) for (m = 0; m < ps; ++m)
newpoints.push(points[i + m]); newpoints.push(points[i + m]);
// we might be able to interpolate a point below, // we might be able to interpolate a point below,
// this can give us a better y // this can give us a better y
if (withlines && j > 0 && otherpoints[j - otherps] != null) if (j > 0 && otherpoints[j - otherps] != null)
bottom = qy + (otherpoints[j - otherps + accumulateOffset] - qy) * (px - qx) / (otherpoints[j - otherps + keyOffset] - qx); bottom = qy + (otherpoints[j - otherps + accumulateOffset] - qy) * (px - qx) / (otherpoints[j - otherps + keyOffset] - qx);
newpoints[l + accumulateOffset] += bottom; newpoints[l + accumulateOffset] += bottom;
i += ps; i += ps;
} }
}
fromgap = false; if (l != newpoints.length && withbottom)
newpoints[l + 2] = bottom;
if (l != newpoints.length && withbottom)
newpoints[l + 2] += bottom;
}
// maintain the line steps invariant
if (withsteps && l != newpoints.length && l > 0
&& newpoints[l] != null
&& newpoints[l] != newpoints[l - ps]
&& newpoints[l + 1] != newpoints[l - ps + 1]) {
for (m = 0; m < ps; ++m)
newpoints[l + ps + m] = newpoints[l + m];
newpoints[l + 1] = newpoints[l - ps + 1];
}
} }
datapoints.points = newpoints; datapoints.points = newpoints;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment