diff --git a/CHANGELOG.md b/CHANGELOG.md index 01315d1b773..55298901618 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ -# 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 * **Login**: Adds option to disable username/password logins, closes [#4674](https://github.com/grafana/grafana/issues/4674) @@ -18,12 +23,13 @@ * **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**: 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 * **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) -### Bugfixes +### Bug fixes * **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) * **Graph panel**: Fixed problem with auto decimals on y axis when datamin=datamax, fixes [#6070](https://github.com/grafana/grafana/pull/6070) diff --git a/circle.yml b/circle.yml index 39dc626df04..2422715bda3 100644 --- a/circle.yml +++ b/circle.yml @@ -5,7 +5,7 @@ machine: GOPATH: "/home/ubuntu/.go_workspace" ORG_PATH: "github.com/grafana" REPO_PATH: "${ORG_PATH}/grafana" - GODIST: "go1.7.1.linux-amd64.tar.gz" + GODIST: "go1.7.3.linux-amd64.tar.gz" post: - mkdir -p download - test -e download/$GODIST || curl -o download/$GODIST https://storage.googleapis.com/golang/$GODIST diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/notifications.md index 18745b222b7..ce9dcdf1b93 100644 --- a/docs/sources/alerting/notifications.md +++ b/docs/sources/alerting/notifications.md @@ -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 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. - - - - - - - - - - - - - - - - - - - ### 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 @@ -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 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 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. -Example json schema: +Example json body: ```json { "title": "My alert", @@ -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 To set up PagerDuty, all you have to do is to provide an api key. @@ -120,7 +101,3 @@ config file. This is not an optional requirement, you can get slack and email notifications without setting this up. - - - - diff --git a/docs/sources/alerting/rules.md b/docs/sources/alerting/rules.md index 823637941da..ecab88f48d2 100644 --- a/docs/sources/alerting/rules.md +++ b/docs/sources/alerting/rules.md @@ -1,5 +1,5 @@ +++ -title = "Alerting Engine Rules Guide" +title = "Alerting Engine & Rules Guide" description = "Configuring Alert Rules" keywords = ["grafana", "alerting", "guide", "rules"] type = "docs" @@ -73,7 +73,7 @@ in the scenario below. - 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 -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. ### No Data / Null values @@ -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 [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 {{< imgbox max-width="40%" img="/img/docs/v4/alert_test_rule.png" caption="Test Rule" >}} diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index 2c37ef4d14f..90ebd2053fd 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -14,13 +14,22 @@ weight = 1 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 - $ 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 dpkg -i grafana_3.1.1-1470047149_amd64.deb + $ sudo dpkg -i grafana_4.0.0-1478693311beta1_amd64.deb ## APT Repository diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md index b23e851182b..cf33e69aa12 100644 --- a/docs/sources/installation/rpm.md +++ b/docs/sources/installation/rpm.md @@ -14,7 +14,8 @@ weight = 2 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 @@ -33,6 +34,21 @@ Or install manually using `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 Add the following to a new file at `/etc/yum.repos.d/grafana.repo` diff --git a/docs/sources/installation/windows.md b/docs/sources/installation/windows.md index f75f29085c5..537e156e948 100644 --- a/docs/sources/installation/windows.md +++ b/docs/sources/installation/windows.md @@ -13,7 +13,9 @@ weight = 3 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 diff --git a/package.json b/package.json index e1b009ecf24..a17bf04dbcc 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "company": "Coding Instinct AB" }, "name": "grafana", - "version": "4.0.0-pre1", + "version": "4.0.0-beta1", "repository": { "type": "git", "url": "http://github.com/grafana/grafana.git" diff --git a/pkg/tsdb/influxdb/influxdb.go b/pkg/tsdb/influxdb/influxdb.go index 61be1b08556..a64a6af2ee9 100644 --- a/pkg/tsdb/influxdb/influxdb.go +++ b/pkg/tsdb/influxdb/influxdb.go @@ -57,7 +57,7 @@ func (e *InfluxDBExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, } if setting.Env == setting.DEV { - glog.Debug("Influxdb query", "raw query", query) + glog.Debug("Influxdb query", "raw query", rawQuery) } req, err := e.createRequest(rawQuery) diff --git a/pkg/tsdb/influxdb/model_parser.go b/pkg/tsdb/influxdb/model_parser.go index cbd4b397d9d..2db7a78ed75 100644 --- a/pkg/tsdb/influxdb/model_parser.go +++ b/pkg/tsdb/influxdb/model_parser.go @@ -12,7 +12,6 @@ type InfluxdbQueryParser struct{} func (qp *InfluxdbQueryParser) Parse(model *simplejson.Json, dsInfo *tsdb.DataSourceInfo) (*Query, error) { policy := model.Get("policy").MustString("default") rawQuery := model.Get("query").MustString("") - interval := model.Get("interval").MustString("") alias := model.Get("alias").MustString("") measurement := model.Get("measurement").MustString("") @@ -37,7 +36,8 @@ func (qp *InfluxdbQueryParser) Parse(model *simplejson.Json, dsInfo *tsdb.DataSo return nil, err } - if interval == "" { + interval := model.Get("interval").MustString("") + if interval == "" && dsInfo.JsonData != nil { dsInterval := dsInfo.JsonData.Get("timeInterval").MustString("") if dsInterval != "" { interval = dsInterval diff --git a/pkg/tsdb/influxdb/query_builder.go b/pkg/tsdb/influxdb/query_builder.go index a4b122cbbd2..b783ff5c603 100644 --- a/pkg/tsdb/influxdb/query_builder.go +++ b/pkg/tsdb/influxdb/query_builder.go @@ -5,9 +5,15 @@ import ( "strconv" "strings" + "regexp" + "github.com/grafana/grafana/pkg/tsdb" ) +var ( + regexpOperatorPattern *regexp.Regexp = regexp.MustCompile(`^\/.*\/$`) +) + type QueryBuilder struct{} func (qb *QueryBuilder) Build(query *Query, queryContext *tsdb.QueryContext) (string, error) { @@ -43,18 +49,28 @@ func (qb *QueryBuilder) renderTags(query *Query) []string { str += " " } - value := tag.Value - nValue, err := strconv.ParseFloat(tag.Value, 64) - - if tag.Operator == "=~" || tag.Operator == "!~" { - value = fmt.Sprintf("%s", value) - } else if err == nil { - value = fmt.Sprintf("%v", nValue) - } else { - value = fmt.Sprintf("'%s'", value) + //If the operator is missing we fall back to sensible defaults + if tag.Operator == "" { + if regexpOperatorPattern.Match([]byte(tag.Value)) { + tag.Operator = "=~" + } else { + tag.Operator = "=" + } } - res = append(res, fmt.Sprintf(`%s"%s" %s %s`, str, tag.Key, tag.Operator, value)) + textValue := "" + numericValue, err := strconv.ParseFloat(tag.Value, 64) + + // quote value unless regex or number + if tag.Operator == "=~" || tag.Operator == "!~" { + textValue = tag.Value + } else if err == nil { + textValue = fmt.Sprintf("%v", numericValue) + } else { + textValue = fmt.Sprintf("'%s'", tag.Value) + } + + res = append(res, fmt.Sprintf(`%s"%s" %s %s`, str, tag.Key, tag.Operator, textValue)) } return res diff --git a/pkg/tsdb/influxdb/query_builder_test.go b/pkg/tsdb/influxdb/query_builder_test.go index d13fe3a3c6f..408db18e549 100644 --- a/pkg/tsdb/influxdb/query_builder_test.go +++ b/pkg/tsdb/influxdb/query_builder_test.go @@ -86,16 +86,34 @@ func TestInfluxdbQueryBuilder(t *testing.T) { So(rawQuery, ShouldEqual, `Raw query`) }) - Convey("can render regex tags", func() { - query := &Query{Tags: []*Tag{&Tag{Operator: "=~", Value: "value", Key: "key"}}} + 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`) + 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() { + query := &Query{Tags: []*Tag{&Tag{Operator: "=~", Value: `/value/`, Key: "key"}}} + + So(strings.Join(builder.renderTags(query), ""), ShouldEqual, `"key" =~ /value/`) }) 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" = 1`) + 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" = 10001.1`) }) Convey("can render string tags", func() { diff --git a/public/app/core/components/help/help.html b/public/app/core/components/help/help.html index 8482e9df7d1..3356f21d452 100644 --- a/public/app/core/components/help/help.html +++ b/public/app/core/components/help/help.html @@ -22,7 +22,7 @@

mod = - CTRL on windows, CMD key on Mac + CTRL on windows or linux and CMD key on Mac

diff --git a/public/app/core/time_series2.ts b/public/app/core/time_series2.ts index d672e0dd0dc..25a99a38ad8 100644 --- a/public/app/core/time_series2.ts +++ b/public/app/core/time_series2.ts @@ -102,6 +102,7 @@ export default class TimeSeries { this.stats.min = Number.MAX_VALUE; this.stats.avg = null; this.stats.current = null; + this.stats.timeStep = Number.MAX_VALUE; this.allIsNull = true; this.allIsZero = true; @@ -110,11 +111,22 @@ export default class TimeSeries { var currentTime; var currentValue; var nonNulls = 0; + var previousTime; for (var i = 0; i < this.datapoints.length; i++) { currentValue = this.datapoints[i][0]; 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 (ignoreNulls) { continue; } if (nullAsZero) { @@ -145,10 +157,6 @@ export default class TimeSeries { 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.min === Number.MAX_VALUE) { this.stats.min = null; } diff --git a/public/app/plugins/datasource/grafana/datasource.ts b/public/app/plugins/datasource/grafana/datasource.ts index 3ae030e4423..c1c0ce6388a 100644 --- a/public/app/plugins/datasource/grafana/datasource.ts +++ b/public/app/plugins/datasource/grafana/datasource.ts @@ -1,17 +1,39 @@ /// +import _ from 'lodash'; + class GrafanaDatasource { /** @ngInject */ constructor(private backendSrv) {} query(options) { - return this.backendSrv.get('/api/metrics/test', { - from: options.range.from.valueOf(), - to: options.range.to.valueOf(), - scenario: 'random_walk', - interval: options.intervalMs, - maxDataPoints: options.maxDataPoints + return this.backendSrv.post('/api/tsdb/query', { + from: options.range.from.valueOf().toString(), + to: options.range.to.valueOf().toString(), + queries: [ + { + "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}; }); } diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 92e5ce177e1..14e0d5c99c0 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -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 render_panel() { panelWidth = elem.width(); @@ -279,9 +297,7 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) { break; } default: { - if (data.length && data[0].stats.timeStep) { - options.series.bars.barWidth = data[0].stats.timeStep / 1.5; - } + options.series.bars.barWidth = getMinTimeStepOfSeries(data) / 1.5; addTimeAxis(options); break; } diff --git a/public/app/plugins/panel/graph/graph_tooltip.js b/public/app/plugins/panel/graph/graph_tooltip.js index 704eb9097ec..3aec815428f 100644 --- a/public/app/plugins/panel/graph/graph_tooltip.js +++ b/public/app/plugins/panel/graph/graph_tooltip.js @@ -2,7 +2,7 @@ define([ 'jquery', 'lodash' ], -function ($, _) { +function ($) { 'use strict'; function GraphTooltip(elem, dashboard, scope, getSeriesFn) { @@ -21,7 +21,10 @@ function ($, _) { var initial = last*ps; var len = series.datapoints.points.length; 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; } } @@ -51,23 +54,35 @@ function ($, _) { //now we know the current X (j) position for X and Y values var last_value = 0; //needed for stacked values + var minDistance, minTime; + for (i = 0; i < seriesList.length; i++) { series = seriesList[i]; 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 }); continue; } 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 }); continue; } 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]; + // 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 (panel.tooltip.value_type === 'individual') { value = series.data[hoverIndex][1]; @@ -89,6 +104,7 @@ function ($, _) { hoverIndex = this.findHoverIndexFromDataPoints(pos.x, series, hoverIndex); } + // Be sure we have a yaxis so that it does not brake series sorting yaxis = 0; if (series.yaxis) { yaxis = series.yaxis.n; @@ -106,8 +122,8 @@ function ($, _) { }); } - // Find point which closer to pointer - results.time = _.min(results, 'distance').time; + // Time of the point closer to pointer + results.time = minTime; return results; }; @@ -153,6 +169,8 @@ function ($, _) { seriesHtml = ''; + absoluteTime = dashboard.formatDate(seriesHoverInfo.time, tooltipFormat); + // Dynamically reorder the hovercard for the current time point if the // option is enabled, sort by yaxis by default. if (panel.tooltip.sort === 2) { @@ -169,8 +187,6 @@ function ($, _) { }); } - var distance, time; - for (i = 0; i < seriesHoverInfo.length; i++) { hoverInfo = seriesHoverInfo[i]; @@ -178,11 +194,6 @@ function ($, _) { continue; } - if (! distance || hoverInfo.distance < distance) { - distance = hoverInfo.distance; - time = hoverInfo.time; - } - var highlightClass = ''; if (item && i === item.seriesIndex) { highlightClass = 'graph-tooltip-list-item--highlight'; @@ -198,7 +209,6 @@ function ($, _) { plot.highlight(hoverInfo.index, hoverInfo.hoverIndex); } - absoluteTime = dashboard.formatDate(time, tooltipFormat); self.showTooltip(absoluteTime, seriesHtml, pos); } // single series tooltip diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index c4b1bd89044..e546a33c30b 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -66,7 +66,7 @@ class GraphCtrl extends MetricsPanelCtrl { // fill factor fill : 1, // line width in pixels - linewidth : 2, + linewidth : 1, // show hide points points : false, // point radius in pixels diff --git a/public/app/plugins/panel/graph/specs/graph_specs.ts b/public/app/plugins/panel/graph/specs/graph_specs.ts index 9f8d91ca9de..046409b77b2 100644 --- a/public/app/plugins/panel/graph/specs/graph_specs.ts +++ b/public/app/plugins/panel/graph/specs/graph_specs.ts @@ -135,7 +135,7 @@ describe('grafanaGraph', 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); }); }); diff --git a/public/sass/components/_view_states.scss b/public/sass/components/_view_states.scss index 42a97f5f0bd..8ab13c56776 100644 --- a/public/sass/components/_view_states.scss +++ b/public/sass/components/_view_states.scss @@ -59,34 +59,34 @@ .dashnav-action-icons, .dashnav-move-timeframe { opacity: 0; - transition: opacity 1.5s ease-in-out; + transition: all 1.5s ease-in-out 1s; } // navbar buttons .navbar-brand-btn, .navbar-inner { - border: none; + border-color: transparent; background: transparent; - transition: background 1.5s ease-in-out; + transition: all 1.5s ease-in-out 1s; .fa { opacity: 0; - transition: opacity 1.5s ease-in-out; + transition: all 1.5s ease-in-out 1s; } } .navbar-page-btn { - border: none; - transform: translate3d(-50px, 0, 0); + border-color: 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 { opacity: 0; - transition: opacity 1.5s ease-in-out; + transition: all 1.5s ease-in-out 1s; } } .gf-timepicker-nav-btn { transform: translate3d(40px, 0, 0); - transition: transform 1.5s ease-in-out; + transition: transform 1.5s ease-in-out 1s; } } diff --git a/public/sass/pages/_alerting.scss b/public/sass/pages/_alerting.scss index abe8c775415..b739485f170 100644 --- a/public/sass/pages/_alerting.scss +++ b/public/sass/pages/_alerting.scss @@ -50,7 +50,6 @@ .panel-alert-state { &--alerting { - background-color: mix($critical,$panel-bg, 3%); animation: alerting-panel 1.6s cubic-bezier(1,.1,.73,1) 0s infinite alternate; box-shadow: 0 0 10px rgba($critical,0.5); opacity: 1; diff --git a/public/vendor/flot/jquery.flot.js b/public/vendor/flot/jquery.flot.js index ec91314c49d..8d61b3e9b28 100644 --- a/public/vendor/flot/jquery.flot.js +++ b/public/vendor/flot/jquery.flot.js @@ -1201,24 +1201,21 @@ Licensed under the MIT license. points[k + m] = null; } } - else { - // a little bit of line specific stuff that - // perhaps shouldn't be here, but lacking - // better means... - if (insertSteps && k > 0 - && points[k - ps] != null - && points[k - ps] != points[k] - && points[k - ps + 1] != points[k + 1]) { - // copy the point to make room for a middle point - for (m = 0; m < ps; ++m) - points[k + ps + m] = points[k + m]; - // middle point has same y - points[k + 1] = points[k - ps + 1]; + if (insertSteps && k > 0 && (!nullify || points[k - ps] != null)) { + // copy the point to make room for a middle point + for (m = 0; m < ps; ++m) + points[k + ps + m] = points[k + m]; - // we've added a point, better reflect that - k += ps; - } + // middle point has same y + points[k + 1] = points[k - ps + 1] || 0; + + // if series has null values, let's give the last !null value a nice step + if(nullify) + points[k] = p[0]; + + // we've added a point, better reflect that + k += ps; } } } diff --git a/public/vendor/flot/jquery.flot.stack.js b/public/vendor/flot/jquery.flot.stack.js index ffb412ca2a0..5367ff30603 100644 --- a/public/vendor/flot/jquery.flot.stack.js +++ b/public/vendor/flot/jquery.flot.stack.js @@ -78,41 +78,46 @@ charts or filled areas). i = 0, j = 0, l, m; 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; + // newpoints will replace current series with + // as many points as different timestamps we have in the 2 (current & previous) series l = newpoints.length; + px = points[i + keyOffset]; + py = points[i + accumulateOffset]; + qx = otherpoints[j + keyOffset]; + qy = otherpoints[j + accumulateOffset]; + bottom = 0; - if (points[i] == null) { - // copy gaps + if (i < points.length && px == null) { + // let's ignore null points from current series, nothing to do with them + i += ps; + } + else if (j < otherpoints.length && qx == null) { + // let's ignore null points from previous series, nothing to do with them + j += otherps; + } + else if (i >= points.length) { + // 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) + newpoints.push(otherpoints[j + m]); + bottom = qy; + 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 if (j >= otherpoints.length) { - // for lines, we can't use the rest of the points - if (!withlines) { - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - } - i += ps; - } - else if (otherpoints[j] == null) { - // oops, got a gap - for (m = 0; m < ps; ++m) - newpoints.push(null); - fromgap = true; - j += otherps; - } else { - // cases where we actually got two points - px = points[i + keyOffset]; - py = points[i + accumulateOffset]; - qx = otherpoints[j + keyOffset]; - qy = otherpoints[j + accumulateOffset]; - bottom = 0; - + // next available points from current and previous series have the same timestamp if (px == qx) { + // so take the point from the current series and skip the previous' one for (m = 0; m < ps; ++m) newpoints.push(points[i + m]); @@ -122,55 +127,39 @@ charts or filled areas). i += ps; j += otherps; } + // next available point with the smallest timestamp is from the previous series else if (px > qx) { - // we got past point below, might need to - // insert interpolated extra point - if (withlines && i > 0 && points[i - ps] != null) { - intery = py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px); - newpoints.push(qx); - newpoints.push(intery + qy); - for (m = 2; m < ps; ++m) - newpoints.push(points[i + m]); - bottom = qy; - } + // so take the point from the previous series so that next series will correctly stack + for (m = 0; m < ps; ++m) + newpoints.push(otherpoints[j + m]); + + // we might be able to interpolate + if (i > 0 && points[i - ps] != null) + newpoints[l + accumulateOffset] += py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px); + + bottom = qy; j += otherps; } - else { // px < qx - if (fromgap && withlines) { - // if we come from a gap, we just skip this point - i += ps; - continue; - } - + // (px < qx) next available point with the smallest timestamp is from the current series + else { + // so of course let's take the point from the current series for (m = 0; m < ps; ++m) newpoints.push(points[i + m]); // we might be able to interpolate a point below, // 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); newpoints[l + accumulateOffset] += bottom; i += ps; } + } - fromgap = false; - - 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]; - } + if (l != newpoints.length && withbottom) + newpoints[l + 2] = bottom; } datapoints.points = newpoints;