From 2a93bed4537d5b93fc15856c8c678dcaa700d89f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 19 Jun 2017 19:22:44 -0400 Subject: [PATCH 01/64] ux: aligned tabbed view body with header title --- public/sass/components/_tabbed_view.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/sass/components/_tabbed_view.scss b/public/sass/components/_tabbed_view.scss index 751469f9463..6cc04f259d6 100644 --- a/public/sass/components/_tabbed_view.scss +++ b/public/sass/components/_tabbed_view.scss @@ -9,7 +9,6 @@ padding: 0; .tabbed-view-header { -/* padding: 0; */ background-color: $body-bg; padding: 1.5em 1rem 0 1rem; } @@ -48,7 +47,7 @@ } .tabbed-view-body { - padding: $spacer*2; + padding: $spacer*2 $spacer; min-height: 250px; } From 2479e51a6b7b3ea017bd63abbf48a909ddd86e0d Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 20 Jun 2017 02:02:05 +0200 Subject: [PATCH 02/64] mysql: Null value should not be considered as previous value fixes #8655 --- pkg/tsdb/mysql/mysql.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go index d4bd9dbacc1..d647aff64f9 100644 --- a/pkg/tsdb/mysql/mysql.go +++ b/pkg/tsdb/mysql/mysql.go @@ -307,6 +307,9 @@ func (s *stringStringScan) Update(rows *sql.Rows) error { return err } + s.time = null.FloatFromPtr(nil) + s.value = null.FloatFromPtr(nil) + for i := 0; i < s.columnCount; i++ { if rb, ok := s.rowPtrs[i].(*sql.RawBytes); ok { s.rowValues[i] = string(*rb) From bc6a57ce329d831c42b5d9acf80964e2e9a57163 Mon Sep 17 00:00:00 2001 From: Haidara Mohamed El Mouctar Date: Wed, 21 Jun 2017 16:41:56 +0200 Subject: [PATCH 03/64] [Docs] Add documentation for max_idle_conn and max_open_conn (#8675) --- docs/sources/installation/configuration.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 20f191b26e1..943916410b5 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -203,6 +203,12 @@ For MySQL, use either `true`, `false`, or `skip-verify`. (MySQL only) The common name field of the certificate used by the `mysql` server. Not necessary if `ssl_mode` is set to `skip-verify`. +### max_idle_conn +The maximum number of connections in the idle connection pool. + +### max_open_conn +The maximum number of open connections to the database. +
## [security] From b54b43a42e6faf51d84cff346035fefe2fe4f101 Mon Sep 17 00:00:00 2001 From: Ben Tranter Date: Thu, 22 Jun 2017 18:08:37 -0400 Subject: [PATCH 04/64] Add tests for diff formatters --- pkg/components/dashdiffs/formatter_test.go | 131 +++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 pkg/components/dashdiffs/formatter_test.go diff --git a/pkg/components/dashdiffs/formatter_test.go b/pkg/components/dashdiffs/formatter_test.go new file mode 100644 index 00000000000..472d998feb5 --- /dev/null +++ b/pkg/components/dashdiffs/formatter_test.go @@ -0,0 +1,131 @@ +package dashdiffs + +import ( + "testing" + + "github.com/grafana/grafana/pkg/components/simplejson" + . "github.com/smartystreets/goconvey/convey" +) + +func TestDiff(t *testing.T) { + // Sample json docs for tests only + const ( + leftJSON = `{ + "key": "value", + "object": { + "key": "value", + "anotherObject": { + "same": "this field is the same in rightJSON", + "change": "this field should change in rightJSON", + "delete": "this field doesn't appear in rightJSON" + } + }, + "array": [ + "same", + "change", + "delete" + ], + "embeddedArray": { + "array": [ + "same", + "change", + "delete" + ] + } + }` + + rightJSON = `{ + "key": "differentValue", + "object": { + "key": "value", + "newKey": "value", + "anotherObject": { + "same": "this field is the same in rightJSON", + "change": "this field should change in rightJSON", + "add": "this field is added" + } + }, + "array": [ + "same", + "changed!", + "add" + ], + "embeddedArray": { + "array": [ + "same", + "changed!", + "add" + ] + } + }` + ) + + Convey("Testing dashboard diffs", t, func() { + + // Compute the diff between the two JSON objects + baseData, err := simplejson.NewJson([]byte(leftJSON)) + So(err, ShouldBeNil) + + newData, err := simplejson.NewJson([]byte(rightJSON)) + So(err, ShouldBeNil) + + left, jsonDiff, err := getDiff(baseData, newData) + So(err, ShouldBeNil) + + Convey("The JSONFormatter should produce the expected JSON tokens", func() { + f := NewJSONFormatter(left) + _, err := f.Format(jsonDiff) + So(err, ShouldBeNil) + + // Total up the change types. If the number of different change + // types is correct, it means that the diff is producing correct + // output to the template rendered. + changeCounts := make(map[ChangeType]int) + for _, line := range f.Lines { + changeCounts[line.Change]++ + } + + // The expectedChangeCounts here were determined by manually + // looking at the JSON + expectedChangeCounts := map[ChangeType]int{ + ChangeNil: 12, + ChangeAdded: 2, + ChangeDeleted: 1, + ChangeOld: 5, + ChangeNew: 5, + ChangeUnchanged: 5, + } + So(changeCounts, ShouldResemble, expectedChangeCounts) + }) + + Convey("The BasicFormatter should produce the expected BasicBlocks", func() { + f := NewBasicFormatter(left) + _, err := f.Format(jsonDiff) + So(err, ShouldBeNil) + + bd := &BasicDiff{} + blocks := bd.Basic(f.jsonDiff.Lines) + + changeCounts := make(map[ChangeType]int) + for _, block := range blocks { + for _, change := range block.Changes { + changeCounts[change.Change]++ + } + + for _, summary := range block.Summaries { + changeCounts[summary.Change]++ + } + + changeCounts[block.Change]++ + } + + expectedChangeCounts := map[ChangeType]int{ + ChangeNil: 3, + ChangeAdded: 2, + ChangeDeleted: 1, + ChangeOld: 3, + } + So(changeCounts, ShouldResemble, expectedChangeCounts) + }) + }) +} From a3d22ae9c7e35cc332cfc02353f6b8595cf1869b Mon Sep 17 00:00:00 2001 From: Ben Tranter Date: Thu, 22 Jun 2017 18:23:31 -0400 Subject: [PATCH 05/64] Document logic behind basic diff --- pkg/components/dashdiffs/formatter_basic.go | 47 ++++++++++++++++++--- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/pkg/components/dashdiffs/formatter_basic.go b/pkg/components/dashdiffs/formatter_basic.go index 01c7757112d..5b1d5504bc6 100644 --- a/pkg/components/dashdiffs/formatter_basic.go +++ b/pkg/components/dashdiffs/formatter_basic.go @@ -71,6 +71,8 @@ func NewBasicFormatter(left interface{}) *BasicFormatter { } } +// Format takes the diff of two JSON documents, and returns the difference +// between them summarized in an HTML document. func (b *BasicFormatter) Format(d diff.Diff) ([]byte, error) { // calling jsonDiff.Format(d) populates the JSON diff's "Lines" value, // which we use to compute the basic dif @@ -90,23 +92,40 @@ func (b *BasicFormatter) Format(d diff.Diff) ([]byte, error) { return buf.Bytes(), nil } -// Basic is V2 of the basic diff +// Basic transforms a slice of JSONLines into a slice of BasicBlocks. func (b *BasicDiff) Basic(lines []*JSONLine) []*BasicBlock { // init an array you can append to for the basic "blocks" blocks := make([]*BasicBlock, 0) - // iterate through each line for _, line := range lines { - // TODO: this condition needs an explaination? what does it mean? + // In order to produce distinct "blocks" when rendering the basic diff, + // we need a way to distinguish between differnt sections of data. + // To do this, we consider the value(s) of each top-level JSON key to + // represent a distinct block for Grafana's JSON data structure, so + // we perform this check to see if we've entered a new "block". If we + // have, we simply append the existing block to the array of blocks. if b.LastIndent == 2 && line.Indent == 1 && line.Change == ChangeNil { if b.Block != nil { blocks = append(blocks, b.Block) } } + // Record the last indent level at each pass in case we need to + // check for a change in depth inside the JSON data structures. b.LastIndent = line.Indent // TODO: why special handling for indent 2? + // Here we + // If the line's indentation is at level 1, then we know it's a top + // level key in the JSON document. As mentioned earlier, we treat these + // specially as they indicate their values belong to distinct blocks. + // + // At level 1, we only record single-line changes, ie, the "added", + // "deleted", "old" or "new" cases, since we know those values aren't + // arrays or maps. We only handle these cases at level 2 or deeper, + // since for those we either output a "change" or "summary". This is + // done for formatting reasons only, so we have logical "blocks" to + // display. if line.Indent == 1 { switch line.Change { case ChangeNil: @@ -139,17 +158,31 @@ func (b *BasicDiff) Basic(lines []*JSONLine) []*BasicBlock { b.Block.New = line.Val b.Block.LineEnd = line.LineNum - // then write out the change + // For every "old" change there is a corresponding "new", which + // is why we wait until we detect the "new" change before + // appending the change. blocks = append(blocks, b.Block) default: // ok } } - // TODO: why special handling for indent > 2 ? - // Other Lines + // Here is where we handle changes for all types, appending each change + // to the current block based on the value. + // + // Values which only occupy a single line in JSON (like a string or + // int, for example) are treated as "Basic Changes" that we append to + // the current block as soon as they're detected. + // + // Values which occupy multiple lines (either slices or maps) are + // treated as "Basic Summaries". When we detect the "ChangeNil" type, + // we know we've encountered one of these types, so we record the + // starting position as well the type of the change, and stop + // performing comparisons until we find the end of that change. Upon + // finding the change, we append it to the current block, and begin + // performing comparisons again. if line.Indent > 1 { - // Ensure single line change + // Ensure a single line change if line.Key != "" && line.Val != nil && !b.writing { switch line.Change { case ChangeAdded, ChangeDeleted: From 86a73c359be48ac24470add660de3305f82375ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 23 Jun 2017 11:32:04 -0400 Subject: [PATCH 06/64] fix: data source selector did not show, fixes #8692 --- public/app/core/components/form_dropdown/form_dropdown.ts | 1 + public/app/features/panel/metrics_tab.ts | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/public/app/core/components/form_dropdown/form_dropdown.ts b/public/app/core/components/form_dropdown/form_dropdown.ts index d8212e2f55c..1c611e94397 100644 --- a/public/app/core/components/form_dropdown/form_dropdown.ts +++ b/public/app/core/components/form_dropdown/form_dropdown.ts @@ -30,6 +30,7 @@ export class FormDropdownCtrl { optionCache: any; lookupText: boolean; + /** @ngInject **/ constructor(private $scope, $element, private $sce, private templateSrv, private $q) { this.inputElement = $element.find('input').first(); this.linkElement = $element.find('a').first(); diff --git a/public/app/features/panel/metrics_tab.ts b/public/app/features/panel/metrics_tab.ts index c03c4d83bc2..e1825173172 100644 --- a/public/app/features/panel/metrics_tab.ts +++ b/public/app/features/panel/metrics_tab.ts @@ -1,7 +1,6 @@ /// import _ from 'lodash'; -//import {coreModule} from 'app/core/core'; import {DashboardModel} from '../dashboard/model'; export class MetricsTabCtrl { @@ -32,7 +31,6 @@ export class MetricsTabCtrl { } this.addQueryDropdown = {text: 'Add Query', value: null, fake: true}; - // update next ref id this.panelCtrl.nextRefId = this.dashboard.getNextQueryLetter(this.panel); } @@ -80,4 +78,3 @@ export function metricsTabDirective() { }; } -//coreModule.directive('metricsTab', metricsTabDirective); From be29357d22bcefd440569e69dbadc07cbadbed8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 23 Jun 2017 11:48:58 -0400 Subject: [PATCH 07/64] fix: added missing url route for annotation state history delete, fixes #8660 --- pkg/api/api.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/api/api.go b/pkg/api/api.go index c3a3728338d..ae9b8e7803f 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -285,6 +285,7 @@ func (hs *HttpServer) registerRoutes() { }, reqEditorRole) r.Get("/annotations", wrap(GetAnnotations)) + r.Post("/annotations/mass-delete", reqOrgAdmin, bind(dtos.DeleteAnnotationsCmd{}), wrap(DeleteAnnotations)) r.Group("/annotations", func() { r.Post("/", bind(dtos.PostAnnotationsCmd{}), wrap(PostAnnotation)) From 73fcc919cdadb8efb9a9802a6f629ddf424a6514 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 23 Jun 2017 11:49:32 -0400 Subject: [PATCH 08/64] change: made dashboard save message optional --- public/app/features/dashboard/save_modal.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/app/features/dashboard/save_modal.ts b/public/app/features/dashboard/save_modal.ts index 01a42655a5f..36bae222243 100644 --- a/public/app/features/dashboard/save_modal.ts +++ b/public/app/features/dashboard/save_modal.ts @@ -29,8 +29,7 @@ const template = ` ng-model="ctrl.message" ng-model-options="{allowInvalid: true}" ng-maxlength="this.max" - autocomplete="off" - required /> + autocomplete="off" /> {{ctrl.message.length || 0}} From 60da730c9503344b17260d59311262489e002e6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 23 Jun 2017 12:55:40 -0400 Subject: [PATCH 09/64] mysql: fix for TIME columns, fixes #8534 --- pkg/tsdb/mysql/mysql.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go index d647aff64f9..367124a46b8 100644 --- a/pkg/tsdb/mysql/mysql.go +++ b/pkg/tsdb/mysql/mysql.go @@ -183,6 +183,7 @@ func (e MysqlExecutor) getTypedRowData(types []*sql.ColumnType, rows *core.Rows) values := make([]interface{}, len(types)) for i, stype := range types { + e.log.Info("type", "type", stype) switch stype.DatabaseTypeName() { case mysql.FieldTypeNameTiny: values[i] = new(int8) @@ -209,7 +210,7 @@ func (e MysqlExecutor) getTypedRowData(types []*sql.ColumnType, rows *core.Rows) case mysql.FieldTypeNameDateTime: values[i] = new(time.Time) case mysql.FieldTypeNameTime: - values[i] = new(time.Duration) + values[i] = new(string) case mysql.FieldTypeNameYear: values[i] = new(int16) case mysql.FieldTypeNameNULL: From 5aac2d2078cacc38d2493c0ac1b3659251cb4675 Mon Sep 17 00:00:00 2001 From: Denis Doria Date: Sun, 25 Jun 2017 14:23:03 +0200 Subject: [PATCH 10/64] Include user Id on the lookup api (#8698) Implements feature request #8682 --- pkg/api/user.go | 1 + pkg/models/user.go | 1 + 2 files changed, 2 insertions(+) diff --git a/pkg/api/user.go b/pkg/api/user.go index 39e7fce1462..7e8bad91ab0 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -42,6 +42,7 @@ func GetUserByLoginOrEmail(c *middleware.Context) Response { } user := query.Result result := m.UserProfileDTO{ + Id: user.Id, Name: user.Name, Email: user.Email, Login: user.Login, diff --git a/pkg/models/user.go b/pkg/models/user.go index bdf81056232..e3981dc3880 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -163,6 +163,7 @@ type SignedInUser struct { } type UserProfileDTO struct { + Id int64 `json:"id"` Email string `json:"email"` Name string `json:"name"` Login string `json:"login"` From e8d01218d8164157d8e6bbb5ab8e8b4cb5a3ce7d Mon Sep 17 00:00:00 2001 From: Denis Doria Date: Sun, 25 Jun 2017 14:23:37 +0200 Subject: [PATCH 11/64] Fix label showing up when Axes are configured to not be displayed (#8697) This should close the issue #8695 --- public/app/plugins/panel/graph/graph.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 408d89854cb..16d259c210e 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -145,14 +145,14 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv, popoverSrv) { } // add left axis labels - if (panel.yaxes[0].label) { + if (panel.yaxes[0].label && panel.yaxes[0].show) { var yaxisLabel = $("
") .text(panel.yaxes[0].label) .appendTo(elem); } // add right axis labels - if (panel.yaxes[1].label) { + if (panel.yaxes[1].label && panel.yaxes[1].show) { var rightLabel = $("
") .text(panel.yaxes[1].label) .appendTo(elem); From 8c8b1dde8a6a44329806e48f8bb9ae242be58ad8 Mon Sep 17 00:00:00 2001 From: Louis Law Date: Sun, 25 Jun 2017 23:34:23 +0800 Subject: [PATCH 12/64] [fix] fix minor issue in gitignore file. (#8694) --- .gitignore | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitignore b/.gitignore index 1ab7068c96a..5d3c506ca7a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,10 +12,8 @@ vendor/phantomjs/phantomjs docs/AWS_S3_BUCKET docs/GIT_BRANCH -docs/VERSION docs/GITCOMMIT docs/changed-files -docs/changed-files # locally required config files public/css/*.min.css From 10127e8ac9ce1a2da9aaab36daed36d7018f04de Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 26 Jun 2017 17:42:50 +0200 Subject: [PATCH 13/64] docs: small updates --- docs/sources/features/datasources/influxdb.md | 4 ++-- docs/sources/installation/debian.md | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/sources/features/datasources/influxdb.md b/docs/sources/features/datasources/influxdb.md index 2837363e145..1f6c36a6bb3 100644 --- a/docs/sources/features/datasources/influxdb.md +++ b/docs/sources/features/datasources/influxdb.md @@ -88,8 +88,8 @@ You can switch to raw query mode by clicking hamburger icon and then `Switch edi - $m = replaced with measurement name - $measurement = replaced with measurement name - $col = replaced with column name -- $tag_hostname = replaced with the value of the hostname tag -- You can also use [[tag_hostname]] pattern replacement syntax +- $tag_hostname = replaced with the value of the hostname tag. Use your tag instead of hostname and the tag must be used to group by in the query when used in the ALIAS BY field. +- You can also use [[tag_hostname]] pattern replacement syntax. For example, in the ALIAS BY field using this text `Host: [[tag_hostname]]` would substitute in the `hostname` tag value for each legend value and an example legend value would be: `Host: server1`. ### Table query / raw data diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index b83ebdc4a12..5fde442afbf 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -81,6 +81,7 @@ sudo apt-get install -y apt-transport-https - Installs systemd service (if systemd is available) name `grafana-server.service` - The default configuration sets the log file at `/var/log/grafana/grafana.log` - The default configuration specifies an sqlite3 db at `/var/lib/grafana/grafana.db` +- Installs HTML/JS/CSS and other Grafana files at `/usr/share/grafana` ## Start the server (init.d service) From 1deeef9e91a4c34087afd177a1763401ad33e159 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 27 Jun 2017 13:38:49 +0300 Subject: [PATCH 14/64] Table panel: add option for preserving text formatting (#8708) * table_panel: add option for preserving text formatting * table_panel: fix undefined style error * table_panel: fix class adding (add space before 'class') * table_panel: aligin Type options labels --- public/app/plugins/panel/table/column_options.html | 13 ++++++++----- public/app/plugins/panel/table/renderer.ts | 8 +++++++- public/sass/components/_panel_table.scss | 4 ++++ 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/panel/table/column_options.html b/public/app/plugins/panel/table/column_options.html index 35fa4ac2d8b..db4758ab8f3 100644 --- a/public/app/plugins/panel/table/column_options.html +++ b/public/app/plugins/panel/table/column_options.html @@ -33,27 +33,30 @@
Type
- +
- +
- + +
+
+
- +
- +
diff --git a/public/app/plugins/panel/table/renderer.ts b/public/app/plugins/panel/table/renderer.ts index ae56e8dff12..dc031df8d1e 100644 --- a/public/app/plugins/panel/table/renderer.ts +++ b/public/app/plugins/panel/table/renderer.ts @@ -130,6 +130,7 @@ export class TableRenderer { renderCell(columnIndex, value, addWidthHack = false) { value = this.formatColumnValue(columnIndex, value); var style = ''; + var cellClass = ''; if (this.colorState.cell) { style = ' style="background-color:' + this.colorState.cell + ';color: white"'; this.colorState.cell = null; @@ -153,7 +154,12 @@ export class TableRenderer { this.table.columns[columnIndex].hidden = false; } - return '' + value + widthHack + ''; + var columnStyle = this.table.columns[columnIndex].style; + if (columnStyle && columnStyle.preserveFormat) { + cellClass = ' class="table-panel-cell-pre" '; + } + + return '' + value + widthHack + ''; } render(page) { diff --git a/public/sass/components/_panel_table.scss b/public/sass/components/_panel_table.scss index ec0c73e2ec7..17dbc910b2d 100644 --- a/public/sass/components/_panel_table.scss +++ b/public/sass/components/_panel_table.scss @@ -72,6 +72,10 @@ &:last-child { border-right: none; } + + &.table-panel-cell-pre { + white-space: pre; + } } } From 53ea9cfbcf281255585e7654666e0f682480095b Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 27 Jun 2017 16:59:40 +0200 Subject: [PATCH 15/64] docs: update ha_setup with alerting deduping --- docs/sources/tutorials/ha_setup.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/sources/tutorials/ha_setup.md b/docs/sources/tutorials/ha_setup.md index 998b382a43f..9dd8aac4618 100644 --- a/docs/sources/tutorials/ha_setup.md +++ b/docs/sources/tutorials/ha_setup.md @@ -35,6 +35,4 @@ But we suggest that you store the session in redis/memcache since it makes it ea ## Alerting -Currently alerting does not support high availability. But this is something that we will be working on in the future. - - +Currently alerting supports a limited form of high availability. Since v4.2.0 of Grafana, alert notifications are deduped when running multiple servers. This means all alerts are executed on every server but no duplicate alert notifications are sent due to the deduping logic. Proper load balancing of alerts will be introduced in the future. From 8e5672aee6a710433a68bfeb2f7913a5a1d236e8 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 26 Jun 2017 18:13:32 +0300 Subject: [PATCH 16/64] heatmap: fix Y axis value rounding with linear scale --- public/app/plugins/panel/heatmap/rendering.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index 085d9bd2673..153375dfa41 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -131,7 +131,9 @@ export default function link(scope, elem, attrs, ctrl) { tick_interval = tickStep(y_min, y_max, ticks); ticks = Math.ceil((y_max - y_min) / tick_interval); + let decimalsAuto = getPrecision(tick_interval); let decimals = panel.yAxis.decimals === null ? getPrecision(tick_interval) : panel.yAxis.decimals; + let scaledDecimals = getScaledDecimals(decimals, tick_interval); // Set default Y min and max if no data if (_.isEmpty(data.buckets)) { @@ -153,7 +155,7 @@ export default function link(scope, elem, attrs, ctrl) { let yAxis = d3.axisLeft(yScale) .ticks(ticks) - .tickFormat(tickValueFormatter(decimals)) + .tickFormat(tickValueFormatter(decimals, scaledDecimals)) .tickSizeInner(0 - width) .tickSizeOuter(0) .tickPadding(Y_AXIS_TICK_PADDING); @@ -293,10 +295,14 @@ export default function link(scope, elem, attrs, ctrl) { return tickValues; } - function tickValueFormatter(decimals) { + function getScaledDecimals(decimals, tick_size) { + return decimals - Math.floor(Math.log(tick_size) / Math.LN10); + } + + function tickValueFormatter(decimals, scaledDecimals = null) { let format = panel.yAxis.format; return function(value) { - return kbn.valueFormats[format](value, decimals); + return kbn.valueFormats[format](value, decimals, scaledDecimals); }; } From b63d2b3279256f009bbb7d263a7e15ea4829f295 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 27 Jun 2017 12:26:41 +0300 Subject: [PATCH 17/64] heatmap: fix Y axis decimals with log scale --- public/app/plugins/panel/heatmap/rendering.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index 153375dfa41..54dbb812a95 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -132,7 +132,7 @@ export default function link(scope, elem, attrs, ctrl) { ticks = Math.ceil((y_max - y_min) / tick_interval); let decimalsAuto = getPrecision(tick_interval); - let decimals = panel.yAxis.decimals === null ? getPrecision(tick_interval) : panel.yAxis.decimals; + let decimals = panel.yAxis.decimals === null ? decimalsAuto : panel.yAxis.decimals; let scaledDecimals = getScaledDecimals(decimals, tick_interval); // Set default Y min and max if no data @@ -215,7 +215,10 @@ export default function link(scope, elem, attrs, ctrl) { let domain = yScale.domain(); let tick_values = logScaleTickValues(domain, log_base); - let decimals = panel.yAxis.decimals; + + let decimalsAuto = getPrecision(y_min); + let decimals = panel.yAxis.decimals || decimalsAuto; + let scaledDecimals = decimals - 2; data.yAxis = { min: y_min, @@ -225,7 +228,7 @@ export default function link(scope, elem, attrs, ctrl) { let yAxis = d3.axisLeft(yScale) .tickValues(tick_values) - .tickFormat(tickValueFormatter(decimals)) + .tickFormat(tickValueFormatter(decimals, scaledDecimals)) .tickSizeInner(0 - width) .tickSizeOuter(0) .tickPadding(Y_AXIS_TICK_PADDING); From 7c840cdf380232ea7196ca322aa38ef8ec8861bb Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 27 Jun 2017 13:13:50 +0300 Subject: [PATCH 18/64] heatmap: fix tooltip decimals --- public/app/core/utils/ticks.ts | 4 ++++ public/app/plugins/panel/heatmap/heatmap_ctrl.ts | 2 ++ public/app/plugins/panel/heatmap/heatmap_tooltip.ts | 12 ++++++++---- public/app/plugins/panel/heatmap/rendering.ts | 11 ++++++----- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/public/app/core/utils/ticks.ts b/public/app/core/utils/ticks.ts index 7e7abbcd8f0..fa68b04d614 100644 --- a/public/app/core/utils/ticks.ts +++ b/public/app/core/utils/ticks.ts @@ -25,3 +25,7 @@ export function tickStep(start: number, stop: number, count: number): number { return stop < start ? -step1 : step1; } + +export function getScaledDecimals(decimals, tick_size) { + return decimals - Math.floor(Math.log(tick_size) / Math.LN10); +} diff --git a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts index 9d770f21e95..37aa9410ea8 100644 --- a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts +++ b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts @@ -95,6 +95,8 @@ export class HeatmapCtrl extends MetricsPanelCtrl { series: any; timeSrv: any; dataWarning: any; + decimals: number; + scaledDecimals: number; /** @ngInject */ constructor($scope, $injector, private $rootScope, timeSrv) { diff --git a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts index 3af78156536..097d9f5897d 100644 --- a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts +++ b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts @@ -15,6 +15,7 @@ export class HeatmapTooltip { tooltip: any; scope: any; dashboard: any; + panelCtrl: any; panel: any; heatmapPanel: any; mouseOverBucket: boolean; @@ -23,6 +24,7 @@ export class HeatmapTooltip { constructor(elem, scope) { this.scope = scope; this.dashboard = scope.ctrl.dashboard; + this.panelCtrl = scope.ctrl; this.panel = scope.ctrl.panel; this.heatmapPanel = elem; this.mouseOverBucket = false; @@ -85,8 +87,10 @@ export class HeatmapTooltip { let tooltipTimeFormat = 'YYYY-MM-DD HH:mm:ss'; let time = this.dashboard.formatDate(xData.x, tooltipTimeFormat); - let decimals = this.panel.tooltipDecimals || 5; - let valueFormatter = this.valueFormatter(decimals); + + let decimals = this.panel.tooltipDecimals || this.panelCtrl.decimals; + let scaledDecimals = decimals - 2; + let valueFormatter = this.valueFormatter(decimals, scaledDecimals); let tooltipHtml = `
${time}
`; @@ -220,13 +224,13 @@ export class HeatmapTooltip { .style("top", top + "px"); } - valueFormatter(decimals) { + valueFormatter(decimals, scaledDecimals = null) { let format = this.panel.yAxis.format; return function(value) { if (_.isInteger(value)) { decimals = 0; } - return kbn.valueFormats[format](value, decimals); + return kbn.valueFormats[format](value, decimals, scaledDecimals); }; } } diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index 54dbb812a95..c713d254021 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -5,7 +5,7 @@ import $ from 'jquery'; import moment from 'moment'; import kbn from 'app/core/utils/kbn'; import {appEvents, contextSrv} from 'app/core/core'; -import {tickStep} from 'app/core/utils/ticks'; +import {tickStep, getScaledDecimals} from 'app/core/utils/ticks'; import d3 from 'd3'; import {HeatmapTooltip} from './heatmap_tooltip'; import {convertToCards, mergeZeroBuckets} from './heatmap_data_converter'; @@ -134,6 +134,8 @@ export default function link(scope, elem, attrs, ctrl) { let decimalsAuto = getPrecision(tick_interval); let decimals = panel.yAxis.decimals === null ? decimalsAuto : panel.yAxis.decimals; let scaledDecimals = getScaledDecimals(decimals, tick_interval); + ctrl.decimals = decimals; + ctrl.scaledDecimals = scaledDecimals; // Set default Y min and max if no data if (_.isEmpty(data.buckets)) { @@ -218,7 +220,10 @@ export default function link(scope, elem, attrs, ctrl) { let decimalsAuto = getPrecision(y_min); let decimals = panel.yAxis.decimals || decimalsAuto; + // TODO: calculate scaledDecimals for log scales using tick size (as in jquery.flot.js) let scaledDecimals = decimals - 2; + ctrl.decimals = decimals; + ctrl.scaledDecimals = scaledDecimals; data.yAxis = { min: y_min, @@ -298,10 +303,6 @@ export default function link(scope, elem, attrs, ctrl) { return tickValues; } - function getScaledDecimals(decimals, tick_size) { - return decimals - Math.floor(Math.log(tick_size) / Math.LN10); - } - function tickValueFormatter(decimals, scaledDecimals = null) { let format = panel.yAxis.format; return function(value) { From c12a7d7f5980b4256b48244389a5ef9c40fa504e Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 27 Jun 2017 13:39:20 +0300 Subject: [PATCH 19/64] heatmap: adjust tests for fixed decimals calc --- public/app/plugins/panel/heatmap/specs/renderer_specs.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/panel/heatmap/specs/renderer_specs.ts b/public/app/plugins/panel/heatmap/specs/renderer_specs.ts index 3bb3c04d122..9ca7297e9b6 100644 --- a/public/app/plugins/panel/heatmap/specs/renderer_specs.ts +++ b/public/app/plugins/panel/heatmap/specs/renderer_specs.ts @@ -204,7 +204,7 @@ describe('grafanaHeatmap', function () { it('should draw correct Y axis', function () { var yTicks = getTicks(ctx.element, ".axis-y"); - expect(yTicks).to.eql(['1', '32', '1 K']); + expect(yTicks).to.eql(['1', '32', '1.0 K']); }); }); @@ -221,7 +221,7 @@ describe('grafanaHeatmap', function () { it('should draw correct Y axis', function () { var yTicks = getTicks(ctx.element, ".axis-y"); - expect(yTicks).to.eql(['1', '1 K', '1 Mil']); + expect(yTicks).to.eql(['1', '1 K', '1.0 Mil']); }); }); @@ -247,7 +247,7 @@ describe('grafanaHeatmap', function () { it('should draw correct Y axis', function () { var yTicks = getTicks(ctx.element, ".axis-y"); - expect(yTicks).to.eql(['0 ns', '17 min', '33 min', '50 min', '1 hour']); + expect(yTicks).to.eql(['0 ns', '17 min', '33 min', '50 min', '1.11 hour']); }); }); From 12644372c4b68b6ba88be51f288f6a0cea55adaa Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 27 Jun 2017 13:59:54 +0300 Subject: [PATCH 20/64] heatmap: fix scaledDecimals calculation (use the same method as in flot.js) --- public/app/core/utils/ticks.ts | 36 +++++++++++++++++++ .../plugins/panel/heatmap/heatmap_tooltip.ts | 2 +- public/app/plugins/panel/heatmap/rendering.ts | 12 ++++--- 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/public/app/core/utils/ticks.ts b/public/app/core/utils/ticks.ts index fa68b04d614..b033e9247a1 100644 --- a/public/app/core/utils/ticks.ts +++ b/public/app/core/utils/ticks.ts @@ -29,3 +29,39 @@ export function tickStep(start: number, stop: number, count: number): number { export function getScaledDecimals(decimals, tick_size) { return decimals - Math.floor(Math.log(tick_size) / Math.LN10); } + +/** + * Calculate tick size based on min and max values, number of ticks and precision. + * @param min Axis minimum + * @param max Axis maximum + * @param noTicks Number of ticks + * @param tickDecimals Tick decimal precision + */ +export function getFlotTickSize(min: number, max: number, noTicks: number, tickDecimals: number) { + var delta = (max - min) / noTicks, + dec = -Math.floor(Math.log(delta) / Math.LN10), + maxDec = tickDecimals; + + var magn = Math.pow(10, -dec), + norm = delta / magn, // norm is between 1.0 and 10.0 + size; + + if (norm < 1.5) { + size = 1; + } else if (norm < 3) { + size = 2; + // special case for 2.5, requires an extra decimal + if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) { + size = 2.5; + ++dec; + } + } else if (norm < 7.5) { + size = 5; + } else { + size = 10; + } + + size *= magn; + + return size; +} diff --git a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts index 097d9f5897d..d455d89d3b2 100644 --- a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts +++ b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts @@ -89,7 +89,7 @@ export class HeatmapTooltip { let time = this.dashboard.formatDate(xData.x, tooltipTimeFormat); let decimals = this.panel.tooltipDecimals || this.panelCtrl.decimals; - let scaledDecimals = decimals - 2; + let scaledDecimals = this.panel.tooltipDecimals ? decimals - 2 : this.panelCtrl.scaledDecimals; let valueFormatter = this.valueFormatter(decimals, scaledDecimals); let tooltipHtml = `
${time}
diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index c713d254021..94903cfcb81 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -5,7 +5,7 @@ import $ from 'jquery'; import moment from 'moment'; import kbn from 'app/core/utils/kbn'; import {appEvents, contextSrv} from 'app/core/core'; -import {tickStep, getScaledDecimals} from 'app/core/utils/ticks'; +import {tickStep, getScaledDecimals, getFlotTickSize} from 'app/core/utils/ticks'; import d3 from 'd3'; import {HeatmapTooltip} from './heatmap_tooltip'; import {convertToCards, mergeZeroBuckets} from './heatmap_data_converter'; @@ -133,7 +133,9 @@ export default function link(scope, elem, attrs, ctrl) { let decimalsAuto = getPrecision(tick_interval); let decimals = panel.yAxis.decimals === null ? decimalsAuto : panel.yAxis.decimals; - let scaledDecimals = getScaledDecimals(decimals, tick_interval); + // Calculate scaledDecimals for log scales using tick size (as in jquery.flot.js) + let flot_tick_size = getFlotTickSize(y_min, y_max, ticks, decimalsAuto); + let scaledDecimals = getScaledDecimals(decimals, flot_tick_size); ctrl.decimals = decimals; ctrl.scaledDecimals = scaledDecimals; @@ -220,8 +222,10 @@ export default function link(scope, elem, attrs, ctrl) { let decimalsAuto = getPrecision(y_min); let decimals = panel.yAxis.decimals || decimalsAuto; - // TODO: calculate scaledDecimals for log scales using tick size (as in jquery.flot.js) - let scaledDecimals = decimals - 2; + + // Calculate scaledDecimals for log scales using tick size (as in jquery.flot.js) + let flot_tick_size = getFlotTickSize(y_min, y_max, tick_values.length, decimalsAuto); + let scaledDecimals = getScaledDecimals(decimals, flot_tick_size); ctrl.decimals = decimals; ctrl.scaledDecimals = scaledDecimals; From 83fbace6b9e122216a190c48400b513c434f3795 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 27 Jun 2017 19:25:54 +0300 Subject: [PATCH 21/64] heatmap: fix tooltip decimals calculation --- .../plugins/panel/heatmap/heatmap_tooltip.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts index d455d89d3b2..531ed8ca5b2 100644 --- a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts +++ b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts @@ -88,9 +88,17 @@ export class HeatmapTooltip { let tooltipTimeFormat = 'YYYY-MM-DD HH:mm:ss'; let time = this.dashboard.formatDate(xData.x, tooltipTimeFormat); - let decimals = this.panel.tooltipDecimals || this.panelCtrl.decimals; - let scaledDecimals = this.panel.tooltipDecimals ? decimals - 2 : this.panelCtrl.scaledDecimals; - let valueFormatter = this.valueFormatter(decimals, scaledDecimals); + // Decimals override. Code from panel/graph/graph.ts + let valueFormatter; + if (_.isNumber(this.panel.tooltipDecimals)) { + valueFormatter = this.valueFormatter(this.panel.tooltipDecimals, null); + } else { + // auto decimals + // legend and tooltip gets one more decimal precision + // than graph legend ticks + let decimals = (this.panelCtrl.decimals || -1) + 1; + valueFormatter = this.valueFormatter(this.panel.tooltipDecimals, this.panelCtrl.scaledDecimals + 2); + } let tooltipHtml = `
${time}
`; @@ -227,9 +235,6 @@ export class HeatmapTooltip { valueFormatter(decimals, scaledDecimals = null) { let format = this.panel.yAxis.format; return function(value) { - if (_.isInteger(value)) { - decimals = 0; - } return kbn.valueFormats[format](value, decimals, scaledDecimals); }; } From b674b9dba202f50a0f5dd23cdda098633d25b054 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 27 Jun 2017 22:23:22 +0200 Subject: [PATCH 22/64] heatmap: small fix for tooltip auto decimals Closes #8717 --- public/app/plugins/panel/heatmap/heatmap_tooltip.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts index 531ed8ca5b2..5ec0a8fa308 100644 --- a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts +++ b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts @@ -97,7 +97,7 @@ export class HeatmapTooltip { // legend and tooltip gets one more decimal precision // than graph legend ticks let decimals = (this.panelCtrl.decimals || -1) + 1; - valueFormatter = this.valueFormatter(this.panel.tooltipDecimals, this.panelCtrl.scaledDecimals + 2); + valueFormatter = this.valueFormatter(decimals, this.panelCtrl.scaledDecimals + 2); } let tooltipHtml = `
${time}
From 1a61d2814cc81801638be25c79b36e3dd8eb2160 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 19 Jun 2017 15:33:31 +0200 Subject: [PATCH 23/64] docs: updates to build from source --- docs/sources/project/building_from_source.md | 38 +++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/docs/sources/project/building_from_source.md b/docs/sources/project/building_from_source.md index 3d6673d2fb0..5056f1bd7b1 100644 --- a/docs/sources/project/building_from_source.md +++ b/docs/sources/project/building_from_source.md @@ -15,14 +15,21 @@ dev environment. Grafana ships with its own required backend server; also comple - [Go 1.8.1](https://golang.org/dl/) - [NodeJS LTS](https://nodejs.org/download/) +- [Git](https://git-scm.com/downloads) ## Get Code -Create a directory for the project and set your path accordingly. Then download and install Grafana into your $GOPATH directory +Create a directory for the project and set your path accordingly (or use the [default Go workspace directory](https://golang.org/doc/code.html#GOPATH)). Then download and install Grafana into your $GOPATH directory: + ``` export GOPATH=`pwd` go get github.com/grafana/grafana ``` +On Windows use setx instead of export and then restart your command prompt: +``` +setx GOPATH %cd% +``` + You may see an error such as: `package github.com/grafana/grafana: no buildable Go source files`. This is just a warning, and you can proceed with the directions. ## Building the backend @@ -36,6 +43,12 @@ go run build.go build # (or 'go build ./pkg/cmd/grafana-server') The Grafana backend includes Sqlite3 which requires GCC to compile. So in order to compile Grafana on windows you need to install GCC. We recommend [TDM-GCC](http://tdm-gcc.tdragon.net/download). +[node-gyp](https://github.com/nodejs/node-gyp#installation) is the Node.js native addon build tool and it requires extra dependencies to be installed on Windows. In a command prompt which is run as administrator, run: + +``` +npm --add-python-to-path='true' --debug install --global windows-build-tools +``` + ## Build the Front-end Assets To build less to css for the frontend you will need a recent version of node (v0.12.0), @@ -55,6 +68,8 @@ go get github.com/Unknwon/bra bra run ``` +If the `bra run` command does not work, make sure that the bin directory in your Go workspace directory is in the path. $GOPATH/bin (or %GOPATH%\bin in Windows) is in your path. + ## Running Grafana Locally You can run a local instance of Grafana by running: ``` @@ -94,3 +109,24 @@ Learn more about Grafana config options in the [Configuration section](/installa ## Create a pull requests Please contribute to the Grafana project and submit a pull request! Build new features, write or update documentation, fix bugs and generally make Grafana even more awesome. + +## Troubleshooting + +**Problem**: PhantomJS or node-sass errors when running grunt + +**Solution**: delete the node_modules directory. Install [node-gyp](https://github.com/nodejs/node-gyp#installation) properly for your platform. Then run `yarn install --pure-lockfile` again. +

+ +**Problem**: When running `bra run` for the first time you get an error that it is not a recognized command. + +**Solution**: Add the bin directory in your Go workspace directory to the path. Per default this is `$HOME/go/bin` on Linux and `%USERPROFILE%\go\bin` on Windows or `$GOPATH/bin` (`%GOPATH%\bin` on Windows) if you have set your own workspace directory. +

+ +**Problem**: When executing a `go get` command on Windows and you get an error about the git repository not existing. + +**Solution**: `go get` requires Git. If you run `go get` without Git then it will create an empty directory in your Go workspace for the library you are trying to get. Even after installing Git, you will get a similar error. To fix this, delete the empty directory (for example: if you tried to run `go get github.com/Unknwon/bra` then delete `%USERPROFILE%\go\src\github.com\Unknwon\bra`) and run the `go get` command again. +

+ +**Problem**: On Windows, getting errors about a tool not being installed even though you just installed that tool. + +**Solution**: It is usually because it got added to the path and you have to restart your command prompt to use it. From 8973b48f96d7bf67178ff1aeb06e653fee61897e Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 19 Jun 2017 15:36:08 +0200 Subject: [PATCH 24/64] setting: add tests for windows --- .gitignore | 1 + pkg/setting/setting_test.go | 88 ++++++++++++++++++------- tests/config-files/override_windows.ini | 3 + 3 files changed, 68 insertions(+), 24 deletions(-) create mode 100644 tests/config-files/override_windows.ini diff --git a/.gitignore b/.gitignore index 5d3c506ca7a..08cfb7a2931 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ awsconfig /public/vendor/npm /tmp vendor/phantomjs/phantomjs +vendor/phantomjs/phantomjs.exe docs/AWS_S3_BUCKET docs/GIT_BRANCH diff --git a/pkg/setting/setting_test.go b/pkg/setting/setting_test.go index b213c2795af..640a1648340 100644 --- a/pkg/setting/setting_test.go +++ b/pkg/setting/setting_test.go @@ -3,6 +3,7 @@ package setting import ( "os" "path/filepath" + "runtime" "testing" . "github.com/smartystreets/goconvey/convey" @@ -52,13 +53,22 @@ func TestLoadingSettings(t *testing.T) { }) Convey("Should be able to override via command line", func() { - NewConfigContext(&CommandLineArgs{ - HomePath: "../../", - Args: []string{"cfg:paths.data=/tmp/data", "cfg:paths.logs=/tmp/logs"}, - }) + if runtime.GOOS == "windows" { + NewConfigContext(&CommandLineArgs{ + HomePath: "../../", + Args: []string{`cfg:paths.data=c:\tmp\data`, `cfg:paths.logs=c:\tmp\logs`}, + }) + So(DataPath, ShouldEqual, `c:\tmp\data`) + So(LogsPath, ShouldEqual, `c:\tmp\logs`) + } else { + NewConfigContext(&CommandLineArgs{ + HomePath: "../../", + Args: []string{"cfg:paths.data=/tmp/data", "cfg:paths.logs=/tmp/logs"}, + }) - So(DataPath, ShouldEqual, "/tmp/data") - So(LogsPath, ShouldEqual, "/tmp/logs") + So(DataPath, ShouldEqual, "/tmp/data") + So(LogsPath, ShouldEqual, "/tmp/logs") + } }) Convey("Should be able to override defaults via command line", func() { @@ -74,33 +84,63 @@ func TestLoadingSettings(t *testing.T) { }) Convey("Defaults can be overridden in specified config file", func() { - NewConfigContext(&CommandLineArgs{ - HomePath: "../../", - Config: filepath.Join(HomePath, "tests/config-files/override.ini"), - Args: []string{"cfg:default.paths.data=/tmp/data"}, - }) + if runtime.GOOS == "windows" { + NewConfigContext(&CommandLineArgs{ + HomePath: "../../", + Config: filepath.Join(HomePath, "tests/config-files/override_windows.ini"), + Args: []string{`cfg:default.paths.data=c:\tmp\data`}, + }) - So(DataPath, ShouldEqual, "/tmp/override") + So(DataPath, ShouldEqual, `c:\tmp\override`) + } else { + NewConfigContext(&CommandLineArgs{ + HomePath: "../../", + Config: filepath.Join(HomePath, "tests/config-files/override.ini"), + Args: []string{"cfg:default.paths.data=/tmp/data"}, + }) + + So(DataPath, ShouldEqual, "/tmp/override") + } }) Convey("Command line overrides specified config file", func() { - NewConfigContext(&CommandLineArgs{ - HomePath: "../../", - Config: filepath.Join(HomePath, "tests/config-files/override.ini"), - Args: []string{"cfg:paths.data=/tmp/data"}, - }) + if runtime.GOOS == "windows" { + NewConfigContext(&CommandLineArgs{ + HomePath: "../../", + Config: filepath.Join(HomePath, "tests/config-files/override_windows.ini"), + Args: []string{`cfg:paths.data=c:\tmp\data`}, + }) - So(DataPath, ShouldEqual, "/tmp/data") + So(DataPath, ShouldEqual, `c:\tmp\data`) + } else { + NewConfigContext(&CommandLineArgs{ + HomePath: "../../", + Config: filepath.Join(HomePath, "tests/config-files/override.ini"), + Args: []string{"cfg:paths.data=/tmp/data"}, + }) + + So(DataPath, ShouldEqual, "/tmp/data") + } }) Convey("Can use environment variables in config values", func() { - os.Setenv("GF_DATA_PATH", "/tmp/env_override") - NewConfigContext(&CommandLineArgs{ - HomePath: "../../", - Args: []string{"cfg:paths.data=${GF_DATA_PATH}"}, - }) + if runtime.GOOS == "windows" { + os.Setenv("GF_DATA_PATH", `c:\tmp\env_override`) + NewConfigContext(&CommandLineArgs{ + HomePath: "../../", + Args: []string{"cfg:paths.data=${GF_DATA_PATH}"}, + }) - So(DataPath, ShouldEqual, "/tmp/env_override") + So(DataPath, ShouldEqual, `c:\tmp\env_override`) + } else { + os.Setenv("GF_DATA_PATH", "/tmp/env_override") + NewConfigContext(&CommandLineArgs{ + HomePath: "../../", + Args: []string{"cfg:paths.data=${GF_DATA_PATH}"}, + }) + + So(DataPath, ShouldEqual, "/tmp/env_override") + } }) Convey("instance_name default to hostname even if hostname env is empty", func() { diff --git a/tests/config-files/override_windows.ini b/tests/config-files/override_windows.ini new file mode 100644 index 00000000000..c0219afc8c8 --- /dev/null +++ b/tests/config-files/override_windows.ini @@ -0,0 +1,3 @@ +[paths] +data = c:\tmp\override + From 91ad2605172615dcbf57adb13321b35d31b6d06d Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 19 Jun 2017 21:01:42 +0200 Subject: [PATCH 25/64] build: on windows, ignore linux packaging --- appveyor.yml | 1 + build.go | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 303c3abca9e..af28a77b8c5 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -29,6 +29,7 @@ install: build_script: - go run build.go build + - go test -v ./pkg/... - grunt release - go run build.go sha-dist - cp dist/* . diff --git a/build.go b/build.go index f60a53e5ddb..a1d1d3012ab 100644 --- a/build.go +++ b/build.go @@ -95,7 +95,9 @@ func main() { case "package": grunt(gruntBuildArg("release")...) - createLinuxPackages() + if runtime.GOOS != "windows" { + createLinuxPackages() + } case "pkg-rpm": grunt(gruntBuildArg("release")...) @@ -345,7 +347,11 @@ func ChangeWorkingDir(dir string) { } func grunt(params ...string) { - runPrint("./node_modules/.bin/grunt", params...) + if runtime.GOOS == "windows" { + runPrint(`.\node_modules\.bin\grunt`, params...) + } else { + runPrint("./node_modules/.bin/grunt", params...) + } } func gruntBuildArg(task string) []string { From 3ac306a72ee8a44617935c6fc7af21a2d6044a6e Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 28 Jun 2017 16:29:46 +0200 Subject: [PATCH 26/64] playlist: fixes #6727. Remember Kiosk mode --- public/app/features/playlist/playlist_srv.ts | 45 ++++++++++++++++---- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/public/app/features/playlist/playlist_srv.ts b/public/app/features/playlist/playlist_srv.ts index 5ddfe572cda..c9553dcdcca 100644 --- a/public/app/features/playlist/playlist_srv.ts +++ b/public/app/features/playlist/playlist_srv.ts @@ -3,6 +3,7 @@ import angular from 'angular'; import coreModule from '../../core/core_module'; import kbn from 'app/core/utils/kbn'; +import appEvents from 'app/core/app_events'; class PlaylistSrv { private cancelPromise: any; @@ -14,7 +15,13 @@ class PlaylistSrv { public isPlaying: boolean; /** @ngInject */ - constructor(private $rootScope: any, private $location: any, private $timeout: any, private backendSrv: any) { } + constructor( + private $rootScope: any, + private $location: any, + private $timeout: any, + private backendSrv: any, + private $routeParams: any + ) { } next() { this.$timeout.cancel(this.cancelPromise); @@ -22,14 +29,32 @@ class PlaylistSrv { var playedAllDashboards = this.index > this.dashboards.length - 1; if (playedAllDashboards) { - window.location.href = this.startUrl; - } else { - var dash = this.dashboards[this.index]; - this.$location.url('dashboard/' + dash.uri); - - this.index++; - this.cancelPromise = this.$timeout(() => this.next(), this.interval); + window.location.href = this.getUrlWithKioskMode(); + return; } + + var dash = this.dashboards[this.index]; + this.$location.url('dashboard/' + dash.uri); + + this.index++; + this.cancelPromise = this.$timeout(() => this.next(), this.interval); + } + + getUrlWithKioskMode() { + const inKioskMode = document.body.classList.contains('page-kiosk-mode'); + + // check if should add kiosk query param + if (inKioskMode && this.startUrl.indexOf('kiosk') === -1) { + return this.startUrl + '?kiosk=true'; + } + + // check if should remove kiosk query param + if (!inKioskMode) { + return this.startUrl.split("?")[0]; + } + + // already has kiosk query param, just return startUrl + return this.startUrl; } prev() { @@ -45,6 +70,10 @@ class PlaylistSrv { this.playlistId = playlistId; this.isPlaying = true; + if (this.$routeParams.kiosk) { + appEvents.emit('toggle-kiosk-mode'); + } + this.backendSrv.get(`/api/playlists/${playlistId}`).then(playlist => { this.backendSrv.get(`/api/playlists/${playlistId}/dashboards`).then(dashboards => { this.dashboards = dashboards; From 8634c9d457d6e624d762a476e817a401bebcf442 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 28 Jun 2017 18:12:55 +0300 Subject: [PATCH 27/64] Histogram fix (#8727) * histogram: don't cut negative values, issue #8628 * histogram: add percent/count option * histogram: add tests for values normalizing * histogram: improved ticks rendering * histogram: fix default value in axes editor --- .../app/plugins/panel/graph/axes_editor.html | 6 ++++ public/app/plugins/panel/graph/axes_editor.ts | 6 ++++ public/app/plugins/panel/graph/graph.ts | 29 +++++++++++++---- public/app/plugins/panel/graph/histogram.ts | 11 +++++-- public/app/plugins/panel/graph/module.ts | 3 +- .../panel/graph/specs/histogram_specs.ts | 32 ++++++++++++++++--- 6 files changed, 73 insertions(+), 14 deletions(-) diff --git a/public/app/plugins/panel/graph/axes_editor.html b/public/app/plugins/panel/graph/axes_editor.html index b0ab759bf18..1e577fe392e 100644 --- a/public/app/plugins/panel/graph/axes_editor.html +++ b/public/app/plugins/panel/graph/axes_editor.html @@ -67,6 +67,12 @@ +
+ +
+ +
+
diff --git a/public/app/plugins/panel/graph/axes_editor.ts b/public/app/plugins/panel/graph/axes_editor.ts index adb3d1d6706..155265e5987 100644 --- a/public/app/plugins/panel/graph/axes_editor.ts +++ b/public/app/plugins/panel/graph/axes_editor.ts @@ -10,6 +10,7 @@ export class AxesEditorCtrl { xAxisModes: any; xAxisStatOptions: any; xNameSegment: any; + histogramValues: any; /** @ngInject **/ constructor(private $scope, private $q) { @@ -34,6 +35,11 @@ export class AxesEditorCtrl { // 'Data field': 'field', }; + this.histogramValues = { + 'Percent': 'percent', + 'Count': 'count' + }; + this.xAxisStatOptions = [ {text: 'Avg', value: 'avg'}, {text: 'Min', value: 'min'}, diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 16d259c210e..8c928fb6a41 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -312,10 +312,13 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv, popoverSrv) { let histMax = _.max(_.map(data, s => s.stats.max)); let ticks = panel.xaxis.buckets || panelWidth / 50; bucketSize = tickStep(histMin, histMax, ticks); - let histogram = convertValuesToHistogram(values, bucketSize); + let normalize = panel.xaxis.histogramValue === 'percent'; + let histogram = convertValuesToHistogram(values, bucketSize, normalize); + + let seriesLabel = panel.xaxis.histogramValue || "count"; data[0].data = histogram; - data[0].alias = data[0].label = data[0].id = "count"; + data[0].alias = data[0].label = data[0].id = seriesLabel; data = [data[0]]; options.series.bars.barWidth = bucketSize * 0.8; @@ -422,21 +425,32 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv, popoverSrv) { function addXHistogramAxis(options, bucketSize) { let ticks, min, max; + let defaultTicks = panelWidth / 50; if (data.length && bucketSize) { ticks = _.map(data[0].data, point => point[0]); + min = _.min(ticks); + max = _.max(ticks); + + // Adjust tick step + let tickStep = bucketSize; + let ticks_num = Math.floor((max - min) / tickStep); + while (ticks_num > defaultTicks) { + tickStep = tickStep * 2; + ticks_num = Math.ceil((max - min) / tickStep); + } // Expand ticks for pretty view - min = Math.max(0, _.min(ticks) - bucketSize); - max = _.max(ticks) + bucketSize; + min = Math.floor(min / tickStep) * tickStep; + max = Math.ceil(max / tickStep) * tickStep; ticks = []; - for (let i = min; i <= max; i += bucketSize) { + for (let i = min; i <= max; i += tickStep) { ticks.push(i); } } else { // Set defaults if no data - ticks = panelWidth / 100; + ticks = defaultTicks / 2; min = 0; max = 1; } @@ -450,6 +464,9 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv, popoverSrv) { label: "Histogram", ticks: ticks }; + + // Use 'short' format for histogram values + configureAxisMode(options.xaxis, 'short'); } function addXTableAxis(options) { diff --git a/public/app/plugins/panel/graph/histogram.ts b/public/app/plugins/panel/graph/histogram.ts index e6ad7ebb0f6..efb13c546f0 100644 --- a/public/app/plugins/panel/graph/histogram.ts +++ b/public/app/plugins/panel/graph/histogram.ts @@ -26,7 +26,7 @@ export function getSeriesValues(data: any): number[] { * @param values * @param bucketSize */ -export function convertValuesToHistogram(values: number[], bucketSize: number): any[] { +export function convertValuesToHistogram(values: number[], bucketSize: number, normalize = false): any[] { let histogram = {}; for (let i = 0; i < values.length; i++) { @@ -38,9 +38,16 @@ export function convertValuesToHistogram(values: number[], bucketSize: number): } } - return _.map(histogram, (count, bound) => { + let histogam_series = _.map(histogram, (count, bound) => { + if (normalize && values.length) { + return [Number(bound), count / values.length]; + } + return [Number(bound), count]; }); + + // Sort by Y axis values + return _.sortBy(histogam_series, point => point[0]); } function getBucketBound(value: number, bucketSize: number): number { diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index e9ca8c5c4e2..fed739a60d7 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -58,7 +58,8 @@ class GraphCtrl extends MetricsPanelCtrl { mode: 'time', name: null, values: [], - buckets: null + buckets: null, + histogramValue: 'percent' }, // show/hide lines lines : true, diff --git a/public/app/plugins/panel/graph/specs/histogram_specs.ts b/public/app/plugins/panel/graph/specs/histogram_specs.ts index 71b3def8d1d..4d5ad02aec9 100644 --- a/public/app/plugins/panel/graph/specs/histogram_specs.ts +++ b/public/app/plugins/panel/graph/specs/histogram_specs.ts @@ -1,7 +1,6 @@ /// - +import _ from 'lodash'; import { describe, beforeEach, it, expect } from '../../../../../test/lib/common'; - import { convertValuesToHistogram, getSeriesValues } from '../histogram'; describe('Graph Histogam Converter', function () { @@ -11,13 +10,13 @@ describe('Graph Histogam Converter', function () { let bucketSize = 10; beforeEach(() => { - values = [1, 2, 10, 11, 17, 20, 29]; + values = [1, 2, 10, 11, 17, 20, 29, 30, 31, 33]; }); it('Should convert to series-like array', () => { bucketSize = 10; let expected = [ - [0, 2], [10, 3], [20, 2] + [0, 2], [10, 3], [20, 2], [30, 3] ]; let histogram = convertValuesToHistogram(values, bucketSize); @@ -27,12 +26,35 @@ describe('Graph Histogam Converter', function () { it('Should not add empty buckets', () => { bucketSize = 5; let expected = [ - [0, 2], [10, 2], [15, 1], [20, 1], [25, 1] + [0, 2], [10, 2], [15, 1], [20, 1], [25, 1], [30, 3] ]; let histogram = convertValuesToHistogram(values, bucketSize); expect(histogram).to.eql(expected); }); + + it('Should normalize values', () => { + bucketSize = 5; + let normalize = true; + let expected = [ + [0, 0.2], [10, 0.2], [15, 0.1], [20, 0.1], [25, 0.1], [30, 0.3] + ]; + + let histogram = convertValuesToHistogram(values, bucketSize, normalize); + expect(histogram).to.eql(expected); + }); + + it('Sum of normalized values should be 1', () => { + bucketSize = 5; + let normalize = true; + let expected = [ + [0, 0.2], [10, 0.2], [15, 0.1], [20, 0.1], [25, 0.1], [30, 0.3] + ]; + + let histogram = convertValuesToHistogram(values, bucketSize, normalize); + let sum = _.reduce(histogram, (sum, point) => sum + point[1], 0); + expect(sum).to.eql(1); + }); }); describe('Series to values converter', () => { From 97a7081b5735af8bb746ffd0e797f946f990ab34 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 29 Jun 2017 11:40:28 +0300 Subject: [PATCH 28/64] Fix 8706 (#8734) * heatmap: fix incorrect time for UTC timezone, fixes #8706 * heatmap: fix tests for time format --- public/app/plugins/panel/heatmap/rendering.ts | 9 ++++++++- .../plugins/panel/heatmap/specs/renderer_specs.ts | 14 +++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index 94903cfcb81..b928173eafe 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -100,10 +100,17 @@ export default function link(scope, elem, attrs, ctrl) { let ticks = chartWidth / DEFAULT_X_TICK_SIZE_PX; let grafanaTimeFormatter = grafanaTimeFormat(ticks, timeRange.from, timeRange.to); + let timeFormat; + let dashboardTimeZone = ctrl.dashboard.getTimezone(); + if (dashboardTimeZone === 'utc') { + timeFormat = d3.utcFormat(grafanaTimeFormatter); + } else { + timeFormat = d3.timeFormat(grafanaTimeFormatter); + } let xAxis = d3.axisBottom(xScale) .ticks(ticks) - .tickFormat(d3.timeFormat(grafanaTimeFormatter)) + .tickFormat(timeFormat) .tickPadding(X_AXIS_TICK_PADDING) .tickSize(chartHeight); diff --git a/public/app/plugins/panel/heatmap/specs/renderer_specs.ts b/public/app/plugins/panel/heatmap/specs/renderer_specs.ts index 9ca7297e9b6..5d3eb665e55 100644 --- a/public/app/plugins/panel/heatmap/specs/renderer_specs.ts +++ b/public/app/plugins/panel/heatmap/specs/renderer_specs.ts @@ -153,11 +153,11 @@ describe('grafanaHeatmap', function () { it('should draw correct X axis', function () { var xTicks = getTicks(ctx.element, ".axis-x"); let expectedTicks = [ - formatLocalTime("01 Mar 2017 10:00:00"), - formatLocalTime("01 Mar 2017 10:15:00"), - formatLocalTime("01 Mar 2017 10:30:00"), - formatLocalTime("01 Mar 2017 10:45:00"), - formatLocalTime("01 Mar 2017 11:00:00") + formatTime("01 Mar 2017 10:00:00"), + formatTime("01 Mar 2017 10:15:00"), + formatTime("01 Mar 2017 10:30:00"), + formatTime("01 Mar 2017 10:45:00"), + formatTime("01 Mar 2017 11:00:00") ]; expect(xTicks).to.eql(expectedTicks); }); @@ -261,7 +261,7 @@ function getTicks(element, axisSelector) { }).get(); } -function formatLocalTime(timeStr) { +function formatTime(timeStr) { let format = "HH:mm"; - return moment.utc(timeStr, 'DD MMM YYYY HH:mm:ss').local().format(format); + return moment.utc(timeStr, 'DD MMM YYYY HH:mm:ss').format(format); } From 7ea5930a90eb198ecd2b4051f27424ae35fd07bf Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 29 Jun 2017 12:18:10 +0200 Subject: [PATCH 29/64] alerting: minor fix --- public/app/features/alerting/notification_edit_ctrl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/alerting/notification_edit_ctrl.ts b/public/app/features/alerting/notification_edit_ctrl.ts index 768cc16266e..d1b855cbf3a 100644 --- a/public/app/features/alerting/notification_edit_ctrl.ts +++ b/public/app/features/alerting/notification_edit_ctrl.ts @@ -88,7 +88,7 @@ export class AlertNotificationEditCtrl { this.backendSrv.post(`/api/alert-notifications/test`, payload) .then(res => { - appEvents.emit('alert-succes', ['Test notification sent', '']); + appEvents.emit('alert-success', ['Test notification sent', '']); }); } } From 8683aff3e952ca1b2472d6249a13f1b8c34c77b0 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 29 Jun 2017 12:00:59 +0200 Subject: [PATCH 30/64] appveyor: build fix for go tests --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index af28a77b8c5..9f8e9a26622 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -29,10 +29,10 @@ install: build_script: - go run build.go build - - go test -v ./pkg/... - grunt release - go run build.go sha-dist - cp dist/* . + - go test -v ./pkg/... artifacts: - path: grafana-*windows-*.* From fb99ddf2955e60551cbaae23e358ee4e33136e1c Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 29 Jun 2017 13:58:54 +0200 Subject: [PATCH 31/64] influxdb: tweak to help text --- docs/sources/features/datasources/influxdb.md | 2 +- .../plugins/datasource/influxdb/partials/query.options.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/sources/features/datasources/influxdb.md b/docs/sources/features/datasources/influxdb.md index 1f6c36a6bb3..39a22085d93 100644 --- a/docs/sources/features/datasources/influxdb.md +++ b/docs/sources/features/datasources/influxdb.md @@ -88,7 +88,7 @@ You can switch to raw query mode by clicking hamburger icon and then `Switch edi - $m = replaced with measurement name - $measurement = replaced with measurement name - $col = replaced with column name -- $tag_hostname = replaced with the value of the hostname tag. Use your tag instead of hostname and the tag must be used to group by in the query when used in the ALIAS BY field. +- $tag_exampletag = replaced with the value of the `exampletag` tag. To use your tag as an alias in the ALIAS BY field then the tag must be used to group by in the query. - You can also use [[tag_hostname]] pattern replacement syntax. For example, in the ALIAS BY field using this text `Host: [[tag_hostname]]` would substitute in the `hostname` tag value for each legend value and an example legend value would be: `Host: server1`. ### Table query / raw data diff --git a/public/app/plugins/datasource/influxdb/partials/query.options.html b/public/app/plugins/datasource/influxdb/partials/query.options.html index 80e7f0e59ea..68293b69f70 100644 --- a/public/app/plugins/datasource/influxdb/partials/query.options.html +++ b/public/app/plugins/datasource/influxdb/partials/query.options.html @@ -46,8 +46,8 @@
  • $measurement = replaced with measurement name
  • $1 - $9 = replaced with part of measurement name (if you separate your measurement name with dots)
  • $col = replaced with column name
  • -
  • $tag_hostname = replaced with the value of the hostname tag
  • -
  • You can also use [[tag_hostname]] pattern replacement syntax
  • +
  • $tag_exampletag = replaced with the value of the exampletag tag
  • +
  • You can also use [[tag_exampletag]] pattern replacement syntax
  • From 1fd7b60efec07c8dcf9a1c3182a2a7c1f333db16 Mon Sep 17 00:00:00 2001 From: Ben Tranter Date: Thu, 29 Jun 2017 14:38:48 -0400 Subject: [PATCH 32/64] Add more information to basic diff logic --- pkg/components/dashdiffs/formatter_basic.go | 175 +++++++++++++------- 1 file changed, 114 insertions(+), 61 deletions(-) diff --git a/pkg/components/dashdiffs/formatter_basic.go b/pkg/components/dashdiffs/formatter_basic.go index 5b1d5504bc6..af93ff42bf2 100644 --- a/pkg/components/dashdiffs/formatter_basic.go +++ b/pkg/components/dashdiffs/formatter_basic.go @@ -98,13 +98,7 @@ func (b *BasicDiff) Basic(lines []*JSONLine) []*BasicBlock { blocks := make([]*BasicBlock, 0) for _, line := range lines { - // In order to produce distinct "blocks" when rendering the basic diff, - // we need a way to distinguish between differnt sections of data. - // To do this, we consider the value(s) of each top-level JSON key to - // represent a distinct block for Grafana's JSON data structure, so - // we perform this check to see if we've entered a new "block". If we - // have, we simply append the existing block to the array of blocks. - if b.LastIndent == 2 && line.Indent == 1 && line.Change == ChangeNil { + if b.returnToTopLevelKey(line) { if b.Block != nil { blocks = append(blocks, b.Block) } @@ -114,56 +108,9 @@ func (b *BasicDiff) Basic(lines []*JSONLine) []*BasicBlock { // check for a change in depth inside the JSON data structures. b.LastIndent = line.Indent - // TODO: why special handling for indent 2? - // Here we - // If the line's indentation is at level 1, then we know it's a top - // level key in the JSON document. As mentioned earlier, we treat these - // specially as they indicate their values belong to distinct blocks. - // - // At level 1, we only record single-line changes, ie, the "added", - // "deleted", "old" or "new" cases, since we know those values aren't - // arrays or maps. We only handle these cases at level 2 or deeper, - // since for those we either output a "change" or "summary". This is - // done for formatting reasons only, so we have logical "blocks" to - // display. if line.Indent == 1 { - switch line.Change { - case ChangeNil: - if line.Change == ChangeNil { - if line.Key != "" { - b.Block = &BasicBlock{ - Title: line.Key, - Change: line.Change, - } - } - } - - case ChangeAdded, ChangeDeleted: - blocks = append(blocks, &BasicBlock{ - Title: line.Key, - Change: line.Change, - New: line.Val, - LineStart: line.LineNum, - }) - - case ChangeOld: - b.Block = &BasicBlock{ - Title: line.Key, - Old: line.Val, - Change: line.Change, - LineStart: line.LineNum, - } - - case ChangeNew: - b.Block.New = line.Val - b.Block.LineEnd = line.LineNum - - // For every "old" change there is a corresponding "new", which - // is why we wait until we detect the "new" change before - // appending the change. - blocks = append(blocks, b.Block) - default: - // ok + if block, ok := b.handleTopLevelChange(line); ok { + blocks = append(blocks, block) } } @@ -182,8 +129,8 @@ func (b *BasicDiff) Basic(lines []*JSONLine) []*BasicBlock { // finding the change, we append it to the current block, and begin // performing comparisons again. if line.Indent > 1 { - // Ensure a single line change - if line.Key != "" && line.Val != nil && !b.writing { + // check to ensure a single line change + if b.isSingleLineChange(line) { switch line.Change { case ChangeAdded, ChangeDeleted: @@ -211,13 +158,31 @@ func (b *BasicDiff) Basic(lines []*JSONLine) []*BasicBlock { //ok } + // otherwise, we're dealing with a change at a deeper level. We + // know there's a change somewhere in the JSON tree, but we + // don't know exactly where, so we go deeper. } else { + + // if the change is anything but unchanged, continue processing + // + // we keep "narrowing" the key as we go deeper, in order to + // correctly report the key name for changes found within an + // object or array. if line.Change != ChangeUnchanged { if line.Key != "" { b.narrow = line.Key b.keysIdent = line.Indent } + // if the change isn't nil, and we're not already writing + // out a change, then we've found something. + // + // First, try to determine the title of the embedded JSON + // object. If it's an empty string, then we're in an object + // or array, so we default to using the "narrowed" key. + // + // We also start recording the basic summary, until we find + // the next `ChangeUnchanged`. if line.Change != ChangeNil { if !b.writing { b.writing = true @@ -237,6 +202,17 @@ func (b *BasicDiff) Basic(lines []*JSONLine) []*BasicBlock { } } } + // if we find a `ChangeUnchanged`, we do one of two things: + // + // - if we're recording a change already, then we know + // we've come to the end of that change block, so we write + // that change out be recording the line number of where + // that change ends, and append it to the current block's + // summary. + // + // - if we're not recording a change, then we do nothing, + // since the BasicDiff doesn't report on unchanged JSON + // values. } else { if b.writing { b.writing = false @@ -251,6 +227,81 @@ func (b *BasicDiff) Basic(lines []*JSONLine) []*BasicBlock { return blocks } +// returnToTopLevelKey indicates that we've moved from a key at one level deep +// in the JSON document to a top level key. +// +// In order to produce distinct "blocks" when rendering the basic diff, +// we need a way to distinguish between differnt sections of data. +// To do this, we consider the value(s) of each top-level JSON key to +// represent a distinct block for Grafana's JSON data structure, so +// we perform this check to see if we've entered a new "block". If we +// have, we simply append the existing block to the array of blocks. +func (b *BasicDiff) returnToTopLevelKey(line *JSONLine) bool { + return b.LastIndent == 2 && line.Indent == 1 && line.Change == ChangeNil +} + +// handleTopLevelChange handles a change on one of the top-level keys on a JSON +// document. +// +// If the line's indentation is at level 1, then we know it's a top +// level key in the JSON document. As mentioned earlier, we treat these +// specially as they indicate their values belong to distinct blocks. +// +// At level 1, we only record single-line changes, ie, the "added", +// "deleted", "old" or "new" cases, since we know those values aren't +// arrays or maps. We only handle these cases at level 2 or deeper, +// since for those we either output a "change" or "summary". This is +// done for formatting reasons only, so we have logical "blocks" to +// display. +func (b *BasicDiff) handleTopLevelChange(line *JSONLine) (*BasicBlock, bool) { + switch line.Change { + case ChangeNil: + if line.Change == ChangeNil { + if line.Key != "" { + b.Block = &BasicBlock{ + Title: line.Key, + Change: line.Change, + } + } + } + + case ChangeAdded, ChangeDeleted: + return &BasicBlock{ + Title: line.Key, + Change: line.Change, + New: line.Val, + LineStart: line.LineNum, + }, true + + case ChangeOld: + b.Block = &BasicBlock{ + Title: line.Key, + Old: line.Val, + Change: line.Change, + LineStart: line.LineNum, + } + + case ChangeNew: + b.Block.New = line.Val + b.Block.LineEnd = line.LineNum + + // For every "old" change there is a corresponding "new", which + // is why we wait until we detect the "new" change before + // appending the change. + return b.Block, true + default: + // ok + } + + return nil, false +} + +// isSingleLineChange ensures we're iterating over a single line change (ie, +// either a single line or a old-new value pair was changed in the JSON file). +func (b *BasicDiff) isSingleLineChange(line *JSONLine) bool { + return line.Key != "" && line.Val != nil && !b.writing +} + // encStateMap is used in the template helper var ( encStateMap = map[ChangeType]string{ @@ -273,7 +324,9 @@ var ( ) var ( - // tplBlock is the whole thing + // tplBlock is the container for the basic diff. It iterates over each + // basic block, expanding each "change" and "summary" belonging to every + // block. tplBlock = `{{ define "block" -}} {{ range . }}
    @@ -319,7 +372,7 @@ var ( {{ end }} {{ end }}` - // tplChange is the template for changes + // tplChange is the template for basic changes. tplChange = `{{ define "change" -}}
  • @@ -346,7 +399,7 @@ var (
  • {{ end }}` - // tplSummary is for basis summaries + // tplSummary is for basic summaries. tplSummary = `{{ define "summary" -}}
    From b8aa203707f2c526152f9c0a43110859094e1d70 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Fri, 30 Jun 2017 20:21:05 +0200 Subject: [PATCH 33/64] signup: fix email sent logic for tempuser Fixes #8656 and properly sets the email_sent and email_sent_on fields for a tempuser (signup user). --- pkg/api/org_invite.go | 5 +++++ pkg/models/temp_user.go | 4 ++++ pkg/services/notifications/notifications.go | 8 +++++++- pkg/services/sqlstore/datasource_test.go | 2 ++ pkg/services/sqlstore/temp_user.go | 15 +++++++++++++++ pkg/services/sqlstore/temp_user_test.go | 13 +++++++++++++ 6 files changed, 46 insertions(+), 1 deletion(-) diff --git a/pkg/api/org_invite.go b/pkg/api/org_invite.go index 9e85808950f..b776186aec7 100644 --- a/pkg/api/org_invite.go +++ b/pkg/api/org_invite.go @@ -78,6 +78,11 @@ func AddOrgInvite(c *middleware.Context, inviteDto dtos.AddInviteForm) Response return ApiError(500, "Failed to send email invite", err) } + emailSentCmd := m.UpdateTempUserWithEmailSentCommand{Code: cmd.Result.Code} + if err := bus.Dispatch(&emailSentCmd); err != nil { + return ApiError(500, "Failed to update invite with email sent info", err) + } + return ApiSuccess(fmt.Sprintf("Sent invite to %s", inviteDto.LoginOrEmail)) } diff --git a/pkg/models/temp_user.go b/pkg/models/temp_user.go index 00c496c4844..0e09627aab1 100644 --- a/pkg/models/temp_user.go +++ b/pkg/models/temp_user.go @@ -60,6 +60,10 @@ type UpdateTempUserStatusCommand struct { Status TempUserStatus } +type UpdateTempUserWithEmailSentCommand struct { + Code string +} + type GetTempUsersQuery struct { OrgId int64 Email string diff --git a/pkg/services/notifications/notifications.go b/pkg/services/notifications/notifications.go index c765774d062..25eb2b5936a 100644 --- a/pkg/services/notifications/notifications.go +++ b/pkg/services/notifications/notifications.go @@ -146,7 +146,7 @@ func signUpStartedHandler(evt *events.SignUpStarted) error { return nil } - return sendEmailCommandHandler(&m.SendEmailCommand{ + err := sendEmailCommandHandler(&m.SendEmailCommand{ To: []string{evt.Email}, Template: tmplSignUpStarted, Data: map[string]interface{}{ @@ -155,6 +155,12 @@ func signUpStartedHandler(evt *events.SignUpStarted) error { "SignUpUrl": setting.ToAbsUrl(fmt.Sprintf("signup/?email=%s&code=%s", url.QueryEscape(evt.Email), url.QueryEscape(evt.Code))), }, }) + if err != nil { + return err + } + + emailSentCmd := m.UpdateTempUserWithEmailSentCommand{Code: evt.Code} + return bus.Dispatch(&emailSentCmd) } func signUpCompletedHandler(evt *events.SignUpCompleted) error { diff --git a/pkg/services/sqlstore/datasource_test.go b/pkg/services/sqlstore/datasource_test.go index 2749a3cc426..51bc759fdaf 100644 --- a/pkg/services/sqlstore/datasource_test.go +++ b/pkg/services/sqlstore/datasource_test.go @@ -16,6 +16,8 @@ func InitTestDB(t *testing.T) { //x, err := xorm.NewEngine(sqlutil.TestDB_Mysql.DriverName, sqlutil.TestDB_Mysql.ConnStr) //x, err := xorm.NewEngine(sqlutil.TestDB_Postgres.DriverName, sqlutil.TestDB_Postgres.ConnStr) + // x.ShowSQL() + if err != nil { t.Fatalf("Failed to init in memory sqllite3 db %v", err) } diff --git a/pkg/services/sqlstore/temp_user.go b/pkg/services/sqlstore/temp_user.go index 8864d5fb02a..43e1f027057 100644 --- a/pkg/services/sqlstore/temp_user.go +++ b/pkg/services/sqlstore/temp_user.go @@ -12,6 +12,7 @@ func init() { bus.AddHandler("sql", GetTempUsersQuery) bus.AddHandler("sql", UpdateTempUserStatus) bus.AddHandler("sql", GetTempUserByCode) + bus.AddHandler("sql", UpdateTempUserWithEmailSent) } func UpdateTempUserStatus(cmd *m.UpdateTempUserStatusCommand) error { @@ -35,6 +36,7 @@ func CreateTempUser(cmd *m.CreateTempUserCommand) error { Status: cmd.Status, RemoteAddr: cmd.RemoteAddr, InvitedByUserId: cmd.InvitedByUserId, + EmailSentOn: time.Now(), Created: time.Now(), Updated: time.Now(), } @@ -48,6 +50,19 @@ func CreateTempUser(cmd *m.CreateTempUserCommand) error { }) } +func UpdateTempUserWithEmailSent(cmd *m.UpdateTempUserWithEmailSentCommand) error { + return inTransaction(func(sess *DBSession) error { + user := &m.TempUser{ + EmailSent: true, + EmailSentOn: time.Now(), + } + + _, err := sess.Where("code = ?", cmd.Code).Cols("email_sent", "email_sent_on").Update(user) + + return err + }) +} + func GetTempUsersQuery(query *m.GetTempUsersQuery) error { rawSql := `SELECT tu.id as id, diff --git a/pkg/services/sqlstore/temp_user_test.go b/pkg/services/sqlstore/temp_user_test.go index ebf753890f6..80560258162 100644 --- a/pkg/services/sqlstore/temp_user_test.go +++ b/pkg/services/sqlstore/temp_user_test.go @@ -54,6 +54,19 @@ func TestTempUserCommandsAndQueries(t *testing.T) { So(err, ShouldBeNil) }) + Convey("Should be able update email sent and email sent on", func() { + cmd3 := m.UpdateTempUserWithEmailSentCommand{Code: cmd.Result.Code} + err := UpdateTempUserWithEmailSent(&cmd3) + So(err, ShouldBeNil) + + query := m.GetTempUsersQuery{OrgId: 2256, Status: m.TmpUserInvitePending} + err = GetTempUsersQuery(&query) + + So(err, ShouldBeNil) + So(query.Result[0].EmailSent, ShouldBeTrue) + So(query.Result[0].EmailSentOn, ShouldHappenOnOrAfter, (query.Result[0].Created)) + }) + }) }) } From 1499c2bf747b81db44a9f23cf6d784fc033bff4d Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 3 Jul 2017 10:20:55 +0300 Subject: [PATCH 34/64] Fix User/Org default timezone bug (#8748) * dashboard: don't override timezone if default selected, issue #8503 * dashboard: hide UTC icon immediately after timezone changing --- public/app/features/dashboard/model.ts | 4 ++-- public/app/features/dashboard/timepicker/timepicker.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashboard/model.ts b/public/app/features/dashboard/model.ts index cdb61ac4ad7..01c805694a6 100644 --- a/public/app/features/dashboard/model.ts +++ b/public/app/features/dashboard/model.ts @@ -247,9 +247,9 @@ export class DashboardModel { formatDate(date, format?) { date = moment.isMoment(date) ? date : moment(date); format = format || 'YYYY-MM-DD HH:mm:ss'; - this.timezone = this.getTimezone(); + let timezone = this.getTimezone(); - return this.timezone === 'browser' ? + return timezone === 'browser' ? moment(date).format(format) : moment.utc(date).format(format); } diff --git a/public/app/features/dashboard/timepicker/timepicker.ts b/public/app/features/dashboard/timepicker/timepicker.ts index 2cdfd7a77f6..bf3937f8322 100644 --- a/public/app/features/dashboard/timepicker/timepicker.ts +++ b/public/app/features/dashboard/timepicker/timepicker.ts @@ -56,6 +56,7 @@ export class TimePickerCtrl { if (moment.isMoment(timeRaw.to)) { timeRaw.to.local(); } + this.isUtc = false; } else { this.isUtc = true; } From 6f4c7a4d65c613f2129637b6ffec0a058d6430a4 Mon Sep 17 00:00:00 2001 From: Ben Tranter Date: Mon, 3 Jul 2017 08:29:30 -0400 Subject: [PATCH 35/64] Add dashboard version history documentation (#8741) Adds docs for the new API endpoints, and for the dashboard history feature. --- docs/sources/http_api/dashboard_versions.md | 321 ++++++++++++++++++++ docs/sources/reference/dashboard_history.md | 40 +++ 2 files changed, 361 insertions(+) create mode 100644 docs/sources/http_api/dashboard_versions.md create mode 100644 docs/sources/reference/dashboard_history.md diff --git a/docs/sources/http_api/dashboard_versions.md b/docs/sources/http_api/dashboard_versions.md new file mode 100644 index 00000000000..3d0ec27a3a3 --- /dev/null +++ b/docs/sources/http_api/dashboard_versions.md @@ -0,0 +1,321 @@ ++++ +title = "Dashboard Versions HTTP API " +description = "Grafana Dashboard Versions HTTP API" +keywords = ["grafana", "http", "documentation", "api", "dashboard", "versions"] +aliases = ["/http_api/dashboardversions/"] +type = "docs" +[menu.docs] +name = "Dashboard Versions" +parent = "http_api" ++++ + +# Dashboard Versions + +## Get all dashboard versions + +Query parameters: + +- **limit** - Maximum number of results to return +- **start** - Version to start from when returning queries + +`GET /api/dashboards/id/:dashboardId/versions` + +Gets all existing dashboard versions for the dashboard with the given `dashboardId`. + +**Example request for getting all dashboard versions**: + +```http +GET /api/dashboards/id/1/versions?limit=2?start=0 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +**Example Response** + +```http +HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 +Content-Length: 428 + +[ + { + "id": 2, + "dashboardId": 1, + "parentVersion": 1, + "restoredFrom": 0, + "version": 2, + "created": "2017-06-08T17:24:33-04:00", + "createdBy": "admin", + "message": "Updated panel title" + }, + { + "id": 1, + "dashboardId": 1, + "parentVersion": 0, + "restoredFrom": 0, + "version": 1, + "created": "2017-06-08T17:23:33-04:00", + "createdBy": "admin", + "message": "Initial save" + } +] +``` + +Status Codes: + +- **200** - Ok +- **400** - Errors +- **401** - Unauthorized +- **404** - Dashboard version not found + +## Get dashboard version + +`GET /api/dashboards/id/:dashboardId/versions/:id` + +Get the dashboard version with the given id, for the dashboard with the given id. + +**Example request for getting a dashboard version**: + +```http +GET /api/dashboards/id/1/versions/1 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +**Example response**: + +```http +HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 +Content-Length: 1300 + +{ + "id": 1, + "dashboardId": 1, + "parentVersion": 0, + "restoredFrom": 0, + "version": 1, + "created": "2017-04-26T17:18:38-04:00", + "message": "Initial save", + "data": { + "annotations": { + "list": [ + + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "hideControls": false, + "id": 1, + "links": [ + + ], + "rows": [ + { + "collapse": false, + "height": "250px", + "panels": [ + + ], + "repeat": null, + "repeatIteration": null, + "repeatRowId": null, + "showTitle": false, + "title": "Dashboard Row", + "titleSize": "h6" + } + ], + "schemaVersion": 14, + "style": "dark", + "tags": [ + + ], + "templating": { + "list": [ + + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "browser", + "title": "test", + "version": 1 + }, + "createdBy": "admin" +} +``` + +Status Codes: + +- **200** - Ok +- **401** - Unauthorized +- **404** - Dashboard version not found + +## Restore dashboard + +`POST /api/dashboards/id/:dashboardId/restore` + +Restores a dashboard to a given dashboard version. + +**Example request for restoring a dashboard version**: + +```http +POST /api/dashboards/id/1/restore +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + +{ + "version": 1 +} +``` + +JSON body schema: + +- **version** - The dashboard version to restore to + +**Example response**: + +```http +HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 +Content-Length: 67 + +{ + "slug": "my-dashboard", + "status": "success", + "version": 3 +} +``` + +JSON response body schema: + +- **slug** - the URL friendly slug of the dashboard's title +- **status** - whether the restoration was successful or not +- **version** - the new dashboard version, following the restoration + +Status codes: + +- **200** - OK +- **401** - Unauthorized +- **404** - Not found (dashboard not found or dashboard version not found) +- **500** - Internal server error (indicates issue retrieving dashboard tags from database) + +**Example error response** + +```http +HTTP/1.1 404 Not Found +Content-Type: application/json; charset=UTF-8 +Content-Length: 46 + +{ + "message": "Dashboard version not found" +} +``` + +JSON response body schema: + +- **message** - Message explaining the reason for the request failure. + +## Compare dashboard versions + +`POST /api/dashboards/calculate-diff` + +Compares two dashboard versions by calculating the JSON diff of them. + +**Example request**: + +```http +POST /api/dashboards/calculate-diff HTTP/1.1 +Accept: text/html +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + +{ + "base": { + "dashboardId": 1, + "version": 1 + }, + "new": { + "dashboardId": 1, + "version": 2 + }, + "diffType": "json" +} +``` + +JSON body schema: + +- **base** - an object representing the base dashboard version +- **new** - an object representing the new dashboard version +- **diffType** - the type of diff to return. Can be "json" or "basic". + +**Example response (JSON diff)**: + +```http +HTTP/1.1 200 OK +Content-Type: text/html; charset=UTF-8 + +

    + +

    +``` + +The response is a textual respresentation of the diff, with the dashboard values being in JSON, similar to the diffs seen on sites like GitHub or GitLab. + +Status Codes: + +- **200** - Ok +- **400** - Bad request (invalid JSON sent) +- **401** - Unauthorized +- **404** - Not found + +**Example response (basic diff)**: + +```http +HTTP/1.1 200 OK +Content-Type: text/html; charset=UTF-8 + +
    + +
    +``` + +The response here is a summary of the changes, derived from the diff between the two JSON objects. + +Status Codes: + +- **200** - OK +- **400** - Bad request (invalid JSON sent) +- **401** - Unauthorized +- **404** - Not found diff --git a/docs/sources/reference/dashboard_history.md b/docs/sources/reference/dashboard_history.md new file mode 100644 index 00000000000..e21022e31ec --- /dev/null +++ b/docs/sources/reference/dashboard_history.md @@ -0,0 +1,40 @@ ++++ +title = "Dashboard Version History" +keywords = ["grafana", "dashboard", "documentation", "version", "history"] +type = "docs" +[menu.docs] +name = "Dashboard Version History" +parent = "dashboard_features" +weight = 100 ++++ + + +# Dashboard Version History + +Whenever you save a version of your dashboard, a copy of that version is saved so that previous versions of your dashboard are never lost. A list of these versions is available by clicking the dashboard menu dropdown, and clicking "Version history". + + + +The dashboard version history feature lets you compare and restore to previously saved dashboard versions. + +## Comparing two dashboard versions + +To compare two dashboard versions, select the two versions from the list that you wish to compare. Once selected, the "Compare versions" button will become clickable. Click the button to view the diff between the two versions. + + + +Upon clicking the button, you'll be brought to the diff view. By default, you'll see a textual summary of the changes, like in the image below. + + + +If you want to view the diff of the raw JSON that represents your dashboard, you can do that as well by clicking the "JSON Diff" tab on the left. + +If you want to restore to the version you're diffing against, you can do so by clicking the "Restore to version " button in the top right. + +## Restoring to a previouslty saved dashboard version + +If you need to restore to a previosuly saved dashboard version, you can do so by either clicking the "Restore" button on the right of a row in the dashboard version list, or by clicking the "Restore to version " button appearing in the diff view. Clicking the button will bring up the following popup prompting you to confirm the restoration. + + + +After restoring to a previous version, a new version will be created containing the same exact data as the previous version, only with a different version number. This is indicated in the "Notes column" for the row in the new dashboard version. This is done simply to ensure your previous dashboard versions aren't affected by the change. From 20a2334c87778bf6b208c27b91c783e23eca1681 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 3 Jul 2017 14:47:54 +0200 Subject: [PATCH 36/64] docs: spelling --- docs/sources/reference/dashboard_history.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/sources/reference/dashboard_history.md b/docs/sources/reference/dashboard_history.md index e21022e31ec..0f91347ae65 100644 --- a/docs/sources/reference/dashboard_history.md +++ b/docs/sources/reference/dashboard_history.md @@ -29,12 +29,12 @@ Upon clicking the button, you'll be brought to the diff view. By default, you'll If you want to view the diff of the raw JSON that represents your dashboard, you can do that as well by clicking the "JSON Diff" tab on the left. -If you want to restore to the version you're diffing against, you can do so by clicking the "Restore to version " button in the top right. +If you want to restore to the version you are diffing against, you can do so by clicking the "Restore to version " button in the top right. -## Restoring to a previouslty saved dashboard version +## Restoring to a previously saved dashboard version -If you need to restore to a previosuly saved dashboard version, you can do so by either clicking the "Restore" button on the right of a row in the dashboard version list, or by clicking the "Restore to version " button appearing in the diff view. Clicking the button will bring up the following popup prompting you to confirm the restoration. +If you need to restore to a previously saved dashboard version, you can do so by either clicking the "Restore" button on the right of a row in the dashboard version list, or by clicking the "Restore to version " button appearing in the diff view. Clicking the button will bring up the following popup prompting you to confirm the restoration. -After restoring to a previous version, a new version will be created containing the same exact data as the previous version, only with a different version number. This is indicated in the "Notes column" for the row in the new dashboard version. This is done simply to ensure your previous dashboard versions aren't affected by the change. +After restoring to a previous version, a new version will be created containing the same exact data as the previous version, only with a different version number. This is indicated in the "Notes column" for the row in the new dashboard version. This is done simply to ensure your previous dashboard versions are not affected by the change. From a71423481bdfb24df0844a4fbd005abae79fb605 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 3 Jul 2017 18:07:51 +0200 Subject: [PATCH 37/64] changelog: update --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2e27dade77..4f6770a60e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,11 @@ Pull Request: [#8472](https://github.com/grafana/grafana/pull/8472) * **Elasticsearch**: Added filter aggregation label [#8420](https://github.com/grafana/grafana/pull/8420), thx [@tianzk](github.com/tianzk) * **Sensu**: Added option for source and handler [#8405](https://github.com/grafana/grafana/pull/8405), thx [@joemiller](github.com/joemiller) * **CSV**: Configurable csv export datetime format [#8058](https://github.com/grafana/grafana/issues/8058), thx [@cederigo](github.com/cederigo) +* **Table Panel**: Column style that preserves formatting/indentation (like pre tag) [#6617](https://github.com/grafana/grafana/issues/6617) + +## Minor Enhancements + +* **Elasticsearch**: Add option for result set size in raw_document [#3426](https://github.com/grafana/grafana/issues/3426) [#8527](https://github.com/grafana/grafana/pull/8527), thx [@mk-dhia](github.com/mk-dhia) # 4.3.2 (2017-05-31) From 3ae5f7c632718b95e3e6e26408075288ad99e669 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 3 Jul 2017 20:40:37 +0200 Subject: [PATCH 38/64] docs: built-in variables, $__interval Fixes #8344. Documents the $__interval, $__interval_ms and $timeFilter variables. --- docs/sources/reference/templating.md | 36 ++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md index 0405251f44d..c6dfa9902bd 100644 --- a/docs/sources/reference/templating.md +++ b/docs/sources/reference/templating.md @@ -141,6 +141,42 @@ Use the `Interval` type to create a variable that represents a time span (eg. `1 This variable type is useful as a parameter to group by time (for InfluxDB), Date histogram interval (for Elasticsearch) or as a *summarize* function parameter (for Graphite). +Example using the template variable `myinterval` of type `Interval` in a graphite function: + +``` +summarize($myinterval, sum, false) +``` + +## Global Built-in Variables + +Grafana has global built-in variables that can be used in expressions in the query editor. + +### The $__interval Variable + +This $__interval variable is similar to the `auto` interval variable that is described above. It can be used as a parameter to group by time (for InfluxDB), Date histogram interval (for Elasticsearch) or as a *summarize* function parameter (for Graphite). + +Grafana automatically calculates an interval that can be used to group by time in queries. When there are more data points than can be shown on a graph then queries can be made more efficient by grouping by a larger interval. It is more efficient to group by 1 day than by 10s when looking at 3 months of data and the graph will look the same and the query will be faster. The `$__interval` is calculated using the time range and the width of the graph (the number of pixels). + +Approximate Calculation: `(from - to) / resolution` + +For example, when the time range is 1 hour and the graph is full screen, then the interval might be calculated to `2m` - points are grouped in 2 minute intervals. If the time range is 6 months and the graph is full screen, then the interval might be `1d` (1 day) - points are grouped by day. + +In the InfluxDB data source, the legacy variable `$interval` is the same variable. `$__interval` should be used instead. + +The InfluxDB and Elasticsearch data sources have `Group by time interval` fields that are used as the minimum limit for the `$__interval` variable. + +### The $__interval_ms Variable + +This variable is the `$__interval` variable in milliseconds (and not a time interval formatted string). For example, if the `$__interval` is `20m` then the `$__interval_ms` is `1200000`. + +### The $timeFilter or $__timeFilter Variable + +The `$timeFilter` variable returns the currently selected time range as an expression. For example, the time range interval `Last 7 days` expression is `time > now() - 7d`. + +This is used in the WHERE clause for the InfluxDB data source. Grafana adds it automatically to InfluxDB queries when in Query Editor Mode. It has to be added manually in Text Editor Mode: `WHERE $timeFilter`. + +The `$__timeFilter` is used in the MySQL data source. + ## Repeating Panels Template variables can be very useful to dynamically change your queries across a whole dashboard. If you want From a5afd8152d03a10ba8adeb1475872d3aaf9c1472 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 3 Jul 2017 21:32:12 +0200 Subject: [PATCH 39/64] docs: small update --- docs/sources/reference/templating.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md index c6dfa9902bd..b2346903132 100644 --- a/docs/sources/reference/templating.md +++ b/docs/sources/reference/templating.md @@ -163,7 +163,7 @@ For example, when the time range is 1 hour and the graph is full screen, then th In the InfluxDB data source, the legacy variable `$interval` is the same variable. `$__interval` should be used instead. -The InfluxDB and Elasticsearch data sources have `Group by time interval` fields that are used as the minimum limit for the `$__interval` variable. +The InfluxDB and Elasticsearch data sources have `Group by time interval` fields that are used to hard code the interval or to set the minimum limit for the `$__interval` variable (by using the `>` syntax -> `>10m`). ### The $__interval_ms Variable From 109fd998edc26c167ea2a4213c780c1a7ac27eb7 Mon Sep 17 00:00:00 2001 From: Liang Jiameng Date: Tue, 4 Jul 2017 21:16:32 +0800 Subject: [PATCH 40/64] Add a new notifier : DingTalk (#8473) * add alerting notifier: DingDing * add alerting notifier: DingDing * add dingding unit test * add dingding unit test * delete debug code & format code style. * fix build failed: dingding_test.go --- pkg/metrics/metrics.go | 2 + pkg/services/alerting/notifiers/dingding.go | 90 +++++++++++++++++++ .../alerting/notifiers/dingding_test.go | 49 ++++++++++ 3 files changed, 141 insertions(+) create mode 100644 pkg/services/alerting/notifiers/dingding.go create mode 100644 pkg/services/alerting/notifiers/dingding_test.go diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index c23a53009a9..00354a00d03 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -44,6 +44,7 @@ var ( M_Alerting_Notification_Sent_Slack Counter M_Alerting_Notification_Sent_Email Counter M_Alerting_Notification_Sent_Webhook Counter + M_Alerting_Notification_Sent_DingDing Counter M_Alerting_Notification_Sent_PagerDuty Counter M_Alerting_Notification_Sent_LINE Counter M_Alerting_Notification_Sent_Victorops Counter @@ -116,6 +117,7 @@ func initMetricVars(settings *MetricSettings) { M_Alerting_Notification_Sent_Slack = RegCounter("alerting.notifications_sent", "type", "slack") M_Alerting_Notification_Sent_Email = RegCounter("alerting.notifications_sent", "type", "email") M_Alerting_Notification_Sent_Webhook = RegCounter("alerting.notifications_sent", "type", "webhook") + M_Alerting_Notification_Sent_DingDing = RegCounter("alerting.notifications_sent", "type", "dingding") M_Alerting_Notification_Sent_PagerDuty = RegCounter("alerting.notifications_sent", "type", "pagerduty") M_Alerting_Notification_Sent_Victorops = RegCounter("alerting.notifications_sent", "type", "victorops") M_Alerting_Notification_Sent_OpsGenie = RegCounter("alerting.notifications_sent", "type", "opsgenie") diff --git a/pkg/services/alerting/notifiers/dingding.go b/pkg/services/alerting/notifiers/dingding.go new file mode 100644 index 00000000000..ad5ccf554c3 --- /dev/null +++ b/pkg/services/alerting/notifiers/dingding.go @@ -0,0 +1,90 @@ +package notifiers + +import ( + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/metrics" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/alerting" +) + +func init() { + alerting.RegisterNotifier(&alerting.NotifierPlugin{ + Type: "dingding", + Name: "DingDing", + Description: "Sends HTTP POST request to DingDing", + Factory: NewDingDingNotifier, + OptionsTemplate: ` +

    DingDing settings

    +
    + Url + +
    + `, + }) + +} + +func NewDingDingNotifier(model *m.AlertNotification) (alerting.Notifier, error) { + url := model.Settings.Get("url").MustString() + if url == "" { + return nil, alerting.ValidationError{Reason: "Could not find url property in settings"} + } + + return &DingDingNotifier{ + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + Url: url, + log: log.New("alerting.notifier.dingding"), + }, nil +} + +type DingDingNotifier struct { + NotifierBase + Url string + log log.Logger +} + +func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error { + this.log.Info("Sending dingding") + metrics.M_Alerting_Notification_Sent_DingDing.Inc(1) + + messageUrl, err := evalContext.GetRuleUrl() + if err != nil { + this.log.Error("Failed to get messageUrl", "error", err, "dingding", this.Name) + messageUrl = "" + } + this.log.Info("messageUrl:" + messageUrl) + + message := evalContext.Rule.Message + picUrl := evalContext.ImagePublicUrl + title := evalContext.GetNotificationTitle() + + bodyJSON, err := simplejson.NewJson([]byte(`{ + "msgtype": "link", + "link": { + "text": "` + message + `", + "title": "` + title + `", + "picUrl": "` + picUrl + `", + "messageUrl": "` + messageUrl + `" + } + }`)) + + if err != nil { + this.log.Error("Failed to create Json data", "error", err, "dingding", this.Name) + } + + body, _ := bodyJSON.MarshalJSON() + + cmd := &m.SendWebhookSync{ + Url: this.Url, + Body: string(body), + } + + if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { + this.log.Error("Failed to send DingDing", "error", err, "dingding", this.Name) + return err + } + + return nil +} diff --git a/pkg/services/alerting/notifiers/dingding_test.go b/pkg/services/alerting/notifiers/dingding_test.go new file mode 100644 index 00000000000..3ca267dbf5b --- /dev/null +++ b/pkg/services/alerting/notifiers/dingding_test.go @@ -0,0 +1,49 @@ +package notifiers + +import ( + "testing" + + "github.com/grafana/grafana/pkg/components/simplejson" + m "github.com/grafana/grafana/pkg/models" + . "github.com/smartystreets/goconvey/convey" +) + +func TestDingDingNotifier(t *testing.T) { + Convey("Line notifier tests", t, func() { + Convey("empty settings should return error", func() { + json := `{ }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "dingding_testing", + Type: "dingding", + Settings: settingsJSON, + } + + _, err := NewDingDingNotifier(model) + So(err, ShouldNotBeNil) + + }) + Convey("settings should trigger incident", func() { + json := ` + { + "url": "https://www.google.com" + }` + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "dingding_testing", + Type: "dingding", + Settings: settingsJSON, + } + + not, err := NewDingDingNotifier(model) + notifier := not.(*DingDingNotifier) + + So(err, ShouldBeNil) + So(notifier.Name, ShouldEqual, "dingding_testing") + So(notifier.Type, ShouldEqual, "dingding") + So(notifier.Url, ShouldEqual, "https://www.google.com") + }) + + }) +} From 205be91a842cb2c8ad8dbcf88ed917018ae2b79c Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 4 Jul 2017 15:25:44 +0200 Subject: [PATCH 41/64] changelog: note for DingDing notifier --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f6770a60e5..98460916156 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Pull Request: [#8472](https://github.com/grafana/grafana/pull/8472) * **Sensu**: Added option for source and handler [#8405](https://github.com/grafana/grafana/pull/8405), thx [@joemiller](github.com/joemiller) * **CSV**: Configurable csv export datetime format [#8058](https://github.com/grafana/grafana/issues/8058), thx [@cederigo](github.com/cederigo) * **Table Panel**: Column style that preserves formatting/indentation (like pre tag) [#6617](https://github.com/grafana/grafana/issues/6617) +* **DingDing**: Add DingDing Alert Notifier [#8473](https://github.com/grafana/grafana/pull/8473) thx [@jiamliang](https://github.com/jiamliang) ## Minor Enhancements From f773a9b4c368a4ba5f0e7c22be82ca7adf24e6f1 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 4 Jul 2017 21:17:43 +0200 Subject: [PATCH 42/64] docs: small change --- docs/sources/alerting/notifications.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/notifications.md index cd1316eb479..3119390b275 100644 --- a/docs/sources/alerting/notifications.md +++ b/docs/sources/alerting/notifications.md @@ -112,6 +112,8 @@ Grafana also supports the following Notification Channels: - LINE +- DingDing + # Enable images in notifications {#external-image-store} Grafana can render the panel associated with the alert rule and include that in the notification. Most Notification Channels require that this image be publicly accessable (Slack and PagerDuty for example). In order to include images in alert notifications, Grafana can upload the image to an image store. It currently supports From 74093c700f850cf43e68adb79c499a1abdb0fb66 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 4 Jul 2017 16:33:37 +0200 Subject: [PATCH 43/64] api: adds no-cache header for GET requests Fixes #5356. Internet Explorer aggressively caches GET requests which means that all API calls fetching data are cached. This fix adds a Cache-Control header with the value no-cache to all GET requests to the API. --- pkg/api/http_server.go | 2 ++ pkg/middleware/middleware.go | 8 ++++++++ pkg/middleware/middleware_test.go | 11 +++++++++++ 3 files changed, 21 insertions(+) diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 0468b5cbe8a..4873062a933 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -171,6 +171,8 @@ func (hs *HttpServer) newMacaron() *macaron.Macaron { m.Use(middleware.ValidateHostHeader(setting.Domain)) } + m.Use(middleware.AddDefaultResponseHeaders()) + return m } diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 5aafe12d374..2a1d3e080a5 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -245,3 +245,11 @@ func (ctx *Context) HasHelpFlag(flag m.HelpFlags1) bool { func (ctx *Context) TimeRequest(timer metrics.Timer) { ctx.Data["perfmon.timer"] = timer } + +func AddDefaultResponseHeaders() macaron.Handler { + return func(ctx *Context) { + if ctx.IsApiRequest() && ctx.Req.Method == "GET" { + ctx.Resp.Header().Add("Cache-Control", "no-cache") + } + } +} diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index a1836a6744e..f18261e478d 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -30,6 +30,16 @@ func TestMiddlewareContext(t *testing.T) { So(sc.resp.Code, ShouldEqual, 200) }) + middlewareScenario("middleware should add Cache-Control header for GET requests to API", func(sc *scenarioContext) { + sc.fakeReq("GET", "/api/search").exec() + So(sc.resp.Header().Get("Cache-Control"), ShouldEqual, "no-cache") + }) + + middlewareScenario("middleware should not add Cache-Control header to for non-API GET requests", func(sc *scenarioContext) { + sc.fakeReq("GET", "/").exec() + So(sc.resp.Header().Get("Cache-Control"), ShouldBeEmpty) + }) + middlewareScenario("Non api request should init session", func(sc *scenarioContext) { sc.fakeReq("GET", "/").exec() So(sc.resp.Header().Get("Set-Cookie"), ShouldContainSubstring, "grafana_sess") @@ -327,6 +337,7 @@ func middlewareScenario(desc string, fn scenarioFunc) { startSessionGC = func() {} sc.m.Use(Sessioner(&session.Options{})) sc.m.Use(OrgRedirect()) + sc.m.Use(AddDefaultResponseHeaders()) sc.defaultHandler = func(c *Context) { sc.context = c From 1da98f5e1ec1c1cd26c9f648fd3ed4a208952b36 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 4 Jul 2017 21:48:39 +0200 Subject: [PATCH 44/64] Revert "Histogram fix (#8727)" This reverts commit 8634c9d457d6e624d762a476e817a401bebcf442. --- .../app/plugins/panel/graph/axes_editor.html | 6 ---- public/app/plugins/panel/graph/axes_editor.ts | 6 ---- public/app/plugins/panel/graph/graph.ts | 29 ++++------------- public/app/plugins/panel/graph/histogram.ts | 11 ++----- public/app/plugins/panel/graph/module.ts | 3 +- .../panel/graph/specs/histogram_specs.ts | 32 +++---------------- 6 files changed, 14 insertions(+), 73 deletions(-) diff --git a/public/app/plugins/panel/graph/axes_editor.html b/public/app/plugins/panel/graph/axes_editor.html index 1e577fe392e..b0ab759bf18 100644 --- a/public/app/plugins/panel/graph/axes_editor.html +++ b/public/app/plugins/panel/graph/axes_editor.html @@ -67,12 +67,6 @@
    -
    - -
    - -
    -
    diff --git a/public/app/plugins/panel/graph/axes_editor.ts b/public/app/plugins/panel/graph/axes_editor.ts index 155265e5987..adb3d1d6706 100644 --- a/public/app/plugins/panel/graph/axes_editor.ts +++ b/public/app/plugins/panel/graph/axes_editor.ts @@ -10,7 +10,6 @@ export class AxesEditorCtrl { xAxisModes: any; xAxisStatOptions: any; xNameSegment: any; - histogramValues: any; /** @ngInject **/ constructor(private $scope, private $q) { @@ -35,11 +34,6 @@ export class AxesEditorCtrl { // 'Data field': 'field', }; - this.histogramValues = { - 'Percent': 'percent', - 'Count': 'count' - }; - this.xAxisStatOptions = [ {text: 'Avg', value: 'avg'}, {text: 'Min', value: 'min'}, diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 8c928fb6a41..16d259c210e 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -312,13 +312,10 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv, popoverSrv) { let histMax = _.max(_.map(data, s => s.stats.max)); let ticks = panel.xaxis.buckets || panelWidth / 50; bucketSize = tickStep(histMin, histMax, ticks); + let histogram = convertValuesToHistogram(values, bucketSize); - let normalize = panel.xaxis.histogramValue === 'percent'; - let histogram = convertValuesToHistogram(values, bucketSize, normalize); - - let seriesLabel = panel.xaxis.histogramValue || "count"; data[0].data = histogram; - data[0].alias = data[0].label = data[0].id = seriesLabel; + data[0].alias = data[0].label = data[0].id = "count"; data = [data[0]]; options.series.bars.barWidth = bucketSize * 0.8; @@ -425,32 +422,21 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv, popoverSrv) { function addXHistogramAxis(options, bucketSize) { let ticks, min, max; - let defaultTicks = panelWidth / 50; if (data.length && bucketSize) { ticks = _.map(data[0].data, point => point[0]); - min = _.min(ticks); - max = _.max(ticks); - - // Adjust tick step - let tickStep = bucketSize; - let ticks_num = Math.floor((max - min) / tickStep); - while (ticks_num > defaultTicks) { - tickStep = tickStep * 2; - ticks_num = Math.ceil((max - min) / tickStep); - } // Expand ticks for pretty view - min = Math.floor(min / tickStep) * tickStep; - max = Math.ceil(max / tickStep) * tickStep; + min = Math.max(0, _.min(ticks) - bucketSize); + max = _.max(ticks) + bucketSize; ticks = []; - for (let i = min; i <= max; i += tickStep) { + for (let i = min; i <= max; i += bucketSize) { ticks.push(i); } } else { // Set defaults if no data - ticks = defaultTicks / 2; + ticks = panelWidth / 100; min = 0; max = 1; } @@ -464,9 +450,6 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv, popoverSrv) { label: "Histogram", ticks: ticks }; - - // Use 'short' format for histogram values - configureAxisMode(options.xaxis, 'short'); } function addXTableAxis(options) { diff --git a/public/app/plugins/panel/graph/histogram.ts b/public/app/plugins/panel/graph/histogram.ts index efb13c546f0..e6ad7ebb0f6 100644 --- a/public/app/plugins/panel/graph/histogram.ts +++ b/public/app/plugins/panel/graph/histogram.ts @@ -26,7 +26,7 @@ export function getSeriesValues(data: any): number[] { * @param values * @param bucketSize */ -export function convertValuesToHistogram(values: number[], bucketSize: number, normalize = false): any[] { +export function convertValuesToHistogram(values: number[], bucketSize: number): any[] { let histogram = {}; for (let i = 0; i < values.length; i++) { @@ -38,16 +38,9 @@ export function convertValuesToHistogram(values: number[], bucketSize: number, n } } - let histogam_series = _.map(histogram, (count, bound) => { - if (normalize && values.length) { - return [Number(bound), count / values.length]; - } - + return _.map(histogram, (count, bound) => { return [Number(bound), count]; }); - - // Sort by Y axis values - return _.sortBy(histogam_series, point => point[0]); } function getBucketBound(value: number, bucketSize: number): number { diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index fed739a60d7..e9ca8c5c4e2 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -58,8 +58,7 @@ class GraphCtrl extends MetricsPanelCtrl { mode: 'time', name: null, values: [], - buckets: null, - histogramValue: 'percent' + buckets: null }, // show/hide lines lines : true, diff --git a/public/app/plugins/panel/graph/specs/histogram_specs.ts b/public/app/plugins/panel/graph/specs/histogram_specs.ts index 4d5ad02aec9..71b3def8d1d 100644 --- a/public/app/plugins/panel/graph/specs/histogram_specs.ts +++ b/public/app/plugins/panel/graph/specs/histogram_specs.ts @@ -1,6 +1,7 @@ /// -import _ from 'lodash'; + import { describe, beforeEach, it, expect } from '../../../../../test/lib/common'; + import { convertValuesToHistogram, getSeriesValues } from '../histogram'; describe('Graph Histogam Converter', function () { @@ -10,13 +11,13 @@ describe('Graph Histogam Converter', function () { let bucketSize = 10; beforeEach(() => { - values = [1, 2, 10, 11, 17, 20, 29, 30, 31, 33]; + values = [1, 2, 10, 11, 17, 20, 29]; }); it('Should convert to series-like array', () => { bucketSize = 10; let expected = [ - [0, 2], [10, 3], [20, 2], [30, 3] + [0, 2], [10, 3], [20, 2] ]; let histogram = convertValuesToHistogram(values, bucketSize); @@ -26,35 +27,12 @@ describe('Graph Histogam Converter', function () { it('Should not add empty buckets', () => { bucketSize = 5; let expected = [ - [0, 2], [10, 2], [15, 1], [20, 1], [25, 1], [30, 3] + [0, 2], [10, 2], [15, 1], [20, 1], [25, 1] ]; let histogram = convertValuesToHistogram(values, bucketSize); expect(histogram).to.eql(expected); }); - - it('Should normalize values', () => { - bucketSize = 5; - let normalize = true; - let expected = [ - [0, 0.2], [10, 0.2], [15, 0.1], [20, 0.1], [25, 0.1], [30, 0.3] - ]; - - let histogram = convertValuesToHistogram(values, bucketSize, normalize); - expect(histogram).to.eql(expected); - }); - - it('Sum of normalized values should be 1', () => { - bucketSize = 5; - let normalize = true; - let expected = [ - [0, 0.2], [10, 0.2], [15, 0.1], [20, 0.1], [25, 0.1], [30, 0.3] - ]; - - let histogram = convertValuesToHistogram(values, bucketSize, normalize); - let sum = _.reduce(histogram, (sum, point) => sum + point[1], 0); - expect(sum).to.eql(1); - }); }); describe('Series to values converter', () => { From c1c1bcb874a9ec639fdea2f30656f2f1fbf69c54 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 27 Jun 2017 17:42:21 +0300 Subject: [PATCH 45/64] histogram: don't cut negative values, issue #8628 --- public/app/plugins/panel/graph/graph.ts | 2 +- public/app/plugins/panel/graph/histogram.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 16d259c210e..c8d8fbfb25f 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -427,7 +427,7 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv, popoverSrv) { ticks = _.map(data[0].data, point => point[0]); // Expand ticks for pretty view - min = Math.max(0, _.min(ticks) - bucketSize); + min = _.min(ticks) - bucketSize; max = _.max(ticks) + bucketSize; ticks = []; diff --git a/public/app/plugins/panel/graph/histogram.ts b/public/app/plugins/panel/graph/histogram.ts index e6ad7ebb0f6..c60782942f2 100644 --- a/public/app/plugins/panel/graph/histogram.ts +++ b/public/app/plugins/panel/graph/histogram.ts @@ -38,9 +38,12 @@ export function convertValuesToHistogram(values: number[], bucketSize: number): } } - return _.map(histogram, (count, bound) => { + let histogam_series = _.map(histogram, (count, bound) => { return [Number(bound), count]; }); + + // Sort by Y axis values + return _.sortBy(histogam_series, point => point[0]); } function getBucketBound(value: number, bucketSize: number): number { From 934c0fea6f10d8de61e76e77e66be47cce62399f Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 28 Jun 2017 11:03:36 +0300 Subject: [PATCH 46/64] histogram: improved ticks rendering --- public/app/plugins/panel/graph/graph.ts | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index c8d8fbfb25f..ac75aa28754 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -422,21 +422,32 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv, popoverSrv) { function addXHistogramAxis(options, bucketSize) { let ticks, min, max; + let defaultTicks = panelWidth / 50; if (data.length && bucketSize) { ticks = _.map(data[0].data, point => point[0]); + min = _.min(ticks); + max = _.max(ticks); + + // Adjust tick step + let tickStep = bucketSize; + let ticks_num = Math.floor((max - min) / tickStep); + while (ticks_num > defaultTicks) { + tickStep = tickStep * 2; + ticks_num = Math.ceil((max - min) / tickStep); + } // Expand ticks for pretty view - min = _.min(ticks) - bucketSize; - max = _.max(ticks) + bucketSize; + min = Math.floor(min / tickStep) * tickStep; + max = Math.ceil(max / tickStep) * tickStep; ticks = []; - for (let i = min; i <= max; i += bucketSize) { + for (let i = min; i <= max; i += tickStep) { ticks.push(i); } } else { // Set defaults if no data - ticks = panelWidth / 100; + ticks = defaultTicks / 2; min = 0; max = 1; } @@ -450,6 +461,9 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv, popoverSrv) { label: "Histogram", ticks: ticks }; + + // Use 'short' format for histogram values + configureAxisMode(options.xaxis, 'short'); } function addXTableAxis(options) { From d20455ab5fe9876b93ef68d68f46ebd4256a1d51 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 4 Jul 2017 22:45:33 +0200 Subject: [PATCH 47/64] changelog: note for histogram fix --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98460916156..160c83b8f3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ Pull Request: [#8472](https://github.com/grafana/grafana/pull/8472) * **Elasticsearch**: Add option for result set size in raw_document [#3426](https://github.com/grafana/grafana/issues/3426) [#8527](https://github.com/grafana/grafana/pull/8527), thx [@mk-dhia](github.com/mk-dhia) +## Bug Fixes + +* **Graph**: Bug fix for negative values in histogram mode [#8628](https://github.com/grafana/grafana/issues/8628) + # 4.3.2 (2017-05-31) ## Bug fixes From 1940b33dc129ec43937e0f2ba27c9839220f3489 Mon Sep 17 00:00:00 2001 From: Jesse White Date: Tue, 4 Jul 2017 16:55:13 -0400 Subject: [PATCH 48/64] fix: handling of http errors without any data (#8777) --- public/app/core/services/backend_srv.ts | 2 +- public/test/specs/backend_srv-specs.js | 33 +++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 public/test/specs/backend_srv-specs.js diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index f4c32ab82b1..bffdfa05914 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -179,7 +179,7 @@ export class BackendSrv { } // for Prometheus - if (!err.data.message && _.isString(err.data.error)) { + if (err.data && !err.data.message && _.isString(err.data.error)) { err.data.message = err.data.error; } diff --git a/public/test/specs/backend_srv-specs.js b/public/test/specs/backend_srv-specs.js new file mode 100644 index 00000000000..a151fec3f14 --- /dev/null +++ b/public/test/specs/backend_srv-specs.js @@ -0,0 +1,33 @@ +define([ + 'app/core/config', + 'app/core/services/backend_srv' +], function() { + 'use strict'; + + describe('backend_srv', function() { + var _backendSrv; + var _http; + var _httpBackend; + + beforeEach(module('grafana.core')); + beforeEach(module('grafana.services')); + beforeEach(inject(function ($httpBackend, $http, backendSrv) { + _httpBackend = $httpBackend; + _http = $http; + _backendSrv = backendSrv; + })); + + describe('when handling errors', function() { + it('should return the http status code', function(done) { + _httpBackend.whenGET('gateway-error').respond(502); + _backendSrv.datasourceRequest({ + url: 'gateway-error' + }).catch(function(err) { + expect(err.status).to.be(502); + done(); + }); + _httpBackend.flush(); + }); + }); + }); +}); From 35830571551d956623d1a334d6c4942758edc5f3 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 4 Jul 2017 23:42:22 +0200 Subject: [PATCH 49/64] release: v4.4.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6524892f20a..6b410f041fb 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "company": "Coding Instinct AB" }, "name": "grafana", - "version": "4.4.0-pre1", + "version": "4.4.0", "repository": { "type": "git", "url": "http://github.com/grafana/grafana.git" From 36a1ab48c52d4a33464870ccbb40da098c8244a4 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 5 Jul 2017 01:15:42 +0200 Subject: [PATCH 50/64] packaging: updates for v4.4.0 --- README.md | 1 + docs/sources/guides/whats-new-in-v4-4.md | 50 ++++++++++++++++++++++++ docs/sources/installation/debian.md | 6 +-- docs/sources/installation/rpm.md | 10 ++--- docs/sources/installation/windows.md | 2 +- packaging/publish/publish_both.sh | 2 +- 6 files changed, 61 insertions(+), 10 deletions(-) create mode 100644 docs/sources/guides/whats-new-in-v4-4.md diff --git a/README.md b/README.md index 9d2aabebbf3..41f777d5dec 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Graphite, Elasticsearch, OpenTSDB, Prometheus and InfluxDB. - [What's New in Grafana 4.1](http://docs.grafana.org/guides/whats-new-in-v4-1/) - [What's New in Grafana 4.2](http://docs.grafana.org/guides/whats-new-in-v4-2/) - [What's New in Grafana 4.3](http://docs.grafana.org/guides/whats-new-in-v4-3/) +- [What's New in Grafana 4.4](http://docs.grafana.org/guides/whats-new-in-v4-4/) ## Features diff --git a/docs/sources/guides/whats-new-in-v4-4.md b/docs/sources/guides/whats-new-in-v4-4.md new file mode 100644 index 00000000000..c091a1f7ef1 --- /dev/null +++ b/docs/sources/guides/whats-new-in-v4-4.md @@ -0,0 +1,50 @@ ++++ +title = "What's New in Grafana v4.4" +description = "Feature & improvement highlights for Grafana v4.4" +keywords = ["grafana", "new", "documentation", "4.4.0"] +type = "docs" +[menu.docs] +name = "Version 4.4" +identifier = "v4.4" +parent = "whatsnew" +weight = -2 ++++ + +## What's New in Grafana v4.4 + +Grafana v4.4 is now [available for download](https://grafana.com/grafana/download/4.4.0). + +**Highlights**: + +- Dashboard History - version control for dashboards. + +## New Features + +**Dashboard History**: View dashboard version history, compare any two versions (summary & json diffs), restore to old version. This big feature +was contributed by **Walmart Labs**. Big thanks to them for this massive contribution! +Initial feature request: [#4638](https://github.com/grafana/grafana/issues/4638) +Pull Request: [#8472](https://github.com/grafana/grafana/pull/8472) + +## Enhancements +* **Elasticsearch**: Added filter aggregation label [#8420](https://github.com/grafana/grafana/pull/8420), thx [@tianzk](github.com/tianzk) +* **Sensu**: Added option for source and handler [#8405](https://github.com/grafana/grafana/pull/8405), thx [@joemiller](github.com/joemiller) +* **CSV**: Configurable csv export datetime format [#8058](https://github.com/grafana/grafana/issues/8058), thx [@cederigo](github.com/cederigo) +* **Table Panel**: Column style that preserves formatting/indentation (like pre tag) [#6617](https://github.com/grafana/grafana/issues/6617) +* **DingDing**: Add DingDing Alert Notifier [#8473](https://github.com/grafana/grafana/pull/8473) thx [@jiamliang](https://github.com/jiamliang) + +## Minor Enhancements + +* **Elasticsearch**: Add option for result set size in raw_document [#3426](https://github.com/grafana/grafana/issues/3426) [#8527](https://github.com/grafana/grafana/pull/8527), thx [@mk-dhia](github.com/mk-dhia) + +## Bug Fixes + +* **Graph**: Bug fix for negative values in histogram mode [#8628](https://github.com/grafana/grafana/issues/8628) + +## Download + +Head to the [v4.4 download page](https://grafana.com/grafana/download) for download links & instructions. + +## Thanks + +A big thanks to all the Grafana users who contribute by submitting PRs, bug reports, helping out on our [community site](https://community.grafana.com/) and providing feedback! + diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index 5fde442afbf..fb21f76b599 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -15,7 +15,7 @@ weight = 1 Description | Download ------------ | ------------- -Stable for Debian-based Linux | [grafana_4.3.1_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.3.1_amd64.deb) +Stable for Debian-based Linux | [grafana_4.4.0_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.4.0_amd64.deb) Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing installation. @@ -23,9 +23,9 @@ installation. ## Install Stable ```bash -wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.3.1_amd64.deb +wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.4.0_amd64.deb sudo apt-get install -y adduser libfontconfig -sudo dpkg -i grafana_4.3.1_amd64.deb +sudo dpkg -i grafana_4.4.0_amd64.deb ```