From 1e4eb0105942debe27079174bd284bad5b1824f4 Mon Sep 17 00:00:00 2001 From: bigbenhur Date: Fri, 3 Jun 2016 15:54:14 +0200 Subject: [PATCH 001/301] Support auto grid min/max when using log scale, Issue #3090 --- public/app/plugins/panel/graph/graph.js | 58 ++++++++++--------- .../plugins/panel/graph/specs/graph_specs.ts | 6 +- 2 files changed, 34 insertions(+), 30 deletions(-) diff --git a/public/app/plugins/panel/graph/graph.js b/public/app/plugins/panel/graph/graph.js index 86ad3b5f025..8019a51354b 100755 --- a/public/app/plugins/panel/graph/graph.js +++ b/public/app/plugins/panel/graph/graph.js @@ -373,42 +373,46 @@ function (angular, $, moment, _, kbn, GraphTooltip) { if (axis.logBase === 1) { return; } + if (axis.min < Number.MIN_VALUE) { + axis.min = null; + } var series, i; - var max = axis.max; + var max = axis.max, min = axis.min; - if (max === null) { - for (i = 0; i < data.length; i++) { - series = data[i]; - if (series.yaxis === axis.index) { - if (max < series.stats.max) { - max = series.stats.max; - } + for (i = 0; i < data.length; i++) { + series = data[i]; + if (series.yaxis === axis.index) { + if (max === null || max < series.stats.max) { + max = series.stats.max; + } + if (min === null || min > series.stats.min) { + min = series.stats.min; } } - if (max === void 0) { - max = Number.MAX_VALUE; - } + } + if (max === null && min === null) { + max = Math.pow(axis.logBase,+2); + min = Math.pow(axis.logBase,-2); + } else if (max === null) { + max = min*Math.pow(axis.logBase,+4); + } else if (min === null) { + min = max*Math.pow(axis.logBase,-4); } - axis.min = axis.min !== null ? axis.min : 0; - axis.ticks = [0, 1]; - var nextTick = 1; + axis.transform = function(v) { return Math.log(v) / Math.log(axis.logBase); }; + axis.inverseTransform = function (v) { return Math.pow(axis.logBase,v); }; - while (true) { - nextTick = nextTick * axis.logBase; + min = axis.inverseTransform(Math.floor(axis.transform(min))); + max = axis.inverseTransform(Math.ceil(axis.transform(max))); + + axis.min = axis.min !== null ? axis.min : min; + axis.max = axis.max !== null ? axis.max : max; + + axis.ticks = []; + var nextTick; + for (nextTick = min; nextTick <= max; nextTick *= axis.logBase) { axis.ticks.push(nextTick); - if (nextTick > max) { - break; - } - } - - if (axis.logBase === 10) { - axis.transform = function(v) { return Math.log(v+0.1); }; - axis.inverseTransform = function (v) { return Math.pow(10,v); }; - } else { - axis.transform = function(v) { return Math.log(v+0.1) / Math.log(axis.logBase); }; - axis.inverseTransform = function (v) { return Math.pow(axis.logBase,v); }; } } diff --git a/public/app/plugins/panel/graph/specs/graph_specs.ts b/public/app/plugins/panel/graph/specs/graph_specs.ts index b9c9362e5de..b383a31a976 100644 --- a/public/app/plugins/panel/graph/specs/graph_specs.ts +++ b/public/app/plugins/panel/graph/specs/graph_specs.ts @@ -172,9 +172,9 @@ describe('grafanaGraph', function() { it('should apply axis transform and ticks', function() { var axis = ctx.plotOptions.yaxes[0]; - expect(axis.transform(100)).to.be(Math.log(100+0.1)); - expect(axis.ticks[0]).to.be(0); - expect(axis.ticks[1]).to.be(1); + expect(axis.transform(100)).to.be(Math.log(100)/Math.log(10)); + expect(axis.ticks[0]).to.be(0.01); + expect(axis.ticks[1]).to.be(0.1); }); }); From 29b9d17faa05bf804eb9667810a29ab94d089885 Mon Sep 17 00:00:00 2001 From: bigbenhur Date: Fri, 3 Jun 2016 22:30:38 +0200 Subject: [PATCH 002/301] fix crash due to zero or negative data values in graph with log scale --- public/app/core/time_series2.ts | 6 ++++++ public/app/plugins/panel/graph/graph.js | 7 ++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/public/app/core/time_series2.ts b/public/app/core/time_series2.ts index dfae26fb48b..d01fe1156b3 100644 --- a/public/app/core/time_series2.ts +++ b/public/app/core/time_series2.ts @@ -97,6 +97,7 @@ export default class TimeSeries { this.stats.total = 0; this.stats.max = -Number.MAX_VALUE; this.stats.min = Number.MAX_VALUE; + this.stats.logmin = Number.MAX_VALUE; this.stats.avg = null; this.stats.current = null; this.allIsNull = true; @@ -133,6 +134,11 @@ export default class TimeSeries { if (currentValue < this.stats.min) { this.stats.min = currentValue; } + + if (currentValue < this.stats.logmin && currentValue > 0) { + this.stats.logmin = currentValue; + } + } if (currentValue !== 0) { diff --git a/public/app/plugins/panel/graph/graph.js b/public/app/plugins/panel/graph/graph.js index 8019a51354b..00fec42cd6f 100755 --- a/public/app/plugins/panel/graph/graph.js +++ b/public/app/plugins/panel/graph/graph.js @@ -386,11 +386,12 @@ function (angular, $, moment, _, kbn, GraphTooltip) { if (max === null || max < series.stats.max) { max = series.stats.max; } - if (min === null || min > series.stats.min) { - min = series.stats.min; + if (min === null || min > series.stats.logmin) { + min = series.stats.logmin; } } } + if (max === null && min === null) { max = Math.pow(axis.logBase,+2); min = Math.pow(axis.logBase,-2); @@ -400,7 +401,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) { min = max*Math.pow(axis.logBase,-4); } - axis.transform = function(v) { return Math.log(v) / Math.log(axis.logBase); }; + axis.transform = function(v) { return (v < Number.MIN_VALUE) ? null : Math.log(v) / Math.log(axis.logBase); }; axis.inverseTransform = function (v) { return Math.pow(axis.logBase,v); }; min = axis.inverseTransform(Math.floor(axis.transform(min))); From ef623846c1d2d5e610d738bda49127616b5ed59d Mon Sep 17 00:00:00 2001 From: bigbenhur Date: Mon, 6 Jun 2016 10:33:35 +0200 Subject: [PATCH 003/301] further tests for log-autoscale and log-fixedscale; only generate visible ticks; minor changes to prevent crashes due to bad user input --- public/app/plugins/panel/graph/graph.js | 38 ++++++++++++------- .../plugins/panel/graph/specs/graph_specs.ts | 35 ++++++++++++++--- 2 files changed, 54 insertions(+), 19 deletions(-) diff --git a/public/app/plugins/panel/graph/graph.js b/public/app/plugins/panel/graph/graph.js index 00fec42cd6f..a74997d6651 100755 --- a/public/app/plugins/panel/graph/graph.js +++ b/public/app/plugins/panel/graph/graph.js @@ -376,6 +376,9 @@ function (angular, $, moment, _, kbn, GraphTooltip) { if (axis.min < Number.MIN_VALUE) { axis.min = null; } + if (axis.max < Number.MIN_VALUE) { + axis.max = null; + } var series, i; var max = axis.max, min = axis.min; @@ -392,23 +395,32 @@ function (angular, $, moment, _, kbn, GraphTooltip) { } } - if (max === null && min === null) { - max = Math.pow(axis.logBase,+2); - min = Math.pow(axis.logBase,-2); - } else if (max === null) { - max = min*Math.pow(axis.logBase,+4); - } else if (min === null) { - min = max*Math.pow(axis.logBase,-4); - } - axis.transform = function(v) { return (v < Number.MIN_VALUE) ? null : Math.log(v) / Math.log(axis.logBase); }; axis.inverseTransform = function (v) { return Math.pow(axis.logBase,v); }; - min = axis.inverseTransform(Math.floor(axis.transform(min))); - max = axis.inverseTransform(Math.ceil(axis.transform(max))); + if (max === null && min === null) { + max = axis.inverseTransform(+2); + min = axis.inverseTransform(-2); + } else if (max === null) { + max = min*axis.inverseTransform(+4); + } else if (min === null) { + min = max*axis.inverseTransform(-4); + } - axis.min = axis.min !== null ? axis.min : min; - axis.max = axis.max !== null ? axis.max : max; + if (axis.min !== null) { + min = axis.inverseTransform(Math.ceil(axis.transform(axis.min))); + } else { + min = axis.min = axis.inverseTransform(Math.floor(axis.transform(min))); + } + if (axis.max !== null) { + max = axis.inverseTransform(Math.floor(axis.transform(axis.max))); + } else { + max = axis.max = axis.inverseTransform(Math.ceil(axis.transform(max))); + } + + if (min < Number.MIN_VALUE || max < Number.MIN_VALUE) { + return; + } axis.ticks = []; var nextTick; diff --git a/public/app/plugins/panel/graph/specs/graph_specs.ts b/public/app/plugins/panel/graph/specs/graph_specs.ts index b383a31a976..339b14edb3d 100644 --- a/public/app/plugins/panel/graph/specs/graph_specs.ts +++ b/public/app/plugins/panel/graph/specs/graph_specs.ts @@ -166,15 +166,38 @@ describe('grafanaGraph', function() { }); graphScenario('when logBase is log 10', function(ctx) { - ctx.setup(function(ctrl) { + ctx.setup(function(ctrl, data) { ctrl.panel.yaxes[0].logBase = 10; + data[0] = new TimeSeries({ + datapoints: [[2000,1],[0.002,2],[0,3],[-1,4]], + alias: 'seriesAutoscale', + }); + data[0].yaxis = 1; + ctrl.panel.yaxes[1].logBase = 10; + ctrl.panel.yaxes[1].min = 0.05; + ctrl.panel.yaxes[1].max = 1500; + data[1] = new TimeSeries({ + datapoints: [[2000,1],[0.002,2],[0,3],[-1,4]], + alias: 'seriesFixedscale', + }); + data[1].yaxis = 2; }); - it('should apply axis transform and ticks', function() { - var axis = ctx.plotOptions.yaxes[0]; - expect(axis.transform(100)).to.be(Math.log(100)/Math.log(10)); - expect(axis.ticks[0]).to.be(0.01); - expect(axis.ticks[1]).to.be(0.1); + it('should apply axis transform, autoscaling (if necessary) and ticks', function() { + var axisAutoscale = ctx.plotOptions.yaxes[0]; + expect(axisAutoscale.transform(100)).to.be(2); + expect(axisAutoscale.inverseTransform(-3)).to.be(0.001); + expect(axisAutoscale.min).to.be(0.001); + expect(axisAutoscale.max).to.be(10000); + expect(axisAutoscale.ticks.length).to.be(8); + expect(axisAutoscale.ticks[0]).to.be(0.001); + expect(axisAutoscale.ticks[7]).to.be(10000); + var axisFixedscale = ctx.plotOptions.yaxes[1]; + expect(axisFixedscale.min).to.be(0.05); + expect(axisFixedscale.max).to.be(1500); + expect(axisFixedscale.ticks.length).to.be(5); + expect(axisFixedscale.ticks[0]).to.be(0.1); + expect(axisFixedscale.ticks[4]).to.be(1000); }); }); From 513fcdeeb8bb3c9889fc364843a8e1905aa2780d Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 31 Jan 2017 10:10:25 +0100 Subject: [PATCH 004/301] tech(build): prepare for tag builds --- scripts/build/build.sh | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/scripts/build/build.sh b/scripts/build/build.sh index 26acabe6304..d16a5edec69 100755 --- a/scripts/build/build.sh +++ b/scripts/build/build.sh @@ -24,11 +24,11 @@ fi echo "current dir: $(pwd)" if [ "$CIRCLE_TAG" != "" ]; then - echo "Building incremental build for master" - go run build.go build -else - echo "Building a release" + echo "Building a release from tag $CIRCLE_TAG" go run build.go -includeBuildNumber=false build +else + echo "Building incremental build for $CIRCLE_BRANCH" + go run build.go build fi yarn install --pure-lockfile @@ -41,11 +41,11 @@ gem install fpm -v 1.4 echo "current dir: $(pwd)" if [ "$CIRCLE_TAG" != "" ]; then - echo "Building incremental build for master" - go run build.go package latest -else - echo "Building a release" + echo "Packaging a release from tag $CIRCLE_TAG" go run build.go -includeBuildNumber=false package latest +else + echo "Packaging incremental build for $CIRCLE_BRANCH" + go run build.go package latest fi cp dist/* /tmp/dist/ From 0e892e9f86349c8431d89b7be52c14758f208ab7 Mon Sep 17 00:00:00 2001 From: Stanislav Vetlovskiy Date: Tue, 31 Jan 2017 16:47:14 +0300 Subject: [PATCH 005/301] Add image url to telegram alert notifier If public image url of alert is exist add it to the telegram message --- pkg/services/alerting/notifiers/telegram.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/services/alerting/notifiers/telegram.go b/pkg/services/alerting/notifiers/telegram.go index 0c3e3bd3c61..f70d299bc6c 100644 --- a/pkg/services/alerting/notifiers/telegram.go +++ b/pkg/services/alerting/notifiers/telegram.go @@ -93,6 +93,9 @@ func (this *TelegramNotifier) Notify(evalContext *alerting.EvalContext) error { if err == nil { message = message + fmt.Sprintf("URL: %s\n", ruleUrl) } + if evalContext.ImagePublicUrl != "" { + message = message + fmt.Sprintf("Image: %s\n", evalContext.ImagePublicUrl) + } bodyJSON.Set("text", message) url := fmt.Sprintf(telegeramApiUrl, this.BotToken, "sendMessage") From 0c46def1a0ad38270db261767f1a016676a3bf24 Mon Sep 17 00:00:00 2001 From: Kevin Bowling Date: Tue, 31 Jan 2017 06:54:53 -0700 Subject: [PATCH 006/301] Add an idiomatic plugin path for FreeBSD (#7410) --- pkg/cmd/grafana-cli/utils/grafana_path.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/cmd/grafana-cli/utils/grafana_path.go b/pkg/cmd/grafana-cli/utils/grafana_path.go index fa6dc46d783..afb622bbb93 100644 --- a/pkg/cmd/grafana-cli/utils/grafana_path.go +++ b/pkg/cmd/grafana-cli/utils/grafana_path.go @@ -40,6 +40,8 @@ func returnOsDefault(currentOs string) string { return "../data/plugins" case "darwin": return "/usr/local/var/lib/grafana/plugins" + case "freebsd": + return "/var/db/grafana/plugins" default: //"linux" return "/var/lib/grafana/plugins" } From 91999851cba8630ab3985a6cd80fa1a05394f5aa Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Tue, 31 Jan 2017 23:21:53 +0900 Subject: [PATCH 007/301] support regex expansion in templating "Regex" field (#6565) --- public/app/features/templating/query_variable.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/templating/query_variable.ts b/public/app/features/templating/query_variable.ts index 92b150f2f15..7dfcb854194 100644 --- a/public/app/features/templating/query_variable.ts +++ b/public/app/features/templating/query_variable.ts @@ -136,7 +136,7 @@ export class QueryVariable implements Variable { options = []; if (this.regex) { - regex = kbn.stringToJsRegex(this.templateSrv.replace(this.regex)); + regex = kbn.stringToJsRegex(this.templateSrv.replace(this.regex, {}, 'regex')); } for (i = 0; i < metricNames.length; i++) { From b600e1c50bfb5f0c9c6680fb1d5372499aeab0f7 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 31 Jan 2017 15:22:38 +0100 Subject: [PATCH 008/301] docs(changelog): adds note about closing #7417 --- CHANGELOG.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd9cb6d797e..943bb32b07d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,10 @@ * **SingleStat**: Implements diff aggregation method for singlestat [#7234](https://github.com/grafana/grafana/issues/7234), thx [@oliverpool](https://github.com/oliverpool) * **Dataproxy**: Added setting to enable more verbose logging in dataproxy [#7209](https://github.com/grafana/grafana/pull/7209), thx [@Ricky-N](https://github.com/Ricky-N) * **Alerting**: Better information about why an alert triggered [#7035](https://github.com/grafana/grafana/issues/7035) -* **LINE**: Add LINE as alerting notification channel [#7301](https://github.com/grafana/grafana/pull/7301), thx [#huydx](https://github.com/huydx) -* **Elasticsearch**: Support for Min Doc Count options in Terms aggregation [#7324](https://github.com/grafana/grafana/pull/7324), thx [#lpic10](https://github.com/lpic10) -* **Elasticsearch**: Term aggregation limit can now be changed in template queries [#7112](https://github.com/grafana/grafana/issues/7112), thx [#FFalcon](https://github.com/FFalcon) +* **LINE**: Add LINE as alerting notification channel [#7301](https://github.com/grafana/grafana/pull/7301), thx [@huydx](https://github.com/huydx) +* **Elasticsearch**: Support for Min Doc Count options in Terms aggregation [#7324](https://github.com/grafana/grafana/pull/7324), thx [@lpic10](https://github.com/lpic10) +* **Elasticsearch**: Term aggregation limit can now be changed in template queries [#7112](https://github.com/grafana/grafana/issues/7112), thx [@FFalcon](https://github.com/FFalcon) +* **LINE**: Adds image to notification message [#7417](https://github.com/grafana/grafana/pull/7417), thx [@Erliz](https://github.com/Erliz) ## Tech From 18f20a335746a74cd4b3891beca7b91359deec5c Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 31 Jan 2017 18:37:33 +0100 Subject: [PATCH 009/301] tech(build): deploy to new s3 buckets --- appveyor.yml | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 8b4a16d19cb..0d087e23dac 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -23,6 +23,12 @@ install: - go env - go run build.go setup +before_build: + - cmd: if NOT "%buildTag%"=="" echo Building tag! %buildTag% + - cmd: if NOT "%buildTag%"=="" git fetch + - cmd: if NOT "%buildTag%"=="" git checkout %buildTag% + - cmd: echo Building tag? %buildTag% + build_script: - go run build.go build - grunt release @@ -32,7 +38,17 @@ build_script: artifacts: - path: grafana-*windows-*.* name: binzip + type: zip deploy: - provider: Environment - name: GrafanaBuildsS3 + name: GrafanaReleaseMaster + on: + buildType: master + + - provider: Environment + name: GrafanaReleaseRelease + on: + buildType: release + + From afd135944a778d76b4c61849688c28c8e00ba922 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 31 Jan 2017 20:57:34 +0100 Subject: [PATCH 010/301] tech(build): upgrade golang to 1.7.4 --- appveyor.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 0d087e23dac..30ccabb38d7 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,6 +7,7 @@ clone_folder: c:\gopath\src\github.com\grafana\grafana environment: nodejs_version: "6" GOPATH: c:\gopath + GOVERSION: 1.7.4 install: # install nodejs and npm @@ -14,6 +15,8 @@ install: - npm install -g yarn - yarn install --pure-lockfile - npm install -g grunt-cli + - appveyor DownloadFile https://storage.googleapis.com/golang/go%GOVERSION%.windows-amd64.zip + - 7z x go%GOVERSION%.windows-amd64.zip -y -oC:\ > NUL # install gcc (needed for sqlite3) - choco install -y --limit-output mingw - set PATH=C:\tools\mingw64\bin;%PATH% @@ -23,12 +26,6 @@ install: - go env - go run build.go setup -before_build: - - cmd: if NOT "%buildTag%"=="" echo Building tag! %buildTag% - - cmd: if NOT "%buildTag%"=="" git fetch - - cmd: if NOT "%buildTag%"=="" git checkout %buildTag% - - cmd: echo Building tag? %buildTag% - build_script: - go run build.go build - grunt release From 74c5c5368cde1f492fd80fa4c9865b0147565997 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 31 Jan 2017 21:36:52 +0100 Subject: [PATCH 011/301] tech(build): update windows trigger --- scripts/trigger_windows_build.sh | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/scripts/trigger_windows_build.sh b/scripts/trigger_windows_build.sh index 521a365a299..624509588fb 100755 --- a/scripts/trigger_windows_build.sh +++ b/scripts/trigger_windows_build.sh @@ -1,9 +1,28 @@ #!/bin/bash _token=$1 +_commit=$2 +_buildType=$3 + +post_data=$(cat < Date: Tue, 31 Jan 2017 21:43:18 +0100 Subject: [PATCH 012/301] tech(build): enable windows build trigger --- circle.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/circle.yml b/circle.yml index daa90305720..a72df78bd7e 100644 --- a/circle.yml +++ b/circle.yml @@ -43,7 +43,7 @@ deployment: - ./scripts/build/sign_packages.sh - go run build.go sha1-dist - aws s3 sync ./dist s3://$BUCKET_NAME/master - #- ./scripts/trigger_grafana_docker_build.sh ${TRIGGER_GRAFANA_DOCKER_CIRCLECI_TOKEN} + - ./scripts/trigger_grafana_docker_build.sh ${TRIGGER_GRAFANA_DOCKER_CIRCLECI_TOKEN} ${CIRCLE_SHA1} master gh_tag: tag: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/ commands: @@ -51,5 +51,5 @@ deployment: - ./scripts/build/sign_packages.sh - go run build.go sha1-dist - aws s3 sync ./dist s3://$BUCKET_NAME/release - #- ./scripts/trigger_grafana_docker_build.sh ${TRIGGER_GRAFANA_DOCKER_CIRCLECI_TOKEN} + - ./scripts/trigger_grafana_docker_build.sh ${TRIGGER_GRAFANA_DOCKER_CIRCLECI_TOKEN} ${CIRCLE_SHA1} release From aef4195493a42060dc91e26e1673807d07e0d8b6 Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 1 Feb 2017 07:09:36 +0100 Subject: [PATCH 013/301] tech(build): fixes invalid script file usage --- circle.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/circle.yml b/circle.yml index a72df78bd7e..f4fe487dd77 100644 --- a/circle.yml +++ b/circle.yml @@ -43,7 +43,7 @@ deployment: - ./scripts/build/sign_packages.sh - go run build.go sha1-dist - aws s3 sync ./dist s3://$BUCKET_NAME/master - - ./scripts/trigger_grafana_docker_build.sh ${TRIGGER_GRAFANA_DOCKER_CIRCLECI_TOKEN} ${CIRCLE_SHA1} master + - ./scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} ${CIRCLE_SHA1} master gh_tag: tag: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/ commands: @@ -51,5 +51,5 @@ deployment: - ./scripts/build/sign_packages.sh - go run build.go sha1-dist - aws s3 sync ./dist s3://$BUCKET_NAME/release - - ./scripts/trigger_grafana_docker_build.sh ${TRIGGER_GRAFANA_DOCKER_CIRCLECI_TOKEN} ${CIRCLE_SHA1} release + - ./scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} ${CIRCLE_SHA1} release From 30c334a2b81d03cd6472f36c94d668518c961ebd Mon Sep 17 00:00:00 2001 From: Alexander Menzhinsky Date: Wed, 1 Feb 2017 16:32:51 +0300 Subject: [PATCH 014/301] Add common type for oauth authorization errors --- pkg/api/login.go | 5 ++++ pkg/api/login_oauth.go | 30 +++++++++++++++-------- pkg/social/github_oauth.go | 8 ++---- pkg/social/social.go | 8 ++++++ public/app/core/controllers/login_ctrl.js | 14 ++--------- 5 files changed, 37 insertions(+), 28 deletions(-) diff --git a/pkg/api/login.go b/pkg/api/login.go index 8fbe414c08c..d664f65619b 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -35,6 +35,11 @@ func LoginView(c *middleware.Context) { viewData.Settings["loginHint"] = setting.LoginHint viewData.Settings["disableLoginForm"] = setting.DisableLoginForm + if loginError, ok := c.Session.Get("loginError").(string); ok { + c.Session.Set("loginError", "") // TODO: is there a proper way to delete a session var? + viewData.Settings["loginError"] = loginError + } + if !tryLoginUsingRememberCookie(c) { c.HTML(200, VIEW_INDEX, viewData) return diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index 574d08af09f..9e356347bfd 100644 --- a/pkg/api/login_oauth.go +++ b/pkg/api/login_oauth.go @@ -22,6 +22,13 @@ import ( "github.com/grafana/grafana/pkg/social" ) +var ( + ErrProviderDeniedRequest = errors.New("Login provider denied login request") + ErrEmailNotAllowed = errors.New("Required email domain not fulfilled") + ErrSignUpNotAllowed = errors.New("Signup is not allowed for this adapter") + ErrUsersQuotaReached = errors.New("Users quota reached") +) + func GenStateString() string { rnd := make([]byte, 32) rand.Read(rnd) @@ -44,8 +51,7 @@ func OAuthLogin(ctx *middleware.Context) { error := ctx.Query("error") if error != "" { errorDesc := ctx.Query("error_description") - ctx.Logger.Info("OAuthLogin Failed", "error", error, "errorDesc", errorDesc) - ctx.Redirect(setting.AppSubUrl + "/login?failCode=1003") + redirectWithError(ctx, ErrProviderDeniedRequest, "error", error, "errorDesc", errorDesc) return } @@ -117,10 +123,8 @@ func OAuthLogin(ctx *middleware.Context) { // get user info userInfo, err := connect.UserInfo(client) if err != nil { - if err == social.ErrMissingTeamMembership { - ctx.Redirect(setting.AppSubUrl + "/login?failCode=1000") - } else if err == social.ErrMissingOrganizationMembership { - ctx.Redirect(setting.AppSubUrl + "/login?failCode=1001") + if sErr, ok := err.(*social.Error); ok { + redirectWithError(ctx, sErr) } else { ctx.Handle(500, fmt.Sprintf("login.OAuthLogin(get info from %s)", name), err) } @@ -131,8 +135,7 @@ func OAuthLogin(ctx *middleware.Context) { // validate that the email is allowed to login to grafana if !connect.IsEmailAllowed(userInfo.Email) { - ctx.Logger.Info("OAuth login attempt with unallowed email", "email", userInfo.Email) - ctx.Redirect(setting.AppSubUrl + "/login?failCode=1002") + redirectWithError(ctx, ErrEmailNotAllowed) return } @@ -142,7 +145,7 @@ func OAuthLogin(ctx *middleware.Context) { // create account if missing if err == m.ErrUserNotFound { if !connect.IsSignupAllowed() { - ctx.Redirect(setting.AppSubUrl + "/login") + redirectWithError(ctx, ErrSignUpNotAllowed) return } limitReached, err := middleware.QuotaReached(ctx, "user") @@ -151,7 +154,7 @@ func OAuthLogin(ctx *middleware.Context) { return } if limitReached { - ctx.Redirect(setting.AppSubUrl + "/login") + redirectWithError(ctx, ErrUsersQuotaReached) return } cmd := m.CreateUserCommand{ @@ -179,3 +182,10 @@ func OAuthLogin(ctx *middleware.Context) { ctx.Redirect(setting.AppSubUrl + "/") } + +func redirectWithError(ctx *middleware.Context, err error, v ...interface{}) { + ctx.Logger.Info(err.Error(), v...) + // TODO: we can use the flash storage here once it's implemented + ctx.Session.Set("loginError", err.Error()) + ctx.Redirect(setting.AppSubUrl + "/login") +} diff --git a/pkg/social/github_oauth.go b/pkg/social/github_oauth.go index 64ff1d86851..271a472be84 100644 --- a/pkg/social/github_oauth.go +++ b/pkg/social/github_oauth.go @@ -2,7 +2,6 @@ package social import ( "encoding/json" - "errors" "fmt" "net/http" @@ -21,11 +20,8 @@ type SocialGithub struct { } var ( - ErrMissingTeamMembership = errors.New("User not a member of one of the required teams") -) - -var ( - ErrMissingOrganizationMembership = errors.New("User not a member of one of the required organizations") + ErrMissingTeamMembership = &Error{"User not a member of one of the required teams"} + ErrMissingOrganizationMembership = &Error{"User not a member of one of the required organizations"} ) func (s *SocialGithub) Type() int { diff --git a/pkg/social/social.go b/pkg/social/social.go index 860d068957e..29c4b7ecbb4 100644 --- a/pkg/social/social.go +++ b/pkg/social/social.go @@ -29,6 +29,14 @@ type SocialConnector interface { Client(ctx context.Context, t *oauth2.Token) *http.Client } +type Error struct { + s string +} + +func (e *Error) Error() string { + return e.s +} + var ( SocialBaseUrl = "/login/" SocialMap = make(map[string]SocialConnector) diff --git a/public/app/core/controllers/login_ctrl.js b/public/app/core/controllers/login_ctrl.js index fa3af3d10f0..54323711604 100644 --- a/public/app/core/controllers/login_ctrl.js +++ b/public/app/core/controllers/login_ctrl.js @@ -7,13 +7,6 @@ define([ function (angular, _, coreModule, config) { 'use strict'; - var failCodes = { - "1000": "Required team membership not fulfilled", - "1001": "Required organization membership not fulfilled", - "1002": "Required email domain not fulfilled", - "1003": "Login provider denied login request", - }; - coreModule.default.controller('LoginCtrl', function($scope, backendSrv, contextSrv, $location) { $scope.formModel = { user: '', @@ -36,11 +29,8 @@ function (angular, _, coreModule, config) { $scope.init = function() { $scope.$watch("loginMode", $scope.loginModeChanged); - var params = $location.search(); - if (params.failCode) { - $scope.appEvent('alert-warning', ['Login Failed', failCodes[params.failCode]]); - delete params.failedMsg; - $location.search(params); + if (config.loginError) { + $scope.appEvent('alert-warning', ['Login Failed', config.loginError]); } }; From 3cbca80d3c0b7dc591ce048b287fb265c9b22c63 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 2 Feb 2017 08:59:37 +0100 Subject: [PATCH 015/301] tech(build): starts docker build for latest build --- circle.yml | 1 + scripts/trigger_docker_build.sh | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100755 scripts/trigger_docker_build.sh diff --git a/circle.yml b/circle.yml index f4fe487dd77..8b3fe38550a 100644 --- a/circle.yml +++ b/circle.yml @@ -44,6 +44,7 @@ deployment: - go run build.go sha1-dist - aws s3 sync ./dist s3://$BUCKET_NAME/master - ./scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} ${CIRCLE_SHA1} master + - ./scripts/trigger_docker_build.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN} gh_tag: tag: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/ commands: diff --git a/scripts/trigger_docker_build.sh b/scripts/trigger_docker_build.sh new file mode 100755 index 00000000000..5ca9c0b88c8 --- /dev/null +++ b/scripts/trigger_docker_build.sh @@ -0,0 +1,23 @@ + #!/bin/bash + +_circle_token=$1 +_grafana_version=$2 + +trigger_build_url=https://circleci.com/api/v1/project/grafana/grafana-docker/tree/master?circle-token=${_circle_token} + +post_data=$(cat < Date: Thu, 2 Feb 2017 11:33:34 +0100 Subject: [PATCH 016/301] tech(build): slimdown the size of the build container --- scripts/build/Dockerfile | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/scripts/build/Dockerfile b/scripts/build/Dockerfile index c8df86a0bf1..e24470fc42b 100644 --- a/scripts/build/Dockerfile +++ b/scripts/build/Dockerfile @@ -1,34 +1,32 @@ FROM centos:6.6 -RUN yum install -y yum-plugin-ovl initscripts curl tar gcc libc6-dev git gcc-c++ openssl-devel \ - yum install -y g++ make automake autoconf curl-devel zlib-devel httpd-devel apr-devel apr-util-devel sqlite-devel \ - yum install -y wget yum-utils bzip2 bzip2-devel \ - yum install -y fontconfig freetype freetype-devel fontconfig-devel libstdc++ \ - yum install -y rpm-build patch readline readline-devel libtool bison lzma \ +RUN yum install -y yum-plugin-ovl initscripts curl tar gcc libc6-dev git gcc-c++ openssl-devel && \ + yum install -y g++ make automake autoconf curl-devel zlib-devel httpd-devel apr-devel apr-util-devel sqlite-devel && \ + yum install -y wget yum-utils bzip2 bzip2-devel && \ + yum install -y fontconfig freetype freetype-devel fontconfig-devel libstdc++ && \ + yum install -y rpm-build patch readline readline-devel libtool bison lzma && \ yum install -y which tar # Install RUBY 1.9.3 # install necessary utilities # RUN yum install -y which tar -RUN gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 -RUN curl -sSl https://raw.githubusercontent.com/rvm/rvm/master/binscripts/rvm-installer | bash -s stable -RUN source /etc/profile.d/rvm.sh -RUN /bin/bash -l -c "rvm requirements" -RUN /bin/bash -l -c "rvm install 2.1.9" -RUN /bin/bash -l -c "rvm use 2.1.9 --default" +RUN gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 && \ + curl -sSl https://raw.githubusercontent.com/rvm/rvm/master/binscripts/rvm-installer | bash -s stable && \ + source /etc/profile.d/rvm.sh && \ + /bin/bash -l -c "rvm requirements" && \ + /bin/bash -l -c "rvm install 2.1.9" && \ + /bin/bash -l -c "rvm use 2.1.9 --default" # install nodejs -RUN curl --silent --location https://rpm.nodesource.com/setup_6.x | bash - -RUN yum install -y nodejs --nogpgcheck +RUN curl --silent --location https://rpm.nodesource.com/setup_6.x | bash - && \ + yum install -y nodejs --nogpgcheck -RUN wget https://dl.yarnpkg.com/rpm/yarn.repo -O /etc/yum.repos.d/yarn.repo -RUN yum install -y yarn --nogpgcheck +RUN wget https://dl.yarnpkg.com/rpm/yarn.repo -O /etc/yum.repos.d/yarn.repo && \ + yum install -y yarn --nogpgcheck && \ + wget https://storage.googleapis.com/golang/go1.7.4.linux-amd64.tar.gz && \ + tar -C /usr/local -xzf go1.7.4.linux-amd64.tar.gz ENV GOLANG_VERSION 1.7.4 - -RUN wget https://storage.googleapis.com/golang/go1.7.4.linux-amd64.tar.gz -RUN tar -C /usr/local -xzf go1.7.4.linux-amd64.tar.gz - ENV PATH /usr/local/go/bin:$PATH RUN mkdir -p /go/src /go/bin && chmod -R 777 /go From a580113de4fea132a6b208b945bfc052dab1b8c8 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 2 Feb 2017 12:05:44 +0100 Subject: [PATCH 017/301] feat(alerting): changes default timerange to 15min --- public/app/features/alerting/alert_def.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/alerting/alert_def.ts b/public/app/features/alerting/alert_def.ts index fab612b5558..c65a35878e7 100644 --- a/public/app/features/alerting/alert_def.ts +++ b/public/app/features/alerting/alert_def.ts @@ -13,7 +13,7 @@ var alertQueryDef = new QueryPartDef({ {name: "from", type: "string", options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h', '24h', '48h']}, {name: "to", type: "string", options: ['now']}, ], - defaultParams: ['#A', '5m', 'now', 'avg'] + defaultParams: ['#A', '15m', 'now', 'avg'] }); var conditionTypes = [ From 3e62b1b2d794ceabf9436b86fa02daf3cc402ac4 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 2 Feb 2017 15:55:37 +0100 Subject: [PATCH 018/301] tech(build): use build number as iteration number --- build.go | 8 +++++++- scripts/build/build.sh | 8 ++++---- scripts/build/deploy.sh | 1 + 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/build.go b/build.go index 78f73816ead..f525d798b76 100644 --- a/build.go +++ b/build.go @@ -38,6 +38,7 @@ var ( phjsToRelease string workingDir string includeBuildNumber bool = true + buildNumber int = 0 binaries []string = []string{"grafana-server", "grafana-cli"} ) @@ -58,6 +59,7 @@ func main() { flag.StringVar(&phjsToRelease, "phjs", "", "PhantomJS binary") flag.BoolVar(&race, "race", race, "Use race detector") flag.BoolVar(&includeBuildNumber, "includeBuildNumber", includeBuildNumber, "IncludeBuildNumber in package name") + flag.IntVar(&buildNumber, "buildNumber", 0, "Build number from CI system") flag.Parse() readVersionFromPackageJson() @@ -157,7 +159,11 @@ func readVersionFromPackageJson() { // add timestamp to iteration if includeBuildNumber { - linuxPackageIteration = fmt.Sprintf("%d%s", time.Now().Unix(), linuxPackageIteration) + if buildNumber != 0 { + linuxPackageIteration = fmt.Sprintf("%d%s", buildNumber, linuxPackageIteration) + } else { + linuxPackageIteration = fmt.Sprintf("%d%s", time.Now().Unix(), linuxPackageIteration) + } } } diff --git a/scripts/build/build.sh b/scripts/build/build.sh index d16a5edec69..9674c8a0b8c 100755 --- a/scripts/build/build.sh +++ b/scripts/build/build.sh @@ -25,10 +25,10 @@ echo "current dir: $(pwd)" if [ "$CIRCLE_TAG" != "" ]; then echo "Building a release from tag $CIRCLE_TAG" - go run build.go -includeBuildNumber=false build + go run build.go -buildNumber=${CIRCLE_BUILD_NUM} -includeBuildNumber=false build else echo "Building incremental build for $CIRCLE_BRANCH" - go run build.go build + go run build.go -buildNumber=${CIRCLE_BUILD_NUM} build fi yarn install --pure-lockfile @@ -42,10 +42,10 @@ echo "current dir: $(pwd)" if [ "$CIRCLE_TAG" != "" ]; then echo "Packaging a release from tag $CIRCLE_TAG" - go run build.go -includeBuildNumber=false package latest + go run build.go -buildNumber=${CIRCLE_BUILD_NUM} -includeBuildNumber=false package latest else echo "Packaging incremental build for $CIRCLE_BRANCH" - go run build.go package latest + go run build.go -buildNumber=${CIRCLE_BUILD_NUM} package latest fi cp dist/* /tmp/dist/ diff --git a/scripts/build/deploy.sh b/scripts/build/deploy.sh index 4de3d3f3fb1..6c5661c5dfc 100755 --- a/scripts/build/deploy.sh +++ b/scripts/build/deploy.sh @@ -8,4 +8,5 @@ docker run -i -t --name gfbuild \ -v $(pwd)/dist:/tmp/dist \ -e "CIRCLE_BRANCH=${CIRCLE_BRANCH}" \ -e "CIRCLE_TAG=${CIRCLE_TAG}" \ + -e "CIRCLE_BUILD_NUM=${CIRCLE_BUILD_NUM}" grafana/buildcontainer From 65cf0d0e5c7e7998b2d513daeb6179ac58d7c800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 2 Feb 2017 16:29:11 +0100 Subject: [PATCH 019/301] fix(graph legend): fixed legend table mode scrollbar visible when it needs to to, fixes #6828 --- public/sass/components/_panel_graph.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index 9e919072dce..7b44c29f8fa 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -89,6 +89,7 @@ display: block; overflow-y: auto; overflow-x: hidden; + padding-bottom: 1px; } .graph-legend-series { From 457d6c8f818c73c9e8e963538fd719cf09c19d0d Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 2 Feb 2017 17:23:31 +0100 Subject: [PATCH 020/301] tech(build): fixes broken build --- scripts/build/deploy.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build/deploy.sh b/scripts/build/deploy.sh index 6c5661c5dfc..bfc735e4c79 100755 --- a/scripts/build/deploy.sh +++ b/scripts/build/deploy.sh @@ -8,5 +8,5 @@ docker run -i -t --name gfbuild \ -v $(pwd)/dist:/tmp/dist \ -e "CIRCLE_BRANCH=${CIRCLE_BRANCH}" \ -e "CIRCLE_TAG=${CIRCLE_TAG}" \ - -e "CIRCLE_BUILD_NUM=${CIRCLE_BUILD_NUM}" + -e "CIRCLE_BUILD_NUM=${CIRCLE_BUILD_NUM}" \ grafana/buildcontainer From 032ecad3b73b91393864466041b89967e24240fc Mon Sep 17 00:00:00 2001 From: lucapette Date: Fri, 3 Feb 2017 12:10:22 +0100 Subject: [PATCH 021/301] Fix typo --- pkg/services/sqlstore/migrations/migrations.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go index f0f69ee0e23..a88c1448bb7 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -5,7 +5,7 @@ import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" // --- Migration Guide line --- // 1. Never change a migration that is committed and pushed to master // 2. Always add new migrations (to change or undo previous migrations) -// 3. Some migraitons are not yet written (rename column, table, drop table, index etc) +// 3. Some migrations are not yet written (rename column, table, drop table, index etc) func AddMigrations(mg *Migrator) { addMigrationLogMigrations(mg) From d9b562812674382dac970a7dc45ee012b68769f8 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 3 Feb 2017 16:15:37 +0100 Subject: [PATCH 022/301] docs: update nodejs version requirement in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 14f5669d846..adf4270654a 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ go run build.go build ### Building frontend assets -To build less to css for the frontend you will need a recent version of **node (v4+)**, +To build less to css for the frontend you will need a recent version of **node (v6+)**, npm (v2.5.0) and grunt (v0.4.5). Run the following: ```bash From 220b65afd244e21eade8da55d5f3d1b2b1c1b68f Mon Sep 17 00:00:00 2001 From: Grzegorz Pietrusza Date: Sat, 4 Feb 2017 14:10:40 +0000 Subject: [PATCH 023/301] implement panels loading on scroll --- .../app/features/panel/metrics_panel_ctrl.ts | 15 ++++++++++ public/app/features/panel/panel_directive.ts | 28 +++++++++++++++---- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index d37a3f5db41..de6e331e8a1 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -12,6 +12,8 @@ import * as dateMath from 'app/core/utils/datemath'; import {Subject} from 'vendor/npm/rxjs/Subject'; class MetricsPanelCtrl extends PanelCtrl { + scope: any; + needsRefresh: boolean; loading: boolean; datasource: any; datasourceName: any; @@ -40,6 +42,8 @@ class MetricsPanelCtrl extends PanelCtrl { this.datasourceSrv = $injector.get('datasourceSrv'); this.timeSrv = $injector.get('timeSrv'); this.templateSrv = $injector.get('templateSrv'); + this.scope = $scope; + this.needsRefresh = false; if (!this.panel.targets) { this.panel.targets = [{}]; @@ -50,6 +54,10 @@ class MetricsPanelCtrl extends PanelCtrl { this.events.on('panel-teardown', this.onPanelTearDown.bind(this)); } + private isRenderGraph () { + return window.location.href.indexOf("/dashboard-solo/") === 0; + } + private onPanelTearDown() { if (this.dataSubscription) { this.dataSubscription.unsubscribe(); @@ -66,6 +74,13 @@ class MetricsPanelCtrl extends PanelCtrl { // ignore fetching data if another panel is in fullscreen if (this.otherPanelInFullscreenMode()) { return; } + if (!this.scope.$$childHead || (!this.scope.$$childHead.isVisible() && !this.isRenderGraph())) { + this.scope.$$childHead.needsRefresh = true; + return; + } + + this.scope.$$childHead.needsRefresh = false; + // if we have snapshot data use that if (this.panel.snapshotData) { this.updateTimeRange(); diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts index 24977bd386c..8c51c8795d5 100644 --- a/public/app/features/panel/panel_directive.ts +++ b/public/app/features/panel/panel_directive.ts @@ -1,9 +1,8 @@ /// -import angular from 'angular'; -import $ from 'jquery'; -import _ from 'lodash'; -import Drop from 'tether-drop'; +import angular from "angular"; +import $ from "jquery"; +import Drop from "tether-drop"; var module = angular.module('grafana.directives'); @@ -57,7 +56,7 @@ var panelTemplate = ` `; -module.directive('grafanaPanel', function($rootScope) { +module.directive('grafanaPanel', function($rootScope, $document, $timeout) { return { restrict: 'E', template: panelTemplate, @@ -183,6 +182,25 @@ module.directive('grafanaPanel', function($rootScope) { infoDrop.destroy(); } }); + + var getDataPromise = null; + scope.needsRefresh = false; + + scope.isVisible = function () { + var position = panelContainer[0].getBoundingClientRect(); + return (0 < position.top) && (position.top < window.innerHeight); + }; + + $document.bind('scroll', function () { + if (getDataPromise) { + $timeout.cancel(getDataPromise); + } + if (scope.needsRefresh) { + getDataPromise = $timeout(function () { + scope.ctrl.refresh(); + }, 250); + } + }); } }; }); From a3019a9789caa8c3433e8659cfe963be899638d4 Mon Sep 17 00:00:00 2001 From: Grzegorz Pietrusza Date: Sat, 4 Feb 2017 14:30:24 +0000 Subject: [PATCH 024/301] cleanup --- public/app/features/panel/panel_directive.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts index 8c51c8795d5..637facb50c1 100644 --- a/public/app/features/panel/panel_directive.ts +++ b/public/app/features/panel/panel_directive.ts @@ -1,8 +1,9 @@ /// -import angular from "angular"; -import $ from "jquery"; -import Drop from "tether-drop"; +import angular from 'angular'; +import $ from 'jquery'; +import _ from 'lodash'; +import Drop from 'tether-drop'; var module = angular.module('grafana.directives'); From c05bc0cb17bfbcc11f5ec8b2f7f6800ce7069f67 Mon Sep 17 00:00:00 2001 From: bergquist Date: Sun, 5 Feb 2017 21:19:39 +0100 Subject: [PATCH 025/301] chore(vet): fixes invalid fmt.Sprintf format --- pkg/services/alerting/conditions/query.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/alerting/conditions/query.go b/pkg/services/alerting/conditions/query.go index eafb233a60b..32bcf6a5cb5 100644 --- a/pkg/services/alerting/conditions/query.go +++ b/pkg/services/alerting/conditions/query.go @@ -78,7 +78,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio if context.IsTestRun { context.Logs = append(context.Logs, &alerting.ResultLogEntry{ - Message: fmt.Sprintf("Condition[%d]: Eval: %v, Query Returned No Series (reduced to null/no value)", evalMatch), + Message: fmt.Sprintf("Condition: Eval: %v, Query Returned No Series (reduced to null/no value)", evalMatch), }) } From a36b1d9dcebd46db88bc46f83c7c71fa8b874391 Mon Sep 17 00:00:00 2001 From: bergquist Date: Sun, 5 Feb 2017 21:21:05 +0100 Subject: [PATCH 026/301] tech(build): require go vet to pass --- scripts/circle-test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/circle-test.sh b/scripts/circle-test.sh index d2ff10f5f07..6dce8d540a6 100755 --- a/scripts/circle-test.sh +++ b/scripts/circle-test.sh @@ -20,7 +20,7 @@ exit_if_fail npm test exit_if_fail test -z "$(gofmt -s -l ./pkg | tee /dev/stderr)" exit_if_fail go run build.go build -exit_if_fail go vet ./pkg/... +exit_if_fail test -z "$(go vet ./pkg/... | tee /dev/stderr)" exit_if_fail go test -v ./pkg/... From 9e7df648b5cc7dfe9812bcef6f368ca617ec6f5b Mon Sep 17 00:00:00 2001 From: jifwin Date: Mon, 6 Feb 2017 06:42:26 +0100 Subject: [PATCH 027/301] Fix requests cancelling (#7457) * fix backendSrv request cancelling * revert imports * formatting --- public/app/core/services/backend_srv.ts | 30 +++++++++++++++++-------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 9a5ec5d219b..ba4f4dc2fb9 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -94,6 +94,21 @@ export class BackendSrv { }); }; + addCanceler(requestId, canceler) { + if (requestId in this.inFlightRequests) { + this.inFlightRequests[requestId].push(canceler); + } else { + this.inFlightRequests[requestId] = [canceler]; + } + } + + resolveCancelerIfExists(requestId) { + var cancelers = this.inFlightRequests[requestId]; + if (!_.isUndefined(cancelers) && cancelers.length) { + cancelers[0].resolve(); + } + } + datasourceRequest(options) { options.retry = options.retry || 0; @@ -101,16 +116,13 @@ export class BackendSrv { // particular query. If the requestID exists, the promise it is keyed to // is canceled, canceling the previous datasource request if it is still // in-flight. - var canceler; - if (options.requestId) { - canceler = this.inFlightRequests[options.requestId]; - if (canceler) { - canceler.resolve(); - } + var requestId = options.requestId; + if (requestId) { + this.resolveCancelerIfExists(requestId); // create new canceler - canceler = this.$q.defer(); + var canceler = this.$q.defer(); options.timeout = canceler.promise; - this.inFlightRequests[options.requestId] = canceler; + this.addCanceler(requestId, canceler); } var requestIsLocal = options.url.indexOf('/') === 0; @@ -158,7 +170,7 @@ export class BackendSrv { }).finally(() => { // clean up if (options.requestId) { - delete this.inFlightRequests[options.requestId]; + this.inFlightRequests[options.requestId].shift(); } }); }; From 982dc276d3e8ce0427afbfe78d17e3e22e7701d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 6 Feb 2017 10:19:47 +0100 Subject: [PATCH 028/301] Update building docs readme --- docs/README.md | 34 ++++------------------------------ 1 file changed, 4 insertions(+), 30 deletions(-) diff --git a/docs/README.md b/docs/README.md index 65bd5714615..ea3f8394ace 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,14 +6,13 @@ and the [mkdocs](http://www.mkdocs.org/) tool. **Prepare the Docker Image**: -Build the `grafana/docs-base:latest` image. Run these commands in the -same directory this file is in. **Note** that you may require ``sudo`` +Git clone `grafana/grafana.org` repo. Run these commands in the root of that repo. **Note** that you may require ``sudo`` when running ``make docs-build`` depending on how your system's docker service is configured): ``` -$ git clone https://github.com/grafana/docs-base -$ cd docs-base +$ git clone https://github.com/grafana/grafana.org +$ cd grafana.org $ make docs-build ``` @@ -31,31 +30,6 @@ This command will not return control of the shell to the user. Instead the command is now running a new docker container built from the image we created in the previous step. -Open [localhost:8180](http://localhost:8180) to view the docs. - -**Note** that after running ``make docs`` you may notice a message -like this in the console output - -> Running at: http://0.0.0.0:8000/ - -This is misleading. That is **not** the port the documentation is -served from. You must browse to port **8180** to view the new -documentation. +Open [localhost:3004](http://localhost:3004) to view the docs. -# Adding a New Page - -Adding a new page requires updating the ``mkdocs.yml`` file which is -located in this directory. - -For example, if you are adding documentation for a new HTTP API called -``preferences`` you would: - -1. Create the file ``docs/sources/http_api/preferences.md`` -1. Add a reference to it in ``docs/sources/http_api/overview.md`` -1. Update the list under the **pages** key in the ``docs/mkdocs.yml`` file with a reference to your new page: - - -```yaml -- ['http_api/preferences.md', 'API', 'Preferences API'] -``` From 3827c0a69c23f892ac5491022b5e840263b275ff Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 6 Feb 2017 14:59:29 +0100 Subject: [PATCH 029/301] tech(alerting): adds tags to alerting data model --- pkg/services/alerting/conditions/query.go | 1 + pkg/tsdb/models.go | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/services/alerting/conditions/query.go b/pkg/services/alerting/conditions/query.go index 32bcf6a5cb5..e0a6035355d 100644 --- a/pkg/services/alerting/conditions/query.go +++ b/pkg/services/alerting/conditions/query.go @@ -67,6 +67,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio matches = append(matches, &alerting.EvalMatch{ Metric: series.Name, Value: reducedValue, + Tags: series.Tags, }) } } diff --git a/pkg/tsdb/models.go b/pkg/tsdb/models.go index 68f26a68d50..ade9b1c75f2 100644 --- a/pkg/tsdb/models.go +++ b/pkg/tsdb/models.go @@ -51,8 +51,9 @@ type QueryResult struct { } type TimeSeries struct { - Name string `json:"name"` - Points TimeSeriesPoints `json:"points"` + Name string `json:"name"` + Points TimeSeriesPoints `json:"points"` + Tags map[string]string `json:"tags"` } type TimePoint [2]null.Float From 69566a23fc0302741f43a8078860ee822bda4b85 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Mon, 6 Feb 2017 00:08:35 +0900 Subject: [PATCH 030/301] improve security of Prometheus datasource --- pkg/api/dataproxy.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go index dfdc867d4a4..bc286743533 100644 --- a/pkg/api/dataproxy.go +++ b/pkg/api/dataproxy.go @@ -6,6 +6,7 @@ import ( "net/http" "net/http/httputil" "net/url" + "strings" "time" "github.com/grafana/grafana/pkg/api/cloudwatch" @@ -107,6 +108,13 @@ func ProxyDataSourceRequest(c *middleware.Context) { proxyPath := c.Params("*") + if ds.Type == m.DS_PROMETHEUS { + if !(c.Req.Request.Method == "GET" && strings.Index(proxyPath, "api/") == 0) { + c.JsonApiErr(403, "GET is only allowed on proxied Prometheus datasource", nil) + return + } + } + if ds.Type == m.DS_ES { if c.Req.Request.Method == "DELETE" { c.JsonApiErr(403, "Deletes not allowed on proxied Elasticsearch datasource", nil) From 80b92335bb0c74a18fa8ef959275d40d2a01beae Mon Sep 17 00:00:00 2001 From: "r.khavronenko" Date: Mon, 6 Feb 2017 16:22:09 +0200 Subject: [PATCH 031/301] allow setting basic auth headers for prometheus datasource --- pkg/tsdb/prometheus/prometheus.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go index f3704c2a430..c73e166138d 100644 --- a/pkg/tsdb/prometheus/prometheus.go +++ b/pkg/tsdb/prometheus/prometheus.go @@ -22,6 +22,18 @@ type PrometheusExecutor struct { Transport *http.Transport } +type basicAuthTransport struct { + *http.Transport + + username string + password string +} + +func (bat basicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) { + req.SetBasicAuth(bat.username, bat.password) + return http.DefaultTransport.RoundTrip(req) +} + func NewPrometheusExecutor(dsInfo *models.DataSource) (tsdb.Executor, error) { transport, err := dsInfo.GetHttpTransport() if err != nil { @@ -51,6 +63,14 @@ func (e *PrometheusExecutor) getClient() (prometheus.QueryAPI, error) { Transport: e.Transport, } + if e.BasicAuth { + cfg.Transport = basicAuthTransport{ + Transport: e.Transport, + username: e.BasicAuthUser, + password: e.BasicAuthPassword, + } + } + client, err := prometheus.New(cfg) if err != nil { return nil, err From 57d36b3d421c1f3aee9fcaaa5a5acaa4f1c2d6ff Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 6 Feb 2017 15:25:15 +0100 Subject: [PATCH 032/301] feat(alerting): transform labels into tags for prometheus tsdb --- pkg/tsdb/prometheus/prometheus.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go index f3704c2a430..2e7099e0bb9 100644 --- a/pkg/tsdb/prometheus/prometheus.go +++ b/pkg/tsdb/prometheus/prometheus.go @@ -157,6 +157,11 @@ func parseResponse(value pmodel.Value, query *PrometheusQuery) (map[string]*tsdb for _, v := range data { series := tsdb.TimeSeries{ Name: formatLegend(v.Metric, query), + Tags: map[string]string{}, + } + + for k, v := range v.Metric { + series.Tags[string(k)] = string(v) } for _, k := range v.Values { From 5a7abe365cc3a72ec0ee07ad2a0ad3db681463a6 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 6 Feb 2017 15:55:22 +0100 Subject: [PATCH 033/301] tech(build): echo before running go fmt/vet --- scripts/circle-test.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/circle-test.sh b/scripts/circle-test.sh index 6dce8d540a6..726ba528363 100755 --- a/scripts/circle-test.sh +++ b/scripts/circle-test.sh @@ -18,9 +18,13 @@ yarn install --pure-lockfile exit_if_fail npm test +echo "running go fmt" exit_if_fail test -z "$(gofmt -s -l ./pkg | tee /dev/stderr)" -exit_if_fail go run build.go build + +echo "running go vet" exit_if_fail test -z "$(go vet ./pkg/... | tee /dev/stderr)" + +exit_if_fail go run build.go build exit_if_fail go test -v ./pkg/... From 25be602dce04942c6e30f960febd7c3f4018f939 Mon Sep 17 00:00:00 2001 From: "r.khavronenko" Date: Mon, 6 Feb 2017 17:17:16 +0200 Subject: [PATCH 034/301] go fmt --- pkg/tsdb/prometheus/prometheus.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go index c73e166138d..efe8e2fc528 100644 --- a/pkg/tsdb/prometheus/prometheus.go +++ b/pkg/tsdb/prometheus/prometheus.go @@ -66,8 +66,8 @@ func (e *PrometheusExecutor) getClient() (prometheus.QueryAPI, error) { if e.BasicAuth { cfg.Transport = basicAuthTransport{ Transport: e.Transport, - username: e.BasicAuthUser, - password: e.BasicAuthPassword, + username: e.BasicAuthUser, + password: e.BasicAuthPassword, } } From 3398c28ab2acabede7ffbb05569577659d5e672c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 6 Feb 2017 18:04:34 +0100 Subject: [PATCH 035/301] docs(): fixing redirects for moved doc pages, updating links to point to new urls --- docs/sources/administration/cli.md | 7 ++- docs/sources/alerting/rules.md | 2 +- .../features/datasources/cloudwatch.md | 1 + .../features/datasources/elasticsearch.md | 1 + docs/sources/features/datasources/graphite.md | 1 + docs/sources/features/datasources/influxdb.md | 1 + docs/sources/features/datasources/kairosdb.md | 52 ------------------- docs/sources/features/datasources/opentsdb.md | 1 + .../features/datasources/plugin_api.md | 34 ------------ .../features/datasources/prometheus.md | 3 +- docs/sources/features/datasources/testdata.md | 28 +++++----- docs/sources/guides/basic_concepts.md | 2 +- docs/sources/index.md | 8 +-- docs/sources/installation/debian.md | 8 +-- docs/sources/installation/rpm.md | 8 +-- 15 files changed, 40 insertions(+), 117 deletions(-) delete mode 100644 docs/sources/features/datasources/kairosdb.md delete mode 100644 docs/sources/features/datasources/plugin_api.md diff --git a/docs/sources/administration/cli.md b/docs/sources/administration/cli.md index 99446fb5f1b..ebe910503aa 100644 --- a/docs/sources/administration/cli.md +++ b/docs/sources/administration/cli.md @@ -10,11 +10,14 @@ weight = 8 # Grafana CLI -Grafana cli is a small executable that is bundled with grafana server and is suppose to be executed on the same machine as grafana runs. +Grafana cli is a small executable that is bundled with grafana server and is suppose to be +executed on the same machine as grafana runs. ## Plugins -The CLI helps you install, upgrade and manage your plugins on the same machine it CLI is running. You can find more information about how to install and manage your plugins at the [plugin page] ({{< relref "/installation.md" >}}) +The CLI helps you install, upgrade and manage your plugins on the same machine it CLI is running. +You can find more information about how to install and manage your plugins at the +[plugin page]({{< relref "plugins/installation.md" >}}). ## Admin diff --git a/docs/sources/alerting/rules.md b/docs/sources/alerting/rules.md index af3cfecfdb8..a2688de3100 100644 --- a/docs/sources/alerting/rules.md +++ b/docs/sources/alerting/rules.md @@ -33,7 +33,7 @@ of core Grafana. Only some data soures are supported right now. They include `Gr ### Clustering We have not implemented clustering yet. So if you run multiple instances of grafana-server -you have to make sure [execute_alerts]({{< relref "/installation/configuration.md#alerting" >}}) +you have to make sure [execute_alerts]({{< relref "installation/configuration.md#alerting" >}}) is true on only one instance or otherwise you will get duplicated notifications.
diff --git a/docs/sources/features/datasources/cloudwatch.md b/docs/sources/features/datasources/cloudwatch.md index b4ad0b3288e..c2896c5b172 100644 --- a/docs/sources/features/datasources/cloudwatch.md +++ b/docs/sources/features/datasources/cloudwatch.md @@ -3,6 +3,7 @@ title = "AWS CloudWatch" description = "Guide for using CloudWatch in Grafana" keywords = ["grafana", "cloudwatch", "guide"] type = "docs" +aliases = ["/datasources/cloudwatch"] [menu.docs] name = "AWS Cloudwatch" identifier = "cloudwatch" diff --git a/docs/sources/features/datasources/elasticsearch.md b/docs/sources/features/datasources/elasticsearch.md index 345f68a9220..4f8bfaf968c 100644 --- a/docs/sources/features/datasources/elasticsearch.md +++ b/docs/sources/features/datasources/elasticsearch.md @@ -3,6 +3,7 @@ title = "Using Elasticsearch in Grafana" description = "Guide for using Elasticsearch in Grafana" keywords = ["grafana", "elasticsearch", "guide"] type = "docs" +aliases = ["/datasources/elasticsearch"] [menu.docs] name = "Elasticsearch" parent = "datasources" diff --git a/docs/sources/features/datasources/graphite.md b/docs/sources/features/datasources/graphite.md index 44a4dff3761..11e15ab503e 100644 --- a/docs/sources/features/datasources/graphite.md +++ b/docs/sources/features/datasources/graphite.md @@ -3,6 +3,7 @@ title = "Using Graphite in Grafana" description = "Guide for using graphite in Grafana" keywords = ["grafana", "graphite", "guide"] type = "docs" +aliases = ["/datasources/graphite"] [menu.docs] name = "Graphite" identifier = "graphite" diff --git a/docs/sources/features/datasources/influxdb.md b/docs/sources/features/datasources/influxdb.md index c6afdfdc8d4..c271571aa31 100644 --- a/docs/sources/features/datasources/influxdb.md +++ b/docs/sources/features/datasources/influxdb.md @@ -3,6 +3,7 @@ title = "Using InfluxDB in Grafana" description = "Guide for using InfluxDB in Grafana" keywords = ["grafana", "influxdb", "guide"] type = "docs" +aliases = ["/datasources/influxdb"] [menu.docs] name = "InfluxDB" parent = "datasources" diff --git a/docs/sources/features/datasources/kairosdb.md b/docs/sources/features/datasources/kairosdb.md deleted file mode 100644 index d27105d0ee1..00000000000 --- a/docs/sources/features/datasources/kairosdb.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -page_title: KairosDB Guide -page_description: KairosDB guide for Grafana -page_keywords: grafana, kairosdb, documentation ---- - -# KairosDB Guide -Grafana v2.1 brings initial support for KairosDB Datasources. While the process of adding the datasource is similar to adding a Graphite or OpenTSDB datasource type, Kairos DB does have a few different options for building queries. - -## Adding the data source to Grafana -![](/img/v2/add_KairosDB.jpg) - -1. Open the side menu by clicking the the Grafana icon in the top header. -2. In the side menu under the `Dashboards` link you should find a link named `Data Sources`. - - > NOTE: If this link is missing in the side menu it means that your current user does not have the `Admin` role for the current organization. - -3. Click the `Add new` link in the top header. -4. Select `KairosDB` from the dropdown. - - - -Name | Description ------------- | ------------- -Name | The data source name, important that this is the same as in Grafana v1.x if you plan to import old dashboards. -Default | Default data source means that it will be pre-selected for new panels. -Url | The http protocol, ip and port of your kairosdb server (default port is usually 8080) -Access | Proxy = access via Grafana backend, Direct = access directly from browser. - -## Query editor -Open a graph in edit mode by click the title. - -![](/img/v2/kairos_query_editor.jpg) - -For details on KairosDB metric queries checkout the official. -- [Query Metrics - KairosDB 0.9.4 documentation](http://kairosdb.github.io/kairosdocs/restapi/QueryMetrics.html). - -## Templated queries -KairosDB Datasource Plugin provides following functions in `Variables values query` field in Templating Editor to query `metric names`, `tag names`, and `tag values` to kairosdb server. - -Name | Description -| ------- | --------| -`metrics(query)` | Returns a list of metric names matching `query`. If nothing is given, returns a list of all metric names. -`tag_names(query)` | Returns a list of tag names matching `query`. If nothing is given, returns a list of all tag names. -`tag_values(metric,tag)` | Returns a list of values for `tag` from the given `metric`. - -For details of `metric names`, `tag names`, and `tag values`, please refer to the KairosDB documentations. - -- [List Metric Names - KairosDB 0.9.4 documentation](http://kairosdb.github.io/kairosdocs/restapi/ListMetricNames.html) -- [List Tag Names - KairosDB 0.9.4 documentation](http://kairosdb.github.io/kairosdocs/restapi/ListTagNames.html) -- [List Tag Values - KairosDB 0.9.4 documentation](http://kairosdb.github.io/kairosdocs/restapi/ListTagValues.html) -- [Query Metrics - KairosDB 0.9.4 documentation](http://kairosdb.github.io/kairosdocs/restapi/QueryMetrics.html). diff --git a/docs/sources/features/datasources/opentsdb.md b/docs/sources/features/datasources/opentsdb.md index 7e81ebe1483..435852137a5 100644 --- a/docs/sources/features/datasources/opentsdb.md +++ b/docs/sources/features/datasources/opentsdb.md @@ -3,6 +3,7 @@ title = "Using OpenTSDB in Grafana" description = "Guide for using OpenTSDB in Grafana" keywords = ["grafana", "opentsdb", "guide"] type = "docs" +aliases = ["/datasources/opentsdb"] [menu.docs] name = "OpenTSDB" parent = "datasources" diff --git a/docs/sources/features/datasources/plugin_api.md b/docs/sources/features/datasources/plugin_api.md deleted file mode 100644 index 2e2121ed21a..00000000000 --- a/docs/sources/features/datasources/plugin_api.md +++ /dev/null @@ -1,34 +0,0 @@ ----- -page_title: Data source Plugin API -page_description: Data Source Plugin Description -page_keywords: grafana, data source, plugin, api, docs ---- - -# Data source plugin API - -All data sources in Grafana are implemented as plugins. - -## Breaking change in 2.2 - -In Grafana 2.2 a breaking change was introduced for how data source query editors -are structured, defined and loaded. This was in order to support mixing multiple data sources -in the same panel. - -In Grafana 2.2, the query editor is no longer defined using the partials section in -`plugin.json`, but defined via an angular directive named using convention naming -scheme like `metricQueryEditor`. For example - -Graphite defines a directive like this: - -```javascript -module.directive('metricQueryEditorGraphite', function() { - return {controller: 'GraphiteQueryCtrl', templateUrl: 'app/plugins/datasource/graphite/partials/query.editor.html'}; -}); -``` - -Even though the data source type name is with lowercase `g`, the directive uses capital `G` in `Graphite` because -that is how angular directives needs to be named in order to match an element with name ``. -You also specify the query controller here instead of in the query.editor.html partial like before. - - - diff --git a/docs/sources/features/datasources/prometheus.md b/docs/sources/features/datasources/prometheus.md index 2a35bb615cb..ee272f6b4c6 100644 --- a/docs/sources/features/datasources/prometheus.md +++ b/docs/sources/features/datasources/prometheus.md @@ -3,6 +3,7 @@ title = "Using Prometheus in Grafana" description = "Guide for using Prometheus in Grafana" keywords = ["grafana", "prometheus", "guide"] type = "docs" +aliases = ["/datasources/prometheus"] [menu.docs] name = "Prometheus" parent = "datasources" @@ -74,7 +75,7 @@ You can also use raw queries & regular expressions to extract anything you might ### Using templated variables in queries -When the `Include All` option or `Multi-Value` option is enabled, Grafana converts the labels from plain text to a regex compatible string. +When the `Include All` option or `Multi-Value` option is enabled, Grafana converts the labels from plain text to a regex compatible string. Which means you have to use `=~` instead of `=` in your Prometheus queries. For example `ALERTS{instance=~$instance}` instead of `ALERTS{instance=$instance}`. ![](/img/v2/prometheus_templating.png) diff --git a/docs/sources/features/datasources/testdata.md b/docs/sources/features/datasources/testdata.md index 02e99f7dd8a..3e3f0909700 100644 --- a/docs/sources/features/datasources/testdata.md +++ b/docs/sources/features/datasources/testdata.md @@ -1,23 +1,23 @@ +++ -title = "Grafana TestData" +title = "TestData" keywords = ["grafana", "dashboard", "documentation", "panels", "testdata"] type = "docs" [menu.docs] -name = "Grafana TestData" +name = "TestData" parent = "datasources" -weight = 2 +weight = 20 +++ -# Grafana TestData +# Grafana TestData - > NOTE: This plugin is disable by default. + > NOTE: This plugin is disable by default. -The purpose of this data sources is to make it easier to create fake data for any panel. -Using `Grafana TestData` you can build your own time series and have any panel render it. -This make is much easier to verify functionally since the data can be shared very +The purpose of this data sources is to make it easier to create fake data for any panel. +Using `Grafana TestData` you can build your own time series and have any panel render it. +This make is much easier to verify functionally since the data can be shared very -## Enable +## Enable `Grafana TestData` is not enabled by default. To enable it you have to go to `/plugins/testdata/edit` and click the enable button to enable it for each server. @@ -33,8 +33,8 @@ You can now choose different scenario that you want rendered in the drop down me ## CSV -The comma separated values scenario is the most powerful one since it lets you create any kind of graph you like. -Once you provided the numbers `Grafana TestData` will distribute them evenly based on the time range of your query. +The comma separated values scenario is the most powerful one since it lets you create any kind of graph you like. +Once you provided the numbers `Grafana TestData` will distribute them evenly based on the time range of your query. ![](/img/docs/v41/test_data_csv_example.png) @@ -45,10 +45,10 @@ Once you provided the numbers `Grafana TestData` will distribute them evenly bas ### Commit updates to the dashboards -If you want to submit a change to one of the current dashboards bundled with `Grafana TestData` you have to update the revision property. +If you want to submit a change to one of the current dashboards bundled with `Grafana TestData` you have to update the revision property. Otherwise the dashboard will not be updated automatically for other Grafana users. ## Using test data in issues -If you post an issue on github regarding time series data or rendering of time series data we strongly advice you to use this data source to replicate the data. -That makes it much easier for the developers to replicate and solve the issue you have. +If you post an issue on github regarding time series data or rendering of time series data we strongly advice you to use this data source to replicate the data. +That makes it much easier for the developers to replicate and solve the issue you have. diff --git a/docs/sources/guides/basic_concepts.md b/docs/sources/guides/basic_concepts.md index bbb1cb53728..112b004ec92 100644 --- a/docs/sources/guides/basic_concepts.md +++ b/docs/sources/guides/basic_concepts.md @@ -16,7 +16,7 @@ This document is a “bottom up” introduction to basic concepts in Grafana, an ### Data Source Grafana supports many different storage backends for your time series data (Data Source). Each Data Source has a specific Query Editor that is customized for the features and capabilities that the particular Data Source exposes. -The following datasources are officially supported: [Graphite](/datasources/graphite/), [InfluxDB](/datasources/influxdb/), [OpenTSDB](/datasources/opentsdb/), [Prometheus](/datasources/prometheus/), [Elasticsearch](/datasources/elasticsearch/), [CloudWatch](/datasources/cloudwatch/), and [KairosDB](/datasources/kairosdb) +The following datasources are officially supported: [Graphite]({{< relref "features/datasources/graphite.md" >}}), [InfluxDB]({{< relref "features/datasources/influxdb.md" >}}), [OpenTSDB]({{< relref "features/datasources/opentsdb.md" >}}), [Prometheus]({{< relref "features/datasources/prometheus.md" >}}), [Elasticsearch]({{< relref "features/datasources/elasticsearch.md" >}}), [CloudWatch]({{< relref "features/datasources/cloudwatch.md" >}}). The query language and capabilities of each Data Source are obviously very different. You can combine data from multiple Data Sources onto a single Dashboard, but each Panel is tied to a specific Data Source that belongs to a particular Organization. diff --git a/docs/sources/index.md b/docs/sources/index.md index 0324bf888ad..daf1bd34c36 100644 --- a/docs/sources/index.md +++ b/docs/sources/index.md @@ -42,9 +42,9 @@ those options. ## Data sources guides -- [Graphite](datasources/graphite) -- [Elasticsearch](datasources/elasticsearch) -- [InfluxDB](datasources/influxdb) -- [OpenTSDB](datasources/opentsdb) +- [Graphite]({{< relref "features/datasources/graphite.md" >}}) +- [Elasticsearch]({{< relref "features/datasources/elasticsearch.md" >}}) +- [InfluxDB]({{< relref "features/datasources/influxdb.md" >}}) +- [OpenTSDB]({{< relref "features/datasources/opentsdb.md" >}}) diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index 088f7d14461..9c6140c0c6d 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -114,10 +114,10 @@ those options. ### Adding data sources -- [Graphite]({{< relref "datasources/graphite.md" >}}) -- [InfluxDB]({{< relref "datasources/influxdb.md" >}}) -- [OpenTSDB]({{< relref "datasources/opentsdb.md" >}}) -- [Prometheus]({{< relref "datasources/prometheus.md" >}}) +- [Graphite]({{< relref "features/datasources/graphite.md" >}}) +- [InfluxDB]({{< relref "features/datasources/influxdb.md" >}}) +- [OpenTSDB]({{< relref "features/datasources/opentsdb.md" >}}) +- [Prometheus]({{< relref "features/datasources/prometheus.md" >}}) ## Installing from binary tar file diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md index 8be68232011..63181fc535d 100644 --- a/docs/sources/installation/rpm.md +++ b/docs/sources/installation/rpm.md @@ -121,10 +121,10 @@ those options. ### Adding data sources -- [Graphite]({{< relref "datasources/graphite.md" >}}) -- [InfluxDB]({{< relref "datasources/influxdb.md" >}}) -- [OpenTSDB]({{< relref "datasources/opentsdb.md" >}}) -- [Prometheus]({{< relref "datasources/prometheus.md" >}}) +- [Graphite]({{< relref "features/datasources/graphite.md" >}}) +- [InfluxDB]({{< relref "features/datasources/influxdb.md" >}}) +- [OpenTSDB]({{< relref "features/datasources/opentsdb.md" >}}) +- [Prometheus]({{< relref "features/datasources/prometheus.md" >}}) ### Server side image rendering From abd9233f86fead3dd1d931a5370ad2304511f0c1 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 7 Feb 2017 07:48:01 +0100 Subject: [PATCH 036/301] Docs for developing plugins (#7475) * docs(plugins): new developing plugins section Creates new section called Developing Plugins in the plugin section of the docs. 1. Some changes to the Development guide page 2. Converted defaults/editor mode blog post to new page 3. Converted snapshots blog post to new page 4. Adds new code styleguide page 5. Updates to apps and datasources pages 6. Adds plugin.json schema * docs(links): fixes broken links Fixes broken links to other pages as well as broken image links. --- docs/sources/administration/cli.md | 3 +- docs/sources/alerting/metrics.md | 2 +- docs/sources/features/datasources/index.md | 3 + .../features/datasources/prometheus.md | 10 +- docs/sources/guides/basic_concepts.md | 3 + docs/sources/guides/getting_started.md | 6 +- docs/sources/http_api/auth.md | 2 +- docs/sources/plugins/apps.md | 24 --- docs/sources/plugins/developing/apps.md | 63 ++++++ .../plugins/developing/code-styleguide.md | 182 ++++++++++++++++++ .../plugins/{ => developing}/datasources.md | 50 +++-- .../developing/defaults-and-editor-mode.md | 128 ++++++++++++ .../sources/plugins/developing/development.md | 120 ++++++++++++ docs/sources/plugins/developing/index.md | 8 + .../plugins/{ => developing}/panels.md | 2 +- .../sources/plugins/developing/plugin.json.md | 28 +++ .../plugins/developing/snapshot-mode.md | 79 ++++++++ docs/sources/plugins/development.md | 60 ------ docs/sources/plugins/plugin.json.md | 10 - docs/sources/reference/dashboard.md | 18 +- docs/sources/reference/playlist.md | 2 +- docs/sources/tutorials/hubot_howto.md | 12 +- 22 files changed, 683 insertions(+), 132 deletions(-) delete mode 100644 docs/sources/plugins/apps.md create mode 100644 docs/sources/plugins/developing/apps.md create mode 100644 docs/sources/plugins/developing/code-styleguide.md rename docs/sources/plugins/{ => developing}/datasources.md (69%) create mode 100644 docs/sources/plugins/developing/defaults-and-editor-mode.md create mode 100644 docs/sources/plugins/developing/development.md create mode 100644 docs/sources/plugins/developing/index.md rename docs/sources/plugins/{ => developing}/panels.md (95%) create mode 100644 docs/sources/plugins/developing/plugin.json.md create mode 100644 docs/sources/plugins/developing/snapshot-mode.md delete mode 100644 docs/sources/plugins/development.md delete mode 100644 docs/sources/plugins/plugin.json.md diff --git a/docs/sources/administration/cli.md b/docs/sources/administration/cli.md index ebe910503aa..8c7755506e8 100644 --- a/docs/sources/administration/cli.md +++ b/docs/sources/administration/cli.md @@ -10,8 +10,7 @@ weight = 8 # Grafana CLI -Grafana cli is a small executable that is bundled with grafana server and is suppose to be -executed on the same machine as grafana runs. +Grafana cli is a small executable that is bundled with grafana server and is suppose to be executed on the same machine as grafana runs. ## Plugins diff --git a/docs/sources/alerting/metrics.md b/docs/sources/alerting/metrics.md index b0bf5c9f4fc..02fee6a718a 100644 --- a/docs/sources/alerting/metrics.md +++ b/docs/sources/alerting/metrics.md @@ -13,7 +13,7 @@ weight = 2 > Alerting is only available in Grafana v4.0 and above. -The alert engine publish some internal metrics about itself. You can read more about how Grafana published [interal metrics](/installation/configuration/#metrics) +The alert engine publishes some internal metrics about itself. You can read more about how Grafana published [internal metrics](/installation/configuration/#metrics). Description | Type | Metric name ---------- | ----------- | ---------- diff --git a/docs/sources/features/datasources/index.md b/docs/sources/features/datasources/index.md index af0062ddc51..fcef3370455 100644 --- a/docs/sources/features/datasources/index.md +++ b/docs/sources/features/datasources/index.md @@ -9,13 +9,16 @@ weight = 5 +++ # Data Source Overview + Grafana supports many different storage backends for your time series data (Data Source). Each Data Source has a specific Query Editor that is customized for the features and capabilities that the particular Data Source exposes. ## Querying + The query language and capabilities of each Data Source are obviously very different. You can combine data from multiple Data Sources onto a single Dashboard, but each Panel is tied to a specific Data Source that belongs to a particular Organization. ## Supported Data Sources + The following datasources are officially supported: * [Graphite]({{< relref "graphite.md" >}}) diff --git a/docs/sources/features/datasources/prometheus.md b/docs/sources/features/datasources/prometheus.md index ee272f6b4c6..947d7426230 100644 --- a/docs/sources/features/datasources/prometheus.md +++ b/docs/sources/features/datasources/prometheus.md @@ -16,7 +16,8 @@ weight = 2 Grafana includes support for Prometheus Datasources. While the process of adding the datasource is similar to adding a Graphite or OpenTSDB datasource type, Prometheus does have a few different options for building queries. ## Adding the data source to Grafana -![](/img/v2/add_Prometheus.png) + +![](/img/docs/v2/add_Prometheus.png) 1. Open the side menu by clicking the the Grafana icon in the top header. 2. In the side menu under the `Dashboards` link you should find a link named `Data Sources`. @@ -49,6 +50,7 @@ For details on Prometheus metric queries check out the Prometheus documentation - [Query Metrics - Prometheus documentation](http://prometheus.io/docs/querying/basics/). ## Templated queries + Prometheus Datasource Plugin provides the following functions in `Variables values query` field in Templating Editor to query `metric names` and `labels names` on the Prometheus server. Name | Description @@ -65,8 +67,8 @@ For details of `metric names` & `label names`, and `label values`, please refer You can create a template variable in Grafana and have that variable filled with values from any Prometheus metric exploration query. You can then use this variable in your Prometheus metric queries. -For example you can have a variable that contains all values for label `hostname` if you specify a query like this -in the templating edit view. +For example you can have a variable that contains all values for label `hostname` if you specify a query like this in the templating edit view. + ```sql label_values(hostname) ``` @@ -78,4 +80,4 @@ You can also use raw queries & regular expressions to extract anything you might When the `Include All` option or `Multi-Value` option is enabled, Grafana converts the labels from plain text to a regex compatible string. Which means you have to use `=~` instead of `=` in your Prometheus queries. For example `ALERTS{instance=~$instance}` instead of `ALERTS{instance=$instance}`. -![](/img/v2/prometheus_templating.png) +![](/img/docs/v2/prometheus_templating.png) diff --git a/docs/sources/guides/basic_concepts.md b/docs/sources/guides/basic_concepts.md index 112b004ec92..cb64b105349 100644 --- a/docs/sources/guides/basic_concepts.md +++ b/docs/sources/guides/basic_concepts.md @@ -14,6 +14,7 @@ parent = "guides" This document is a “bottom up” introduction to basic concepts in Grafana, and can be used as a starting point to get familiar with core features. ### Data Source + Grafana supports many different storage backends for your time series data (Data Source). Each Data Source has a specific Query Editor that is customized for the features and capabilities that the particular Data Source exposes. The following datasources are officially supported: [Graphite]({{< relref "features/datasources/graphite.md" >}}), [InfluxDB]({{< relref "features/datasources/influxdb.md" >}}), [OpenTSDB]({{< relref "features/datasources/opentsdb.md" >}}), [Prometheus]({{< relref "features/datasources/prometheus.md" >}}), [Elasticsearch]({{< relref "features/datasources/elasticsearch.md" >}}), [CloudWatch]({{< relref "features/datasources/cloudwatch.md" >}}). @@ -21,6 +22,7 @@ The following datasources are officially supported: [Graphite]({{< relref "featu The query language and capabilities of each Data Source are obviously very different. You can combine data from multiple Data Sources onto a single Dashboard, but each Panel is tied to a specific Data Source that belongs to a particular Organization. ### Organization + Grafana supports multiple organizations in order to support a wide variety of deployment models, including using a single Grafana instance to provide service to multiple potentially untrusted Organizations. In many cases, Grafana will be deployed with a single Organization. @@ -34,6 +36,7 @@ All Dashboards are owned by a particular Organization. For more details on the user model for Grafana, please refer to [Admin](/reference/admin/) ### User + A User is a named account in Grafana. A user can belong to one or more Organizations, and can be assigned different levels of privileges through roles. Grafana supports a wide variety of internal and external ways for Users to authenticate themselves. These include from its own integrated database, from an external SQL server, or from an external LDAP server. diff --git a/docs/sources/guides/getting_started.md b/docs/sources/guides/getting_started.md index 3f039f97ae4..dde8283e79d 100644 --- a/docs/sources/guides/getting_started.md +++ b/docs/sources/guides/getting_started.md @@ -10,12 +10,15 @@ parent = "guides" +++ # Getting started -This guide will help you get started and acquainted with Grafana. It assumes you have a working Grafana server up and running and have added at least one [Data Source](/datasources/overview). + +This guide will help you get started and acquainted with Grafana. It assumes you have a working Grafana server up and running and have added at least one [Data Source](/features/datasources/). ## Beginner guides + Watch the 10min [beginners guide to building dashboards](https://www.youtube.com/watch?v=sKNZMtoSHN4&index=7&list=PLDGkOdUX1Ujo3wHw9-z5Vo12YLqXRjzg2) to get a quick intro to setting up Dashboards and Panels. ## Basic Concepts + Read the [Basic Concepts](/guides/basic_concepts) document to get a crash course in key Grafana concepts. ### Top header @@ -34,6 +37,7 @@ The image above shows you the top header for a Dashboard. 6. Settings: Manage Dashboard settings and features such as Templating and Annotations. ## Dashboards, Panels, Rows, the building blocks of Grafana... + Dashboards are at the core of what Grafana is all about. Dashboards are composed of individual Panels arranged on a number of Rows. Grafana ships with a variety of Panels. Grafana makes it easy to construct the right queries, and customize the display properties so that you can create the perfect Dashboard for your need. Each Panel can interact with data from any configured Grafana Data Source (currently InfluxDB, Graphite, OpenTSDB, and KairosDB). The [Basic Concepts](/guides/basic_concepts) guide explores these key ideas in detail. diff --git a/docs/sources/http_api/auth.md b/docs/sources/http_api/auth.md index aaeda1105b1..ef62f271715 100644 --- a/docs/sources/http_api/auth.md +++ b/docs/sources/http_api/auth.md @@ -30,7 +30,7 @@ curl example: Open the sidemenu and click the organization dropdown and select the `API Keys` option. -![](/img/v2/orgdropdown_api_keys.png) +![](/img/docs/v2/orgdropdown_api_keys.png) You use the token in all requests in the `Authorization` header, like this: diff --git a/docs/sources/plugins/apps.md b/docs/sources/plugins/apps.md deleted file mode 100644 index 74038a9feb9..00000000000 --- a/docs/sources/plugins/apps.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -page_title: App plugin -page_description: App plugin for Grafana -page_keywords: grafana, plugins, documentation ---- - - -# Apps - -App plugins is a new kind of grafana plugin that can bundle datasource and panel plugins within one package. It also enable the plugin author to create custom pages within grafana. The custom pages enables the plugin author to include things like documentation, sign up forms or controlling other services using HTTP requests. - -Datasource and panel plugins will show up like normal plugins. The app pages will be available in the main menu. - - - -## Enabling app plugins -After installing an app it have to be enabled before it show up as an datasource or panel. You can do that on the app page in the config tab. - -### Develop your own App - -> Our goal is not to have a very extensive documentation but rather have actual -> code that people can look at. An example implementation of an app can be found -> in this [example app repo](https://github.com/grafana/example-app) - diff --git a/docs/sources/plugins/developing/apps.md b/docs/sources/plugins/developing/apps.md new file mode 100644 index 00000000000..a3fc35066f6 --- /dev/null +++ b/docs/sources/plugins/developing/apps.md @@ -0,0 +1,63 @@ ++++ +title = "Developing App Plugins" +keywords = ["grafana", "plugins", "documentation"] +type = "docs" +[menu.docs] +name = "Developing App Plugins" +parent = "developing" +weight = 6 ++++ + +# Grafana Apps + +App plugins are a new kind of grafana plugin that can bundle datasource and panel plugins within one package. It also enable the plugin author to create custom pages within grafana. The custom pages enable the plugin author to include things like documentation, sign up forms or controlling other services using HTTP requests. + +Datasource and panel plugins will show up like normal plugins. The app pages will be available in the main menu. + +{{< imgbox img="/img/docs/v3/app-in-main-menu.png" caption="App in Main Menu" >}} + +## Enabling app plugins + +After installing an app, it has to be enabled before it shows up as a datasource or panel. You can do that on the app page in the config tab. + +## Developing an App Plugin + +An App is a bundle of panels, dashboards and/or data source(s). There is nothing different about developing panels and data sources for an app. + +Apps have to be enabled in Grafana and should import any included dashboards when the user enables it. A ConfigCtrl class should be created and the dashboards imported in the postUpdate hook. See example below: + +```javascript +export class ConfigCtrl { + /** @ngInject */ + constructor($scope, $injector, $q) { + this.$q = $q; + this.enabled = false; + this.appEditCtrl.setPostUpdateHook(this.postUpdate.bind(this)); + } + + postUpdate() { + if (!this.appModel.enabled) { + return this.$q.resolve(); + } + return this.appEditCtrl.importDashboards().then(() => { + this.enabled = true; + return { + url: "plugins/raintank-kubernetes-app/page/clusters", + message: "Kubernetes App enabled!" + }; + }); + } +} +ConfigCtrl.templateUrl = 'components/config/config.html'; +``` + +If possible a link to a dashboard or custom page should be shown after enabling the app to guide the user to the appropriate place. + +{{< imgbox img="/img/docs/app_plugin_after_enable.png" caption="After enabling" >}} + +### Develop your own App + +> Our goal is not to have a very extensive documentation but rather have actual +> code that people can look at. An example implementation of an app can be found +> in this [example app repo](https://github.com/grafana/example-app) + diff --git a/docs/sources/plugins/developing/code-styleguide.md b/docs/sources/plugins/developing/code-styleguide.md new file mode 100644 index 00000000000..99d9f565c7d --- /dev/null +++ b/docs/sources/plugins/developing/code-styleguide.md @@ -0,0 +1,182 @@ ++++ +title = "Plugin Code Styleguide" +type = "docs" +[menu.docs] +name = "Plugin Code Styleguide" +parent = "developing" +weight = 2 ++++ + +# Grafana Plugin Code Styleguide + +This guide has two parts. The first part describes the metadata and the second part is a styleguide for HTML/CSS and JavaScript in Grafana plugins and applies if you are using ES6 in your plugin. If using TypeScript then the [Angular TypeScript styleguide](https://angular.io/styleguide) is recommended. + +## Metadata + +The plugin metadata consists of a plugin.json file and the README.md file. These two files are used by Grafana and Grafana.net. + +### Plugin.json (mandatory) + +The plugin.json file is the same concept as the package.json file for an npm package. When Grafana starts it will scan the plugin folders and mount every folder that contains a plugin.json file unless the folder contains a subfolder named `dist`. In that case grafana will mount the `dist` folder instead. + +The most important fields are the first three, especially the id. The convention for the plugin id is **[github username/org]-[plugin name]-[datasource|app|panel]** and it has to be unique. + +Examples: + +``` +raintank-worldping-app +grafana-simple-json-datasource +grafana-piechart-panel +mtanda-histogram-panel +``` + +The full file format for plugin.json is described [here]({{< relref "plugin.json.md" >}}). + +Minimal plugin.json: + +```javascript +{ + "type": "panel", + "name": "Clock", + "id": "yourorg-clock-panel", + + "info": { + "description": "Clock panel for grafana", + "author": { + "name": "Raintank Inc.", + "url": "http://raintank.io" + }, + "keywords": ["clock", "panel"], + "version": "1.0.0", + "updated": "2015-03-24" + }, + + "dependencies": { + "grafanaVersion": "3.x.x", + "plugins": [ ] + } +} +``` + +### README.md + +The README.md file is rendered both on Grafana.net and in the plugins section in Grafana. The only difference from how GitHub renders markdown is that html is not allowed. + +## File and Directory Structure Conventions + +Here is a typical directory structure for a plugin. + +``` +johnnyb-awesome-datasource +|-- dist +|-- spec +| |-- datasource_spec.js +| |-- query_ctrl_spec.js +| |-- test-main.js +|-- src +| |-- img +| | |-- logo.svg +| |-- partials +| | |-- annotations.editor.html +| | |-- config.html +| | |-- query.editor.html +| |-- datasource.js +| |-- module.js +| |-- plugin.json +| |-- query_ctrl.js +|-- Gruntfile.js +|-- LICENSE +|-- package.json +|-- README.md +``` + +Most JavaScript projects have a build step and most Grafana plugins are built using Babel and ES6. The generated JavaScript should be placed in the `dist` directory and the source code in the `src` directory. We recommend that the plugin.json file be placed in the src directory and then copied over to the dist directory when building. The `README.md` can be placed in the root or in the dist directory. + +Directories: + +- `src/` contains plugin source files. +- `src/partials` contains html templates. +- `src/img` contains plugin logos and other images. +- `spec/` contains tests (optional). +- `dist/` contains built content. + +## HTML and CSS + +For the HTML on editor tabs, we recommend using the inbuilt Grafana styles rather than defining your own. This makes plugins feel like a more natural part of Grafana. If done correctly, the html will also be responsive and adapt to smaller screens. The `gf-form` css classes should be used for labels and inputs. + +Below is a minimal example of an editor row with one form group and two fields, a dropdown and a text input: + +```html +
+
+
My Plugin Options
+
+ +
+ +
+
+ + +
+
+
+
+``` + +Use the `width-x` and `max-width-x` classes to control the width of your labels and input fields. Try to get labels and input fields to line up neatly by having the same width for all the labels in a group and the same width for all inputs in a group if possible. + +## Build Scripts + +Our recommendation is to use whatever you usually use - Grunt, Gulp or npm scripts. Most plugins seems to use Grunt so that is probably the easiest to get started with if you do not have a preferred build system. The only requirement is that it supports systemjs which is required by Grafana to load plugins. + +## Linting + +We recommend that you use a linter for your JavaScript. For ES6, the standard linter is [eslint](http://eslint.org/). Rules for linting are described in an .eslintrc that is placed in the root directory. [Here is an example](https://github.com/grafana/worldmap-panel/blob/master/.eslintrc) of linting rules in a plugin. + +### ES6 features + +1. Use `const` if a variable is not going to be reassigned. +2. Prefer to use `let` instead `var` ([Exploring ES6](http://exploringjs.com/es6/ch_core-features.html#_from-var-to-letconst)) +3. Use arrow functions, which don’t shadow `this` ([Exploring ES6](http://exploringjs.com/es6/ch_core-features.html#_from-function-expressions-to-arrow-functions)): + + ```js + testDatasource() { + return this.getServerStatus() + .then(status => { + return this.doSomething(status); + }) + } + ``` + + better than + + ```js + testDatasource() { + var self = this; + return this.getServerStatus() + .then(function(status) { + return self.doSomething(status); + }) + } + ``` +4. Use native _Promise_ object: + + ```js + metricFindQuery(query) { + if (!query) { + return Promise.resolve([]); + } + } + ``` + + better than + + ```js + metricFindQuery(query) { + if (!query) { + return this.$q.when([]); + } + } + ``` +5. If using Lodash, then be consequent and prefer that to the native ES6 array functions. diff --git a/docs/sources/plugins/datasources.md b/docs/sources/plugins/developing/datasources.md similarity index 69% rename from docs/sources/plugins/datasources.md rename to docs/sources/plugins/developing/datasources.md index 332629aa5d8..612a0786976 100644 --- a/docs/sources/plugins/datasources.md +++ b/docs/sources/plugins/developing/datasources.md @@ -1,3 +1,12 @@ ++++ +title = "Developing Datasource Plugins" +keywords = ["grafana", "plugins", "documentation"] +type = "docs" +[menu.docs] +name = "Developing Datasource Plugins" +parent = "developing" +weight = 6 ++++ # Datasources @@ -31,9 +40,11 @@ There are two datasource specific settings for the plugin.json These settings indicates what kind of data the plugin can deliver. At least one of them have to be true ## Datasource + The javascript object that communicates with the database and transforms data to times series. -The Datasource should contain the following functions. +The Datasource should contain the following functions: + ``` query(options) //used by panels to get data testDatasource() //used by datasource configuration page to make sure the connection is working @@ -41,9 +52,14 @@ annotationQuery(options) // used by dashboards to get annotations metricFindQuery(options) // used by query editor to get metric suggestions. ``` +### testDatasource + +When a user clicks on the *Save & Test* button when adding a new data source, the details are first saved to the database and then the `testDatasource` function that is defined in your data source plugin will be called. It is recommended that this function makes a query to the data source that will also test that the authentication details are correct. This is so the data source is correctly configured when the user tries to write a query in a new dashboard. + ### Query -Request object passed to datasource.query function +Request object passed to datasource.query function: + ```json { "range": { "from": "2015-12-22T03:06:13.851Z", "to": "2015-12-22T06:48:24.137Z" }, @@ -57,11 +73,12 @@ Request object passed to datasource.query function } ``` -There are two different kind of results for datasources. -Time series and table. Time series is the most common format and is supported by all datasources and panels. Table format is only support by the Influxdb datasource and table panel. But we might see more of this in the future. +There are two different kinds of results for datasources; +time series and table. Time series is the most common format and is supported by all datasources and panels. Table format is only supported by the InfluxDB datasource and table panel. But we might see more of this in the future. + +Time series response from datasource.query. +An array of: -Time series response from datasource.query -An array of ```json [ { @@ -81,8 +98,9 @@ An array of ] ``` -Table response from datasource.query -An array of +Table response from datasource.query. +An array of: + ```json [ { @@ -119,7 +137,8 @@ An array of ### Annotation Query -Request object passed to datasource.annotationQuery function +Request object passed to datasource.annotationQuery function: + ```json { "range": { "from": "2016-03-04T04:07:55.144Z", "to": "2016-03-04T07:07:55.144Z" }, @@ -132,7 +151,8 @@ Request object passed to datasource.annotationQuery function } ``` -Expected result from datasource.annotationQuery +Expected result from datasource.annotationQuery: + ```json [ { @@ -152,24 +172,24 @@ Expected result from datasource.annotationQuery ## QueryCtrl -A javascript class that will be instantiated and treated as an Angular controller when the user edits metrics in a panel. This class have to inherit from the app/plugins/sdk.QueryCtrl class. +A JavaScript class that will be instantiated and treated as an Angular controller when the user edits metrics in a panel. This class has to inherit from the app/plugins/sdk.QueryCtrl class. Requires a static template or templateUrl variable which will be rendered as the view for this controller. ## ConfigCtrl -A javascript class that will be instantiated and treated as an Angular controller when a user tries to edit or create a new datasource of this type. +A JavaScript class that will be instantiated and treated as an Angular controller when a user tries to edit or create a new datasource of this type. Requires a static template or templateUrl variable which will be rendered as the view for this controller. ## QueryOptionsCtrl -A javascript class that will be instantiated and treated as an Angular controller when the user edits metrics in a panel. This controller is responsible for handling panel wide settings for the datasource. Such as interval, rate and aggregations if needed. +A JavaScript class that will be instantiated and treated as an Angular controller when the user edits metrics in a panel. This controller is responsible for handling panel wide settings for the datasource, such as interval, rate and aggregations if needed. Requires a static template or templateUrl variable which will be rendered as the view for this controller. ## AnnotationsQueryCtrl -A javascript class that will be instantiated and treated as an Angular controller when the user choose this type of datasource in the templating menu in the dashboard. +A JavaScript class that will be instantiated and treated as an Angular controller when the user choose this type of datasource in the templating menu in the dashboard. -Requires a static template or templateUrl variable which will be rendered as the view for this controller. The fields that are bound to this controller is then sent to the Database objects annotationQuery function. +Requires a static template or templateUrl variable which will be rendered as the view for this controller. The fields that are bound to this controller are then sent to the Database objects annotationQuery function. diff --git a/docs/sources/plugins/developing/defaults-and-editor-mode.md b/docs/sources/plugins/developing/defaults-and-editor-mode.md new file mode 100644 index 00000000000..f40aaa90396 --- /dev/null +++ b/docs/sources/plugins/developing/defaults-and-editor-mode.md @@ -0,0 +1,128 @@ ++++ +title = "Plugin Defaults and Editor Mode" +type = "docs" +[menu.docs] +name = "Plugin Defaults and Editor Mode" +parent = "developing" +weight = 3 ++++ + +# Plugin Defaults and Editor Mode + +Most plugins allow users to customize the behavior by changing settings on an editor tab. These setting fields are saved in the dashboard json. + +## Defaults + +We define fields to be saved in Grafana by creating values on the panel object of the controller. You can see these values for any panel by choosing View JSON from the settings menu in Grafana. Here is an excerpt from the clock panel json (with some fields removed), the panel data is saved in the panels array: + +```json +{ + "id": 4, + "title": "Clock", +... + "rows": [ + { +... + "panels": [ + { + "bgColor": "rgb(132, 151, 130)", + "clockType": "24 hour", +``` + +You can define panel data by first creating a variable with default values for the fields and then setting them on the panel object: + +```javascript +const panelDefaults = { + clockType: '24 hour', + fontSize: '60px', + fontWeight: 'normal', + bgColor: null +}; + +constructor($scope, $injector) { + super($scope, $injector); + _.defaults(this.panel, panelDefaults); + + this.updateClock(); +} +``` + +The Lodash function [defaults](https://lodash.com/docs/4.17.4#defaults), which is called in the code above: `_.defaults`, sets a default value only if the value is not already set. This way values that have been changed by the user will not be overwritten. + +These panel fields can be used in the controller or module.html template: + +```html +

{{ctrl.time}}

+``` + +If you want your users to be able to change these panel values then you need to expose them in the Grafana editor. + +## Editor Mode + +Editor mode is when a user clicks Edit on a panel. Every panel has a general tab where you change the title and width and some panels have more inbuilt tabs like the Metrics tab or Time Range tab. A panel plugin can add its own tab(s) so that a user can customize the panel. + +Grafana conventions mean all you need to do is to hook up an Angular template with input fields and Grafana will automatically save the values to the dashboard json and load them on dashboard load. + +## Using Events + +To add an editor tab you need to hook into the event model so that the tab is added when the *init-edit-mode* event is triggered. The following code should be added to the constructor of the plugin Ctrl class: + +```javascript +this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); +``` + +Then you need to create a handler function that is bound to the event. In the example above, the handler is called onInitEditMode. The tab is added by calling the controller function, *addEditorTab*. This function has three parameters; the tab name, the path to a html template for the new editor tab and the tab number. It can be a bit tricky to figure out the path, the path name will be based on the id that is specified in the plugin.json file - for example **grafana-clock-panel**. The code below hooks up an Angular template called editor.html that is located in the `src/partials` directory. + +```javascript +onInitEditMode() { + this.addEditorTab('Options', 'public/plugins/grafana-clock-panel/editor.html', 2); +} +``` + +## Editor HTML and CSS + +For editor tabs html, it is best to use Grafana css styles rather than custom styles. This is to preserve the look and feel of other tabs in Grafana. + +Most editor tabs should use the [gf-form css class](https://github.com/grafana/grafana/blob/master/public/sass/components/_gf-form.scss) from Grafana. The example below has one row with a couple of columns and each column is wrapped in a div like this: + +```html +
+ ``` + +Then each pair, label and field is wrapped in a div with a gf-form class. + +```html +
+ + +
+``` + +Note that there are some Angular attributes here. *ng-model* will update the panel data. *ng-change* will render the panel when you change the value. This change will occur on the onblur event due to the *ng-model-onblur* attribute. This means you can see the effect of your changes on the panel while editing. + +{{< imgbox img="/assets/img/blog/clock-panel-editor.png" caption="Panel Editor" >}} + +On the editor tab we use a drop down for 12/24 hour clock, an input field for font size and a color picker for the background color. + +The drop down/select has its own *gf-form-select-wrapper* css class and looks like this: + +```html +
+ +
+ +
+
+``` + +The color picker (or spectrum picker) is a component that already exists in Grafana. We use it like this for the background color: + +```html + +``` + +## Editor Tab Finished + +To reiterate, this all ties together quite neatly. We specify properties and panel defaults in the constructor for the panel controller and these can then be changed in the editor. Grafana takes care of saving the changes. + +One thing to be aware of is that panel defaults are used the first time a panel is created to set the initial values of the panel properties. After the panel is saved then the saved value will be used instead. So beware if you update panel defaults they will not automatically update the property in an existing panel. For example, if you set the default font size to 60px first and then in version 2 of the plugin change it to 50px, existing panels will still have 60px and only new panels will get the new 50px value. diff --git a/docs/sources/plugins/developing/development.md b/docs/sources/plugins/developing/development.md new file mode 100644 index 00000000000..2c2bc445411 --- /dev/null +++ b/docs/sources/plugins/developing/development.md @@ -0,0 +1,120 @@ ++++ +title = "Developer Guide" +type = "docs" +aliases = ["/plugins/development/", "/plugins/datasources/", "/plugins/apps/", "/plugins/panels/"] +[menu.docs] +name = "Developer Guide" +parent = "developing" +weight = 1 ++++ + +# Developer Guide + +From grafana 3.0 it's very easy to develop your own plugins and share them with other grafana users. + +There are two blog posts about authoring a plugin that might also be of interest to any plugin authors, [Timing is Everything. Writing the Clock Panel Plugin for Grafana 3.0- part 1](http://grafana.org/blog/2016/04/08/timing-is-everything.-writing-the-clock-panel-plugin-for-grafana-3.0/) and [Timing is Everything. Editor Mode in Grafana 3.0 for the Clock Panel Plugin](http://grafana.org/blog/2016/04/15/timing-is-everything.-editor-mode-in-grafana-3.0-for-the-clock-panel-plugin/). + +## Short version + +1. [Setup grafana](http://docs.grafana.org/project/building_from_source/) +2. Clone an example plugin into ```/var/lib/grafana/plugins``` or `data/plugins` (relative to grafana git repo if your running development version from source dir) +3. Code away! + +## What languages? + +Since everything turns into javascript it's up to you to choose which language you want. That said it's probably a good idea to choose es6 or typescript since we use es6 classes in Grafana. So it's easier to get inspiration from the Grafana repo is you choose one of those languages. + +## Buildscript + +You can use any build system you like that support systemjs. All the built content should end up in a folder named ```dist``` and committed to the repository.By committing the dist folder the person who installs your plugin does not have to run any buildscript. + +All our example plugins have build scripted configured. + +## Metadata + +See the [coding styleguide]({{< relref "code-styleguide.md" >}}) for details on the metadata. + +## module.(js|ts) + +This is the entry point for every plugin. This is the place where you should export +your plugin implementation. Depending on what kind of plugin you are developing you +will be expected to export different things. You can find what's expected for [datasource]({{< relref "datasources.md" >}}), [panels]({{< relref "panels.md" >}}) +and [apps]({{< relref "apps.md" >}}) plugins in the documentation. + +The Grafana SDK is quite small so far and can be found here: + +- [SDK file in Grafana](https://github.com/grafana/grafana/blob/master/public/app/plugins/sdk.ts) +- [SDK Readme](https://github.com/grafana/grafana/blob/master/public/app/plugins/plugin_api.md) + +The SDK contains three different plugin classes: PanelCtrl, MetricsPanelCtrl and QueryCtrl. For plugins of the panel type, the module.js file should export one of these. There are some extra classes for [data sources]({{< relref "datasources.md" >}}). + +Example: + +```javascript +import {ClockCtrl} from './clock_ctrl'; + +export { + ClockCtrl as PanelCtrl +}; +``` + +The module class is also where css for the dark and light themes is imported: + +```javascript +import {loadPluginCss} from 'app/plugins/sdk'; +import WorldmapCtrl from './worldmap_ctrl'; + +loadPluginCss({ + dark: 'plugins/grafana-worldmap-panel/css/worldmap.dark.css', + light: 'plugins/grafana-worldmap-panel/css/worldmap.light.css' +}); + +export { + WorldmapCtrl as PanelCtrl +}; +``` + +## Start developing your plugin + +There are three ways that you can start developing a Grafana plugin. + +1. Setup a Grafana development environment. [(described here)](http://docs.grafana.org/project/building_from_source/) and place your plugin in the ```data/plugins``` folder. +2. Install Grafana and place your plugin in the plugins directory which is set in your [config file](/installation/configuration). By default this is `/var/lib/grafana/plugins` on Linux systems. +3. Place your plugin directory anywhere you like and specify it grafana.ini. + +We encourage people to setup the full Grafana environment so that you can get inspiration from the rest of grafana code base. + +When Grafana starts it will scan the plugin folders and mount every folder that contains a plugin.json file unless +the folder contains a subfolder named dist. In that case grafana will mount the dist folder instead. +This makes it possible to have both built and src content in the same plugin git repo. + +## Grafana Events + +There are a number of Grafana events that a plugin can hook into: + +- `init-edit-mode` can be used to add tabs when editing a panel +- `panel-teardown` can be used for clean up +- `data-received` is an event in that is triggered on data refresh and can be hooked into +- `data-snapshot-load` is an event triggered to load data when in snapshot mode. +- `data-error` is used to handle errors on dashboard refresh. + +If a panel receives data and hooks into the `data-received` event then it should handle snapshot mode too. Otherwise the panel will not work if saved as a snapshot. [Getting Plugins to work in Snapshot Mode]({{< relref "snapshot-mode.md" >}}) describes how to add support for this. + +## Examples + +We currently have three different examples that you can fork/download to get started developing your grafana plugin. + + - [simple-json-datasource](https://github.com/grafana/simple-json-datasource) (small datasource plugin for querying json data from backends) + - [example-app](https://github.com/grafana/example-app) + - [clock-panel](https://github.com/grafana/clock-panel) + - [singlestat-panel](https://github.com/grafana/grafana/blob/master/public/app/plugins/panel/singlestat/module.ts) + - [piechart-panel](https://github.com/grafana/piechart-panel) + +## Other Articles + +- [Getting Plugins to work in Snapshot Mode]({{< relref "snapshot-mode.md" >}}) +- [Plugin Defaults and Editor Mode]({{< relref "defaults-and-editor-mode.md" >}}) +- [Grafana Plugin Code Styleguide]({{< relref "code-styleguide.md" >}}) +- [Grafana Apps]({{< relref "apps.md" >}}) +- [Grafana Datasources]({{< relref "datasources.md" >}}) +- [plugin.json Schema]({{< relref "plugin.json.md" >}}) diff --git a/docs/sources/plugins/developing/index.md b/docs/sources/plugins/developing/index.md new file mode 100644 index 00000000000..c9a78fbb39a --- /dev/null +++ b/docs/sources/plugins/developing/index.md @@ -0,0 +1,8 @@ ++++ +title = "Developing Plugins" +type = "docs" +[menu.docs] +parent = "plugins" +identifier = "developing" +weight = 3 ++++ diff --git a/docs/sources/plugins/panels.md b/docs/sources/plugins/developing/panels.md similarity index 95% rename from docs/sources/plugins/panels.md rename to docs/sources/plugins/developing/panels.md index 168d1320ca5..26db69c7c94 100644 --- a/docs/sources/plugins/panels.md +++ b/docs/sources/plugins/developing/panels.md @@ -9,7 +9,7 @@ page_keywords: grafana, plugins, documentation title = "Installing Plugins" type = "docs" [menu.docs] -parent = "plugins" +parent = "developing" weight = 1 +++ diff --git a/docs/sources/plugins/developing/plugin.json.md b/docs/sources/plugins/developing/plugin.json.md new file mode 100644 index 00000000000..8d94ca93af5 --- /dev/null +++ b/docs/sources/plugins/developing/plugin.json.md @@ -0,0 +1,28 @@ ++++ +title = "plugin.json Schema" +keywords = ["grafana", "plugins", "documentation"] +type = "docs" +[menu.docs] +name = "plugin.json Schema" +parent = "developing" +weight = 6 ++++ + +# Plugin.json + +The plugin.json file is mandatory for all plugins. When Grafana starts it will scan the plugin folders and mount every folder that contains a plugin.json file unless the folder contains a subfolder named `dist`. In that case grafana will mount the `dist` folder instead. + +## Plugin JSON Schema + +| Property | Description | +| ------------- |-------------| +| id | unique name of the plugin - [conventions described in styleguide]({{< relref "code-styleguide.md" >}}) | +| type | panel/datasource/app | +| name | Human readable name of the plugin | +| info.description | Description of plugin. Used for searching grafana net plugins | +| info.author | | +| info.keywords | plugin keywords. Used for search on grafana net| +| info.logos | link to project logos | +| info.version | project version of this commit. Must be semver | +| dependencies.grafanaVersion | Required grafana backend version for this plugin | +| dependencies.plugins | required plugins for this plugin. | diff --git a/docs/sources/plugins/developing/snapshot-mode.md b/docs/sources/plugins/developing/snapshot-mode.md new file mode 100644 index 00000000000..dd47b93851a --- /dev/null +++ b/docs/sources/plugins/developing/snapshot-mode.md @@ -0,0 +1,79 @@ ++++ +title = "Snapshot Mode" +type = "docs" +[menu.docs] +name = "Snapshot Mode" +parent = "developing" +weight = 6 ++++ + +# Getting Plugins to work in Snapshot Mode + +{{< imgbox img="/img/docs/Grafana-snapshot-example.png" caption="A dashboard using snapshot data and not live data." >}} + +Grafana has this great feature where you can [save a snapshot of your dashboard](http://docs.grafana.org/reference/sharing/). Instead of sending a screenshot of a dashboard to someone, you can send them a working, interactive Grafana dashboard with the snapshot data embedded inside it. The snapshot can be saved on your Grafana server and is available to all your co-workers. Raintank also hosts a [snapshot server](http://snapshot.raintank.io/) if you want to send the snapshot to someone who does not have access to your Grafana server. + +{{< imgbox img="/img/docs/animated_gifs/snapshots.gif" caption="Selecting a snapshot" >}} + +This all works because Grafana saves a snapshot of the current data in the dashboard json instead of fetching the data from a data source. However, if you are building a custom panel plugin then this will not work straight out of the box. You will need to make some small (and easy!) changes first. + +## Enabling support for loading snapshot data + +Grafana automatically saves data from data sources in the dashboard json when the snapshot is created so we do not have to write any code for that. Enabling snapshot support for reading time series data is very simple. First in the constructor, we need to add an event handler for `data-snapshot-load`. This event is triggered by Grafana when the snapshot data is loaded from the dashboard json. + +```javascript +constructor($scope, $injector, contextSrv) { + super($scope, $injector); + ... + this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); + this.events.on('data-received', this.onDataReceived.bind(this)); + this.events.on('panel-teardown', this.onPanelTeardown.bind(this)); + this.events.on('data-snapshot-load', this.onDataSnapshotLoad.bind(this)); +``` + +Then we need to create a simple event handler that just forwards the data on to our regular `data-received` handler: + +```javascript +onDataSnapshotLoad(snapshotData) { + this.onDataReceived(snapshotData); +} +``` + +This will cover most use cases for snapshot support. Sometimes you will want to save data that is not time series data from a Grafana data source and then you have to do a bit more work to get snapshot support. + +## Saving custom data for snapshots + +Data that is not time series data from a Grafana data source is not saved automatically by Grafana. Saving custom data for snapshot mode has to be done manually. + +{{< imgbox img="/img/docs/Grafana-save-snapshot.png" caption="Save snapshot" >}} + +Grafana gives us a chance to save data to the dashboard json when it is creating a snapshot. In the 'data-received' event handler, you can check the snapshot flag on the dashboard object. If this is true, then Grafana is creating a snapshot and you can manually save custom data to the panel json. In the example, a new field called snapshotLocationData in the panel json is initialized with a snapshot of the custom data. + +```javascript +onDataReceived(dataList) { + if (!dataList) return; + + if (this.dashboard.snapshot && this.locations) { + this.panel.snapshotLocationData = this.locations; + } +``` + +Now the location data is saved in the dashboard json but we will have to load it manually as well. + +## Loading custom data for snapshots + +The example below shows a function that loads the custom data. The data source for the custom data (an external api in this case) is not available in snapshot mode so a guard check is made to see if there is any snapshot data available first. If there is, then the snapshot data is used instead of trying to load the data from the external api. + +```javascript +loadLocationDataFromFile(reload) { + if (this.map && !reload) return; + + if (this.panel.snapshotLocationData) { + this.locations = this.panel.snapshotLocationData; + return; + } +``` + +It is really easy to forget to add this support but it enables a great feature and can be used to demo your panel. + +If there is a panel plugin that you would like to be installed on the Raintank Snapshot server then please contact us via [Slack](https://raintank.slack.com) or [GitHub](https://github.com/grafana/grafana). diff --git a/docs/sources/plugins/development.md b/docs/sources/plugins/development.md deleted file mode 100644 index 9b2a3fb8668..00000000000 --- a/docs/sources/plugins/development.md +++ /dev/null @@ -1,60 +0,0 @@ -+++ -title = "Developer Guide" -type = "docs" -aliases = ["/plugins/datasources/", "/plugins/apps/", "/plugins/panels/"] -[menu.docs] -name = "Developer Guide" -parent = "plugins" -weight = 5 -+++ - -# Developer Guide - -From grafana 3.0 it's very easy to develop your own plugins and share them with other grafana users. - -## Short version - -1. [Setup grafana](http://docs.grafana.org/project/building_from_source/) -2. Clone an example plugin into ```/var/lib/grafana/plugins``` or `data/plugins` (relative to grafana git repo if your running development version from source dir) -3. Code away! - -## What languages? - -Since everything turns into javascript it's up to you to choose which language you want. That said it's probably a good idea to choose es6 or typescript since we use es6 classes in Grafana. So it's easier to get inspiration from the Grafana repo is you choose one of those languages. - -## Buildscript - -You can use any build system you like that support systemjs. All the built content should end up in a folder named ```dist``` and committed to the repository.By committing the dist folder the person who installs your plugin does not have to run any buildscript. - -All our example plugins have build scripted configured. - -## module.(js|ts) - -This is the entry point for every plugin. This is the place where you should export -your plugin implementation. Depending on what kind of plugin you are developing you -will be expected to export different things. You can find what's expected for [datasource](./datasources.md), [panels](./panels.md) -and [apps](./apps.md) plugins in the documentation. - -## Start developing your plugin - -There are three ways that you can start developing a Grafana plugin. - -1. Setup a Grafana development environment. [(described here)](http://docs.grafana.org/project/building_from_source/) and place your plugin in the ```data/plugins``` folder. -2. Install Grafana and place your plugin in the plugins directory which is set in your [config file](../installation/configuration.md). By default this is `/var/lib/grafana/plugins` on Linux systems. -3. Place your plugin directory anywhere you like and specify it grafana.ini. - -We encourage people to setup the full Grafana environment so that you can get inspiration from the rest of grafana code base. - -When Grafana starts it will scan the plugin folders and mount every folder that contains a plugin.json file unless -the folder contains a subfolder named dist. In that case grafana will mount the dist folder instead. -This makes it possible to have both built and src content in the same plugin git repo. - -## Examples - -We currently have three different examples that you can fork/download to get started developing your grafana plugin. - - - [simple-json-datasource](https://github.com/grafana/simple-json-datasource) (small datasource plugin for querying json data from backends) - - [example-app](https://github.com/grafana/example-app) - - [clock-panel](https://github.com/grafana/clock-panel) - - [singlestat-panel](https://github.com/grafana/grafana/blob/master/public/app/plugins/panel/singlestat/module.ts) - - [piechart-panel](https://github.com/grafana/piechart-panel) diff --git a/docs/sources/plugins/plugin.json.md b/docs/sources/plugins/plugin.json.md deleted file mode 100644 index dd4f2f58560..00000000000 --- a/docs/sources/plugins/plugin.json.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -page_title: Plugin json file -page_description: Plugin json for Grafana -page_keywords: grafana, plugins, documentation ---- - -# Plugin.json - -TODO - diff --git a/docs/sources/reference/dashboard.md b/docs/sources/reference/dashboard.md index c1c574ce917..595a10e5081 100644 --- a/docs/sources/reference/dashboard.md +++ b/docs/sources/reference/dashboard.md @@ -66,11 +66,11 @@ Each field in the dashboard JSON is explained below with its usage: | **editable** | whether a dashboard is editable or not | | **hideControls** | whether row controls on the left in green are hidden or not | | **graphTooltip** | TODO | -| **rows** | row metadata, see [rows section](/docs/sources/reference/dashboard.md/#rows) for details | +| **rows** | row metadata, see [rows section](#rows) for details | | **time** | time range for dashboard, i.e. last 6 hours, last 7 days, etc | -| **timepicker** | timepicker metadata, see [timepicker section](/docs/sources/reference/dashboard.md/#timepicker) for details | -| **templating** | templating metadata, see [templating section](/docs/sources/reference/dashboard.md/#templating) for details | -| **annotations** | annotations metadata, see [annotations section](/docs/sources/reference/dashboard.md/#annotations) for details | +| **timepicker** | timepicker metadata, see [timepicker section](#timepicker) for details | +| **templating** | templating metadata, see [templating section](#templating) for details | +| **annotations** | annotations metadata, see [annotations section](#annotations) for details | | **schemaVersion** | TODO | | **version** | TODO | | **links** | TODO | @@ -79,7 +79,7 @@ Each field in the dashboard JSON is explained below with its usage: `rows` field consists of an array of JSON object representing each row in a dashboard, such as shown below: -``` +```json "rows": [ { "collapse": false, @@ -105,14 +105,14 @@ Usage of the fields is explained below: | **collapse** | whether row is collapsed or not | | **editable** | whether a row is editable or not | | **height** | height of the row in pixels | -| **panels** | panels metadata, see [panels section](/docs/sources/reference/dashboard.md/#panels) for details | +| **panels** | panels metadata, see [panels section](#panels) for details | | **title** | title of row | #### panels Panels are the building blocks a dashboard. It consists of datasource queries, type of graphs, aliases, etc. Panel JSON consists of an array of JSON objects, each representing a different panel in a row. Most of the fields are common for all panels but some fields depends on the panel type. Following is an example of panel JSON representing a `graph` panel type: -``` +```json "panels": [ { "aliasColors": {}, @@ -276,7 +276,7 @@ Usage of each field is explained below: Description: TODO -``` +```json "timepicker": { "collapse": false, "enable": true, @@ -330,7 +330,7 @@ Usage of the fields is explained below: `templating` fields contains array of template variables with their saved values along with some other metadata, for example: -``` +```json "templating": { "enable": true, "list": [ diff --git a/docs/sources/reference/playlist.md b/docs/sources/reference/playlist.md index 359a5d8f3bd..e2d59bc322d 100644 --- a/docs/sources/reference/playlist.md +++ b/docs/sources/reference/playlist.md @@ -16,7 +16,7 @@ Since Grafana automatically scales Dashboards to any resolution they're perfect ## Creating a Playlist -{{< docs-imagebox img="img/docs/v3/playlist.png" max-width="25rem" >}} +{{< docs-imagebox img="/img/docs/v3/playlist.png" max-width="25rem" >}} The Playlist feature can be accessed from Grafana's sidemenu, in the Dashboard submenu. diff --git a/docs/sources/tutorials/hubot_howto.md b/docs/sources/tutorials/hubot_howto.md index 17658fbaecd..2c886b94f29 100644 --- a/docs/sources/tutorials/hubot_howto.md +++ b/docs/sources/tutorials/hubot_howto.md @@ -22,15 +22,17 @@ take you to the graph. > is so Hipchat and Slack can show them reliably (they require the image to be publicly available).
- +
## What is Hubot? + [Hubot](https://hubot.github.com/) is an universal and extensible chat bot that can be used with many chat services and has a huge library of third party plugins that allow you to automate anything from your chat rooms. ## Install Hubot + Hubot is very easy to install and host. If you do not already have a bot up and running please read the official [Getting Started With Hubot](https://hubot.github.com/docs/) guide. @@ -63,6 +65,7 @@ The `hubot-grafana` plugin requires a number of environment variables to be set export HUBOT_GRAFANA_S3_REGION=us-standard ### Grafana server side rendering + The hubot plugin will take advantage of the Grafana server side rendering feature that can render any panel on the server using phantomjs. Grafana ships with a phantomjs binary (linux only). @@ -70,11 +73,13 @@ To verify that this feature works try the `Direct link to rendered image` link i If you do not get an image when opening this link verify that the required font packages are installed for phantomjs to work. ### Grafana API Key - + + You need to set the environment variable `HUBOT_GRAFANA_API_KEY` to a Grafana API Key. You can add these from the API Keys page which you find in the Organization dropdown. ### Amazon S3 + The `S3` options are optional but for the images to work properly in services like Slack and Hipchat they need to publicly available. By specifying the `S3` options the hubot-grafana script will publish the rendered panel to `S3` and it will use that URL when it posts to Slack or Hipchat. @@ -99,6 +104,7 @@ panel to `S3` and it will use that URL when it posts to Slack or Hipchat. - Get a templated dashboard with the `$host` parameter set to `carbon-a` ## Aliases + Some of the hubot commands above can lengthy and you might have to remember the dashboard slug (url id). If you have a few favorite graphs you want to be able check up on often (let's say from your mobile) you can create hubot command aliases with the hubot script `hubot-alias`. @@ -115,7 +121,7 @@ Now you can add an alias like this:
Using the alias:
- +
## Summary From 8a92861133e13d27dd3f1adf0cac358b9512838a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 7 Feb 2017 09:09:51 +0100 Subject: [PATCH 037/301] docs(): fixes for rebranded docs site --- docs/Dockerfile | 4 +- docs/Makefile | 49 ++++++------------- docs/config.toml | 70 --------------------------- docs/sources/administration/cli.md | 2 +- docs/sources/alerting/rules.md | 2 +- docs/sources/guides/basic_concepts.md | 2 +- docs/sources/index.md | 18 +++---- docs/sources/installation/debian.md | 8 +-- docs/sources/installation/rpm.md | 8 +-- docs/sources/plugins/development.md | 2 +- 10 files changed, 39 insertions(+), 126 deletions(-) delete mode 100644 docs/config.toml diff --git a/docs/Dockerfile b/docs/Dockerfile index 7652cbd3e3f..7aa9ff1ad6e 100644 --- a/docs/Dockerfile +++ b/docs/Dockerfile @@ -3,9 +3,9 @@ FROM grafana/docs-base:latest # to get the git info for this repo # COPY config.toml /site -RUN rm -rf /site/content/* +# RUN rm -rf /site/content/* -COPY ./sources /site/content/ +# COPY ./sources /site/content/docs/ COPY awsconfig /site diff --git a/docs/Makefile b/docs/Makefile index 718c4b52be7..d3268fc6873 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,50 +1,33 @@ .PHONY: all default docs docs-build docs-shell shell test -# to allow `make DOCSDIR=1 docs-shell` (to create a bind mount in docs) -DOCS_MOUNT := $(if $(DOCSDIR),-v $(CURDIR):/docs/content/grafana/) # to allow `make DOCSPORT=9000 docs` DOCSPORT := 3004 -# Get the IP ADDRESS -DOCKER_IP=$(shell python -c "import urlparse ; print urlparse.urlparse('$(DOCKER_HOST)').hostname or ''") -HUGO_BASE_URL=$(shell test -z "$(DOCKER_IP)" && echo localhost || echo "$(DOCKER_IP)") -HUGO_BIND_IP=0.0.0.0 - -GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD 2>/dev/null) -GIT_BRANCH_CLEAN := $(shell echo $(GIT_BRANCH) | sed -e "s/[^[:alnum:]]/-/g") DOCKER_DOCS_IMAGE := grafana/grafana-docs -DOCKER_RUN_DOCS := docker run --rm -it $(DOCS_MOUNT) -e AWS_S3_BUCKET -e NOCACHE SOURCES_HOST_DIR := "$(shell pwd)/sources" -# for some docs workarounds (see below in "docs-build" target) -GITCOMMIT := $(shell git rev-parse --short HEAD 2>/dev/null) +DOCS_MOUNT := -v $(SOURCES_HOST_DIR):/site/content/docs + +DOCKER_RUN_DOCS := docker run --rm -it $(DOCS_MOUNT) -e NOCACHE -p 3004:3004 -p 3005:3005 + default: docs docs: docs-build - $(DOCKER_RUN_DOCS) -p 3004:3004 -p 3005:3005 -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" /bin/bash -c "grunt && grunt connect --port=3004" - -docs-watch: docs-build - $(DOCKER_RUN_DOCS) -p 3004:3004 -p 3005:3005 -v $(SOURCES_HOST_DIR):/site/content -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" /bin/bash -c "grunt --env=dev-docs && grunt connect --port=3004 & grunt watch --port=3004 --env=dev-docs" - -docs-watch-mac: docs-build - $(DOCKER_RUN_DOCS) -p 3004:3004 -p 3005:3005 -v $(SOURCES_HOST_DIR):/site/content -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" /bin/bash -c "grunt --env=dev-docs-mac && grunt connect --port=3004 & grunt watch --port=3004 --env=dev-docs-mac" - -publish: docs-build - $(DOCKER_RUN_DOCS) "$(DOCKER_DOCS_IMAGE)" /bin/bash -c "./publish.sh staging-docs v3.1" - -publish-prod: docs-build - $(DOCKER_RUN_DOCS) "$(DOCKER_DOCS_IMAGE)" /bin/bash -c "./publish.sh prod-docs root" - -docs-draft: docs-build - $(DOCKER_RUN_DOCS) -p $(if $(DOCSPORT),$(DOCSPORT):)8000 -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" hugo server --buildDrafts="true" --port=$(DOCSPORT) --baseUrl=$(HUGO_BASE_URL) --bind=$(HUGO_BIND_IP) - -docs-shell: docs-build - $(DOCKER_RUN_DOCS) -p $(if $(DOCSPORT),$(DOCSPORT):)8000 "$(DOCKER_DOCS_IMAGE)" bash + $(DOCKER_RUN_DOCS) $(DOCS_MOUNT) -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" /bin/bash -c "grunt && grunt connect --port=3004" test: docs-build - $(DOCKER_RUN_DOCS) -p $(if $(DOCSPORT),$(DOCSPORT):)8000 "$(DOCKER_DOCS_IMAGE)" + $(DOCKER_RUN_DOCS) $(DOCS_MOUNT) -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" /bin/bash -c "ls -la /site/content/docs" + +docs-watch: docs-build + $(DOCKER_RUN_DOCS) $(DOCS_MOUNT) -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" /bin/bash -c "grunt --env=dev-docs && grunt connect --port=3004 & grunt watch --port=3004 --env=dev-docs" + +publish: docs-build + $(DOCKER_RUN_DOCS) $(DOCS_MOUNT) -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" /bin/bash -c "./publish.sh staging-docs v3.1" + +publish-prod: docs-build + $(DOCKER_RUN_DOCS) $(DOCS_MOUNT) -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" /bin/bash -c "./publish.sh prod-docs root" docs-build: - docker build -t "$(DOCKER_DOCS_IMAGE)" . + docker build -t "$(DOCKER_DOCS_IMAGE)" --no-cache . diff --git a/docs/config.toml b/docs/config.toml deleted file mode 100644 index c8d093a81d7..00000000000 --- a/docs/config.toml +++ /dev/null @@ -1,70 +0,0 @@ -baseurl = "http://localhost:3002/" -languageCode = "en-us" -title = "Grafana Docs" -canonifyurls = false -relativeURLs = false -verbose = true -enableRobotsTXT = true -disableSitemap = false -disableRSS = true - -[[menu.top]] - name = "Docs" - url = "" - weight = 1 - -[[menu.top]] - name = "Community" - url = "/community" - weight = 2 - -[[menu.top]] - name = "Support" - url = "/support" - weight = 3 - -[[menu.top]] - name = "Plugins" - url = "https://grafana.net/plugins" - weight = 4 - -[[menu.top]] - name = "Dashboards" - url = "https://grafana.net/dashboards" - weight = 5 - -[[menu.top]] - name = "Hosting" - url = "/hosting" - weight = 6 - -[[menu.top]] - name = "Github" - url = "https://github.com/grafana/grafana" - weight = 7 - -## Main -[[menu.main]] - name = "Feature Gallery" - url = "/features" - weight = 1 - -[[menu.main]] - name = "Live Demo" - url = "http://play.grafana.org" - weight = 2 - -[[menu.main]] - name = "Download" - url = "/download" - weight = 3 - -[[menu.main]] - name = "Blog" - url = "/blog" - weight = 4 - - - - - diff --git a/docs/sources/administration/cli.md b/docs/sources/administration/cli.md index ebe910503aa..4f2bbde3606 100644 --- a/docs/sources/administration/cli.md +++ b/docs/sources/administration/cli.md @@ -17,7 +17,7 @@ executed on the same machine as grafana runs. The CLI helps you install, upgrade and manage your plugins on the same machine it CLI is running. You can find more information about how to install and manage your plugins at the -[plugin page]({{< relref "plugins/installation.md" >}}). +[plugin page]({{< relref "docs/plugins/installation.md" >}}). ## Admin diff --git a/docs/sources/alerting/rules.md b/docs/sources/alerting/rules.md index a2688de3100..aa38367b4aa 100644 --- a/docs/sources/alerting/rules.md +++ b/docs/sources/alerting/rules.md @@ -33,7 +33,7 @@ of core Grafana. Only some data soures are supported right now. They include `Gr ### Clustering We have not implemented clustering yet. So if you run multiple instances of grafana-server -you have to make sure [execute_alerts]({{< relref "installation/configuration.md#alerting" >}}) +you have to make sure [execute_alerts]({{< relref "docs/installation/configuration.md#alerting" >}}) is true on only one instance or otherwise you will get duplicated notifications.
diff --git a/docs/sources/guides/basic_concepts.md b/docs/sources/guides/basic_concepts.md index 112b004ec92..0411db6caf2 100644 --- a/docs/sources/guides/basic_concepts.md +++ b/docs/sources/guides/basic_concepts.md @@ -16,7 +16,7 @@ This document is a “bottom up” introduction to basic concepts in Grafana, an ### Data Source Grafana supports many different storage backends for your time series data (Data Source). Each Data Source has a specific Query Editor that is customized for the features and capabilities that the particular Data Source exposes. -The following datasources are officially supported: [Graphite]({{< relref "features/datasources/graphite.md" >}}), [InfluxDB]({{< relref "features/datasources/influxdb.md" >}}), [OpenTSDB]({{< relref "features/datasources/opentsdb.md" >}}), [Prometheus]({{< relref "features/datasources/prometheus.md" >}}), [Elasticsearch]({{< relref "features/datasources/elasticsearch.md" >}}), [CloudWatch]({{< relref "features/datasources/cloudwatch.md" >}}). +The following datasources are officially supported: [Graphite]({{< relref "docs/features/datasources/graphite.md" >}}), [InfluxDB]({{< relref "docs/features/datasources/influxdb.md" >}}), [OpenTSDB]({{< relref "docs/features/datasources/opentsdb.md" >}}), [Prometheus]({{< relref "docs/features/datasources/prometheus.md" >}}), [Elasticsearch]({{< relref "docs/features/datasources/elasticsearch.md" >}}), [CloudWatch]({{< relref "docs/features/datasources/cloudwatch.md" >}}). The query language and capabilities of each Data Source are obviously very different. You can combine data from multiple Data Sources onto a single Dashboard, but each Panel is tied to a specific Data Source that belongs to a particular Organization. diff --git a/docs/sources/index.md b/docs/sources/index.md index daf1bd34c36..ab2e0f882b8 100644 --- a/docs/sources/index.md +++ b/docs/sources/index.md @@ -24,27 +24,27 @@ other domains including industrial sensors, home automation, weather, and proces - [Installing using Provisioning (Chef, Puppet, Salt, Ansible, etc)](installation/provisioning) - [Nightly Builds](http://grafana.org/builds) -For other platforms Read the [build from source]({{< relref "project/building_from_source.md" >}}) +For other platforms Read the [build from source]({{< relref "docs/project/building_from_source.md" >}}) instructions for more information. ## Configuring Grafana The back-end web server has a number of configuration options. Go the -[Configuration](/installation/configuration) page for details on all +[Configuration]({{< relref "docs/installation/configuration.md" >}}) page for details on all those options. ## Getting started -- [Getting Started](guides/getting_started) -- [Basic Concepts](guides/basic_concepts) -- [Screencasts](tutorials/screencasts) +- [Getting Started]({{< relref "docs/guides/getting_started.md" >}}) +- [Basic Concepts]({{< relref "docs/guides/basic_concepts.md" >}}) +- [Screencasts]({{< relref "docs/tutorials/screencasts.md" >}}) ## Data sources guides -- [Graphite]({{< relref "features/datasources/graphite.md" >}}) -- [Elasticsearch]({{< relref "features/datasources/elasticsearch.md" >}}) -- [InfluxDB]({{< relref "features/datasources/influxdb.md" >}}) -- [OpenTSDB]({{< relref "features/datasources/opentsdb.md" >}}) +- [Graphite]({{< relref "docs/features/datasources/graphite.md" >}}) +- [Elasticsearch]({{< relref "docs/features/datasources/elasticsearch.md" >}}) +- [InfluxDB]({{< relref "docs/features/datasources/influxdb.md" >}}) +- [OpenTSDB]({{< relref "docs/features/datasources/opentsdb.md" >}}) diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index 9c6140c0c6d..7dd97e69810 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -114,10 +114,10 @@ those options. ### Adding data sources -- [Graphite]({{< relref "features/datasources/graphite.md" >}}) -- [InfluxDB]({{< relref "features/datasources/influxdb.md" >}}) -- [OpenTSDB]({{< relref "features/datasources/opentsdb.md" >}}) -- [Prometheus]({{< relref "features/datasources/prometheus.md" >}}) +- [Graphite]({{< relref "docs/features/datasources/graphite.md" >}}) +- [InfluxDB]({{< relref "docs/features/datasources/influxdb.md" >}}) +- [OpenTSDB]({{< relref "docs/features/datasources/opentsdb.md" >}}) +- [Prometheus]({{< relref "docs/features/datasources/prometheus.md" >}}) ## Installing from binary tar file diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md index 63181fc535d..561cff69b46 100644 --- a/docs/sources/installation/rpm.md +++ b/docs/sources/installation/rpm.md @@ -121,10 +121,10 @@ those options. ### Adding data sources -- [Graphite]({{< relref "features/datasources/graphite.md" >}}) -- [InfluxDB]({{< relref "features/datasources/influxdb.md" >}}) -- [OpenTSDB]({{< relref "features/datasources/opentsdb.md" >}}) -- [Prometheus]({{< relref "features/datasources/prometheus.md" >}}) +- [Graphite]({{< relref "docs/features/datasources/graphite.md" >}}) +- [InfluxDB]({{< relref "docs/features/datasources/influxdb.md" >}}) +- [OpenTSDB]({{< relref "docs/features/datasources/opentsdb.md" >}}) +- [Prometheus]({{< relref "docs/features/datasources/prometheus.md" >}}) ### Server side image rendering diff --git a/docs/sources/plugins/development.md b/docs/sources/plugins/development.md index 9b2a3fb8668..edb959e97bc 100644 --- a/docs/sources/plugins/development.md +++ b/docs/sources/plugins/development.md @@ -40,7 +40,7 @@ and [apps](./apps.md) plugins in the documentation. There are three ways that you can start developing a Grafana plugin. 1. Setup a Grafana development environment. [(described here)](http://docs.grafana.org/project/building_from_source/) and place your plugin in the ```data/plugins``` folder. -2. Install Grafana and place your plugin in the plugins directory which is set in your [config file](../installation/configuration.md). By default this is `/var/lib/grafana/plugins` on Linux systems. +2. Install Grafana and place your plugin in the plugins directory which is set in your [config file]({{< relref "docs/installation/configuration.md" >}}). By default this is `/var/lib/grafana/plugins` on Linux systems. 3. Place your plugin directory anywhere you like and specify it grafana.ini. We encourage people to setup the full Grafana environment so that you can get inspiration from the rest of grafana code base. From 285918caf6c836676b43e6d141a62fe2fcd3aa1a Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 6 Feb 2017 16:21:09 +0100 Subject: [PATCH 038/301] tech(build): switch to golang 1.8rc3 --- circle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circle.yml b/circle.yml index 8b3fe38550a..6d61acd6f6a 100644 --- a/circle.yml +++ b/circle.yml @@ -9,7 +9,7 @@ machine: GOPATH: "/home/ubuntu/.go_workspace" ORG_PATH: "github.com/grafana" REPO_PATH: "${ORG_PATH}/grafana" - GODIST: "go1.7.4.linux-amd64.tar.gz" + GODIST: "go1.8rc3.linux-amd64.tar.gz" post: - mkdir -p ~/download - mkdir -p ~/docker From 5e9653f9359779e95ddc62cdd540f26ffe86a664 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 7 Feb 2017 10:21:46 +0100 Subject: [PATCH 039/301] docs(): adds missing index pages + link fix --- docs/sources/installation/configuration.md | 4 +++- docs/sources/project/index.md | 7 +++++++ docs/sources/reference/index.md | 7 +++++++ 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 docs/sources/project/index.md create mode 100644 docs/sources/reference/index.md diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index e222f8b5f4f..4d22d12dd9b 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -55,6 +55,7 @@ Then you can override them using:
## instance_name + Set the name of the grafana-server instance. Used in logging and internal metrics and in clustering info. Defaults to: `${HOSTNAME}`, which will be replaced with environment variable `HOSTNAME`, if that is empty or does not exist Grafana will try to use @@ -145,6 +146,7 @@ things). By default it is configured to use `sqlite3` which is an embedded database (included in the main Grafana binary). ### url + Use either URL or or the other fields below to configure the database Example: `mysql://user:secret@host:port/database` @@ -231,7 +233,7 @@ Default is `false`. Set to `false` to prohibit users from being able to sign up / create user accounts. Defaults to `true`. The admin user can still create -users from the [Grafana Admin Pages](../reference/admin.md) +users from the [Grafana Admin Pages](../../reference/admin) ### allow_org_create diff --git a/docs/sources/project/index.md b/docs/sources/project/index.md new file mode 100644 index 00000000000..513ec911d93 --- /dev/null +++ b/docs/sources/project/index.md @@ -0,0 +1,7 @@ ++++ +title = "Project" +type = "docs" +identifier = "project" +weight = 6 ++++ + diff --git a/docs/sources/reference/index.md b/docs/sources/reference/index.md new file mode 100644 index 00000000000..a1ea21c60d1 --- /dev/null +++ b/docs/sources/reference/index.md @@ -0,0 +1,7 @@ ++++ +title = "reference" +type = "docs" +identifier = "reference" +weight = 6 ++++ + From a1e835884b64854e8d0ec2550c92f3ccc0ed036b Mon Sep 17 00:00:00 2001 From: hagen1778 Date: Tue, 7 Feb 2017 11:56:59 +0200 Subject: [PATCH 040/301] use configured Transport instead of defaultHttpTransport --- pkg/tsdb/prometheus/prometheus.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go index efe8e2fc528..a4a6d8da2eb 100644 --- a/pkg/tsdb/prometheus/prometheus.go +++ b/pkg/tsdb/prometheus/prometheus.go @@ -31,7 +31,7 @@ type basicAuthTransport struct { func (bat basicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) { req.SetBasicAuth(bat.username, bat.password) - return http.DefaultTransport.RoundTrip(req) + return bat.Transport.RoundTrip(req) } func NewPrometheusExecutor(dsInfo *models.DataSource) (tsdb.Executor, error) { From 9ece10ef248fc3ea4dc49e56814abc6d98c0fb76 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 7 Feb 2017 11:03:57 +0100 Subject: [PATCH 041/301] style(dataproxy): simplify expression --- pkg/api/dataproxy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go index bc286743533..b72f676dbbc 100644 --- a/pkg/api/dataproxy.go +++ b/pkg/api/dataproxy.go @@ -109,7 +109,7 @@ func ProxyDataSourceRequest(c *middleware.Context) { proxyPath := c.Params("*") if ds.Type == m.DS_PROMETHEUS { - if !(c.Req.Request.Method == "GET" && strings.Index(proxyPath, "api/") == 0) { + if c.Req.Request.Method != http.MethodGet || !strings.HasPrefix(proxyPath, "api/") { c.JsonApiErr(403, "GET is only allowed on proxied Prometheus datasource", nil) return } From d1a5d9c15c2c34b0eda902809853e11f372a37a9 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Tue, 7 Feb 2017 11:02:12 +0100 Subject: [PATCH 042/301] feat(alerting): Add Threema Gateway integration This commit adds alerting support for Threema Gateway. It supports all Simple IDs (managed by the Gateway server). More information can be found on https://gateway.threema.ch/ --- pkg/metrics/metrics.go | 2 + pkg/services/alerting/notifiers/threema.go | 159 ++++++++++++++++++ .../alerting/notifiers/threema_test.go | 119 +++++++++++++ 3 files changed, 280 insertions(+) create mode 100644 pkg/services/alerting/notifiers/threema.go create mode 100644 pkg/services/alerting/notifiers/threema_test.go diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 1020f28f874..452e6884566 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -49,6 +49,7 @@ var ( M_Alerting_Notification_Sent_Victorops Counter M_Alerting_Notification_Sent_OpsGenie Counter M_Alerting_Notification_Sent_Telegram Counter + M_Alerting_Notification_Sent_Threema Counter M_Alerting_Notification_Sent_Sensu Counter M_Aws_CloudWatch_GetMetricStatistics Counter M_Aws_CloudWatch_ListMetrics Counter @@ -118,6 +119,7 @@ func initMetricVars(settings *MetricSettings) { M_Alerting_Notification_Sent_Victorops = RegCounter("alerting.notifications_sent", "type", "victorops") M_Alerting_Notification_Sent_OpsGenie = RegCounter("alerting.notifications_sent", "type", "opsgenie") M_Alerting_Notification_Sent_Telegram = RegCounter("alerting.notifications_sent", "type", "telegram") + M_Alerting_Notification_Sent_Threema = RegCounter("alerting.notifications_sent", "type", "threema") M_Alerting_Notification_Sent_Sensu = RegCounter("alerting.notifications_sent", "type", "sensu") M_Alerting_Notification_Sent_LINE = RegCounter("alerting.notifications_sent", "type", "LINE") diff --git a/pkg/services/alerting/notifiers/threema.go b/pkg/services/alerting/notifiers/threema.go new file mode 100644 index 00000000000..f805bd24394 --- /dev/null +++ b/pkg/services/alerting/notifiers/threema.go @@ -0,0 +1,159 @@ +package notifiers + +import ( + "fmt" + "net/url" + "strings" + + "github.com/grafana/grafana/pkg/bus" + "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" +) + +var ( + threemaGwBaseURL = "https://msgapi.threema.ch/%s" +) + +func init() { + alerting.RegisterNotifier(&alerting.NotifierPlugin{ + Type: "threema", + Name: "Threema Gateway", + Description: "Sends notifications to Threema using the Threema Gateway", + Factory: NewThreemaNotifier, + OptionsTemplate: ` +

Threema Gateway settings

+

+ Notifications can be configured for any Threema Gateway ID of type + "Basic". End-to-End IDs are not currently supported. +

+

+ The Threema Gateway ID can be set up at + https://gateway.threema.ch/. +

+
+ Gateway ID + + + + Your 8 character Threema Gateway ID (starting with a *) + +
+
+ Recipient ID + + + + The 8 character Threema ID that should receive the alerts + +
+
+ API Secret + + + + Your Threema Gateway API secret + +
+ `, + }) + +} + +type ThreemaNotifier struct { + NotifierBase + GatewayID string + RecipientID string + APISecret string + log log.Logger +} + +func NewThreemaNotifier(model *m.AlertNotification) (alerting.Notifier, error) { + if model.Settings == nil { + return nil, alerting.ValidationError{Reason: "No Settings Supplied"} + } + + gatewayID := model.Settings.Get("gateway_id").MustString() + recipientID := model.Settings.Get("recipient_id").MustString() + apiSecret := model.Settings.Get("api_secret").MustString() + + // Validation + if gatewayID == "" { + return nil, alerting.ValidationError{Reason: "Could not find Threema Gateway ID in settings"} + } + if !strings.HasPrefix(gatewayID, "*") { + return nil, alerting.ValidationError{Reason: "Invalid Threema Gateway ID: Must start with a *"} + } + if len(gatewayID) != 8 { + return nil, alerting.ValidationError{Reason: "Invalid Threema Gateway ID: Must be 8 characters long"} + } + if recipientID == "" { + return nil, alerting.ValidationError{Reason: "Could not find Threema Recipient ID in settings"} + } + if len(recipientID) != 8 { + return nil, alerting.ValidationError{Reason: "Invalid Threema Recipient ID: Must be 8 characters long"} + } + if apiSecret == "" { + return nil, alerting.ValidationError{Reason: "Could not find Threema API secret in settings"} + } + + return &ThreemaNotifier{ + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + GatewayID: gatewayID, + RecipientID: recipientID, + APISecret: apiSecret, + log: log.New("alerting.notifier.threema"), + }, nil +} + +func (notifier *ThreemaNotifier) Notify(evalContext *alerting.EvalContext) error { + notifier.log.Info("Sending alert notification from", "threema_id", notifier.GatewayID) + notifier.log.Info("Sending alert notification to", "threema_id", notifier.RecipientID) + metrics.M_Alerting_Notification_Sent_Threema.Inc(1) + + // Set up basic API request data + data := url.Values{} + data.Set("from", notifier.GatewayID) + data.Set("to", notifier.RecipientID) + data.Set("secret", notifier.APISecret) + + // Build message + message := fmt.Sprintf("%s\nState: %s\nMessage: %s\n", + evalContext.GetNotificationTitle(), evalContext.Rule.Name, evalContext.Rule.Message) + ruleURL, err := evalContext.GetRuleUrl() + if err == nil { + message = message + fmt.Sprintf("URL: %s\n", ruleURL) + } + if evalContext.ImagePublicUrl != "" { + message = message + fmt.Sprintf("Image: %s\n", evalContext.ImagePublicUrl) + } + data.Set("text", message) + + // Prepare and send request + url := fmt.Sprintf(threemaGwBaseURL, "send_simple") + body := data.Encode() + headers := map[string]string{ + "Content-Type": "application/x-www-form-urlencoded", + } + cmd := &m.SendWebhookSync{ + Url: url, + Body: body, + HttpMethod: "POST", + HttpHeader: headers, + } + if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { + notifier.log.Error("Failed to send webhook", "error", err, "webhook", notifier.Name) + return err + } + + return nil +} diff --git a/pkg/services/alerting/notifiers/threema_test.go b/pkg/services/alerting/notifiers/threema_test.go new file mode 100644 index 00000000000..3f23730a249 --- /dev/null +++ b/pkg/services/alerting/notifiers/threema_test.go @@ -0,0 +1,119 @@ +package notifiers + +import ( + "testing" + + "github.com/grafana/grafana/pkg/components/simplejson" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/alerting" + . "github.com/smartystreets/goconvey/convey" +) + +func TestThreemaNotifier(t *testing.T) { + Convey("Threema notifier tests", t, func() { + + Convey("Parsing alert notification from settings", func() { + Convey("empty settings should return error", func() { + json := `{ }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "threema_testing", + Type: "threema", + Settings: settingsJSON, + } + + _, err := NewThreemaNotifier(model) + So(err, ShouldNotBeNil) + }) + + Convey("valid settings should be parsed successfully", func() { + json := ` + { + "gateway_id": "*3MAGWID", + "recipient_id": "ECHOECHO", + "api_secret": "1234" + }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "threema_testing", + Type: "threema", + Settings: settingsJSON, + } + + not, err := NewThreemaNotifier(model) + So(err, ShouldBeNil) + threemaNotifier := not.(*ThreemaNotifier) + + So(err, ShouldBeNil) + So(threemaNotifier.Name, ShouldEqual, "threema_testing") + So(threemaNotifier.Type, ShouldEqual, "threema") + So(threemaNotifier.GatewayID, ShouldEqual, "*3MAGWID") + So(threemaNotifier.RecipientID, ShouldEqual, "ECHOECHO") + So(threemaNotifier.APISecret, ShouldEqual, "1234") + }) + + Convey("invalid Threema Gateway IDs should be rejected (prefix)", func() { + json := ` + { + "gateway_id": "ECHOECHO", + "recipient_id": "ECHOECHO", + "api_secret": "1234" + }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "threema_testing", + Type: "threema", + Settings: settingsJSON, + } + + not, err := NewThreemaNotifier(model) + So(not, ShouldBeNil) + So(err.(alerting.ValidationError).Reason, ShouldEqual, "Invalid Threema Gateway ID: Must start with a *") + }) + + Convey("invalid Threema Gateway IDs should be rejected (length)", func() { + json := ` + { + "gateway_id": "*ECHOECHO", + "recipient_id": "ECHOECHO", + "api_secret": "1234" + }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "threema_testing", + Type: "threema", + Settings: settingsJSON, + } + + not, err := NewThreemaNotifier(model) + So(not, ShouldBeNil) + So(err.(alerting.ValidationError).Reason, ShouldEqual, "Invalid Threema Gateway ID: Must be 8 characters long") + }) + + Convey("invalid Threema Recipient IDs should be rejected (length)", func() { + json := ` + { + "gateway_id": "*3MAGWID", + "recipient_id": "ECHOECH", + "api_secret": "1234" + }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "threema_testing", + Type: "threema", + Settings: settingsJSON, + } + + not, err := NewThreemaNotifier(model) + So(not, ShouldBeNil) + So(err.(alerting.ValidationError).Reason, ShouldEqual, "Invalid Threema Recipient ID: Must be 8 characters long") + }) + + }) + }) +} From 689f5cb686c6d74fe1ddf9d8fe8caf9286aebe73 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Tue, 7 Feb 2017 11:10:05 +0100 Subject: [PATCH 043/301] feat(alerting): Text formatting for Threema alert messages --- pkg/services/alerting/notifiers/threema.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/services/alerting/notifiers/threema.go b/pkg/services/alerting/notifiers/threema.go index f805bd24394..a710d686d28 100644 --- a/pkg/services/alerting/notifiers/threema.go +++ b/pkg/services/alerting/notifiers/threema.go @@ -127,14 +127,14 @@ func (notifier *ThreemaNotifier) Notify(evalContext *alerting.EvalContext) error data.Set("secret", notifier.APISecret) // Build message - message := fmt.Sprintf("%s\nState: %s\nMessage: %s\n", + message := fmt.Sprintf("%s\n\n*State:* %s\n*Message:* %s\n", evalContext.GetNotificationTitle(), evalContext.Rule.Name, evalContext.Rule.Message) ruleURL, err := evalContext.GetRuleUrl() if err == nil { - message = message + fmt.Sprintf("URL: %s\n", ruleURL) + message = message + fmt.Sprintf("*URL:* %s\n", ruleURL) } if evalContext.ImagePublicUrl != "" { - message = message + fmt.Sprintf("Image: %s\n", evalContext.ImagePublicUrl) + message = message + fmt.Sprintf("*Image:* %s\n", evalContext.ImagePublicUrl) } data.Set("text", message) From 8ca4afaf2254e4a311aad0ad9a05cd155105870b Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 7 Feb 2017 11:17:20 +0100 Subject: [PATCH 044/301] docs(changelog): adds note about closing #7459 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 943bb32b07d..cabf9b4fd0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ * **Elasticsearch**: Support for Min Doc Count options in Terms aggregation [#7324](https://github.com/grafana/grafana/pull/7324), thx [@lpic10](https://github.com/lpic10) * **Elasticsearch**: Term aggregation limit can now be changed in template queries [#7112](https://github.com/grafana/grafana/issues/7112), thx [@FFalcon](https://github.com/FFalcon) * **LINE**: Adds image to notification message [#7417](https://github.com/grafana/grafana/pull/7417), thx [@Erliz](https://github.com/Erliz) +* **Dataproxy**: Only allow get that begins with api/ to access Prometheus [#7459](https://github.com/grafana/grafana/pull/7459), thx [@mtanda](https://github.com/mtanda) ## Tech From b3db5aae4b63bdc6d10b7d4a1c6bfceda302a251 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 7 Feb 2017 11:21:20 +0100 Subject: [PATCH 045/301] docs(changelog): adds note about closing #6799 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cabf9b4fd0b..a47b61b9673 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ * **LINE**: Adds image to notification message [#7417](https://github.com/grafana/grafana/pull/7417), thx [@Erliz](https://github.com/Erliz) * **Dataproxy**: Only allow get that begins with api/ to access Prometheus [#7459](https://github.com/grafana/grafana/pull/7459), thx [@mtanda](https://github.com/mtanda) + ## Tech * **Library Upgrade**: Upgraded angularjs from 1.5.8 to 1.6.1 [#7274](https://github.com/grafana/grafana/issues/7274) @@ -25,6 +26,7 @@ * **Alertlist**: Only show scrollbar when required [#7269](https://github.com/grafana/grafana/issues/7269) * **SMTP**: Set LocalName to hostname [#7223](https://github.com/grafana/grafana/issues/7223) * **Sidemenu**: Disable sign out in sidemenu for AuthProxyEnabled [#7377](https://github.com/grafana/grafana/pull/7377), thx [@solugebefola](https://github.com/solugebefola) +* **Prometheus**: Add support for basic auth in Prometheus tsdb package [#6799](https://github.com/grafana/grafana/issues/6799), thx [@hagen1778](https://github.com/hagen1778) # 4.1.2 (unreleased) From 10e100b080f61a802f6c8cc6153aca86f53d3af9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 7 Feb 2017 12:27:16 +0100 Subject: [PATCH 046/301] docs(): updates --- docs/Makefile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index d3268fc6873..0dc223c6130 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -7,7 +7,7 @@ DOCKER_DOCS_IMAGE := grafana/grafana-docs SOURCES_HOST_DIR := "$(shell pwd)/sources" -DOCS_MOUNT := -v $(SOURCES_HOST_DIR):/site/content/docs +DOCS_MOUNT := -v $(SOURCES_HOST_DIR):/site/content DOCKER_RUN_DOCS := docker run --rm -it $(DOCS_MOUNT) -e NOCACHE -p 3004:3004 -p 3005:3005 @@ -15,16 +15,16 @@ DOCKER_RUN_DOCS := docker run --rm -it $(DOCS_MOUNT) -e NOCACHE -p 3004:3004 -p default: docs docs: docs-build - $(DOCKER_RUN_DOCS) $(DOCS_MOUNT) -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" /bin/bash -c "grunt && grunt connect --port=3004" + $(DOCKER_RUN_DOCS) $(DOCS_MOUNT) -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" /bin/bash -c "grunt --env=dev-docs && grunt connect --port=3004" test: docs-build - $(DOCKER_RUN_DOCS) $(DOCS_MOUNT) -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" /bin/bash -c "ls -la /site/content/docs" + $(DOCKER_RUN_DOCS) $(DOCS_MOUNT) -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" /bin/bash -c "ls -la /site/content" docs-watch: docs-build $(DOCKER_RUN_DOCS) $(DOCS_MOUNT) -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" /bin/bash -c "grunt --env=dev-docs && grunt connect --port=3004 & grunt watch --port=3004 --env=dev-docs" publish: docs-build - $(DOCKER_RUN_DOCS) $(DOCS_MOUNT) -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" /bin/bash -c "./publish.sh staging-docs v3.1" + $(DOCKER_RUN_DOCS) $(DOCS_MOUNT) -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" /bin/bash -c "./publish.sh prod-docs v3.1" publish-prod: docs-build $(DOCKER_RUN_DOCS) $(DOCS_MOUNT) -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" /bin/bash -c "./publish.sh prod-docs root" From 5bba0c2a94092ba5393d63996575469ad97f00a2 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 7 Feb 2017 12:45:06 +0100 Subject: [PATCH 047/301] tech(build): upgrade dist builder to golang1.8rc3 --- scripts/build/Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/build/Dockerfile b/scripts/build/Dockerfile index e24470fc42b..87a62a1a456 100644 --- a/scripts/build/Dockerfile +++ b/scripts/build/Dockerfile @@ -23,10 +23,10 @@ RUN curl --silent --location https://rpm.nodesource.com/setup_6.x | bash - && \ RUN wget https://dl.yarnpkg.com/rpm/yarn.repo -O /etc/yum.repos.d/yarn.repo && \ yum install -y yarn --nogpgcheck && \ - wget https://storage.googleapis.com/golang/go1.7.4.linux-amd64.tar.gz && \ - tar -C /usr/local -xzf go1.7.4.linux-amd64.tar.gz + wget https://storage.googleapis.com/golang/go1.8rc3.linux-amd64.tar.gz && \ + tar -C /usr/local -xzf go1.8rc3.linux-amd64.tar.gz -ENV GOLANG_VERSION 1.7.4 +ENV GOLANG_VERSION 1.8rc3 ENV PATH /usr/local/go/bin:$PATH RUN mkdir -p /go/src /go/bin && chmod -R 777 /go From 53942f7987ee6b07f9fae21e535efaf8c7f9ea77 Mon Sep 17 00:00:00 2001 From: James Regovich Date: Tue, 7 Feb 2017 10:50:49 -0600 Subject: [PATCH 048/301] Adding HipChat Notifier --- pkg/services/alerting/notifiers/hipchat.go | 151 ++++++++++++++++++ .../alerting/notifiers/hipchat_test.go | 81 ++++++++++ 2 files changed, 232 insertions(+) create mode 100644 pkg/services/alerting/notifiers/hipchat.go create mode 100644 pkg/services/alerting/notifiers/hipchat_test.go diff --git a/pkg/services/alerting/notifiers/hipchat.go b/pkg/services/alerting/notifiers/hipchat.go new file mode 100644 index 00000000000..8517fa0c9b4 --- /dev/null +++ b/pkg/services/alerting/notifiers/hipchat.go @@ -0,0 +1,151 @@ +package notifiers + +import ( + "encoding/json" + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/log" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/alerting" + "strconv" + "strings" + "time" +) + +func init() { + alerting.RegisterNotifier(&alerting.NotifierPlugin{ + Type: "hipchat", + Name: "HipChat", + Description: "Sends notifications uto a HipChat Room", + Factory: NewHipChatNotifier, + OptionsTemplate: ` +

HipChat settings

+
+ Hip Chat Url + +
+
+ API Key + +
+
+ Room ID + + +
+ `, + }) + +} + +func NewHipChatNotifier(model *m.AlertNotification) (alerting.Notifier, error) { + url := model.Settings.Get("url").MustString() + if strings.HasSuffix(url, "/") { + url = url[:len(url)-1] + } + if url == "" { + return nil, alerting.ValidationError{Reason: "Could not find url property in settings"} + } + + apikey := model.Settings.Get("apikey").MustString() + roomid := model.Settings.Get("roomid").MustString() + + return &HipChatNotifier{ + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + Url: url, + ApiKey: apikey, + RoomId: roomid, + log: log.New("alerting.notifier.hipchat"), + }, nil +} + +type HipChatNotifier struct { + NotifierBase + Url string + ApiKey string + RoomId string + log log.Logger +} + +func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error { + var message string + var color string + + this.log.Info("Executing hipchat notification", "ruleId", evalContext.Rule.Id, "notification", this.Name) + + ruleUrl, err := evalContext.GetRuleUrl() + if err != nil { + this.log.Error("Failed get rule link", "error", err) + return err + } + + message = evalContext.GetNotificationTitle() + " in state " + evalContext.GetStateModel().Text + "
Check Dasboard" + fields := make([]map[string]interface{}, 0) + fieldLimitCount := 4 + message += "
" + for index, evt := range evalContext.EvalMatches { + message += evt.Metric + " :: " + strconv.FormatFloat(evt.Value.Float64, 'f', -1, 64) + "
" + fields = append(fields, map[string]interface{}{ + "title": evt.Metric, + "value": evt.Value, + "short": true, + }) + if index > fieldLimitCount { + break + } + } + + if evalContext.Error != nil { + fields = append(fields, map[string]interface{}{ + "title": "Error message", + "value": evalContext.Error.Error(), + "short": false, + }) + } + + if evalContext.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok. + message += " " + evalContext.Rule.Message + } + //HipChat has a set list of colors + switch evalContext.Rule.State { + case m.AlertStateOK: + color = "green" + case m.AlertStateNoData: + color = "grey" + case m.AlertStateAlerting: + color = "red" + } + + // Add a card with link to the dashboard + card := map[string]interface{}{ + "style": "link", + "url": ruleUrl, + "id": "1", + "title": evalContext.GetNotificationTitle(), + "description": evalContext.GetNotificationTitle() + " in state " + evalContext.GetStateModel().Text, + "icon": map[string]interface{}{ + "url": "http://grafana.org/assets/img/fav32.png", + }, + "date": time.Now().Unix(), + } + + body := map[string]interface{}{ + "message": message, + "notify": "true", + "message_format": "html", + "color": color, + "card": card, + } + hipUrl := this.Url + "/v2/room/" + this.RoomId + "/notification?auth_token=" + this.ApiKey + data, _ := json.Marshal(&body) + cmd := &m.SendWebhookSync{Url: hipUrl, Body: string(data)} + + if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { + this.log.Error("Failed to send hipchat notification", "error", err, "webhook", this.Name) + return err + } + + return nil +} diff --git a/pkg/services/alerting/notifiers/hipchat_test.go b/pkg/services/alerting/notifiers/hipchat_test.go new file mode 100644 index 00000000000..1597be12eb8 --- /dev/null +++ b/pkg/services/alerting/notifiers/hipchat_test.go @@ -0,0 +1,81 @@ +package notifiers + +import ( + "testing" + + "github.com/grafana/grafana/pkg/components/simplejson" + m "github.com/grafana/grafana/pkg/models" + . "github.com/smartystreets/goconvey/convey" +) + +func TestHipChatNotifier(t *testing.T) { + Convey("HipChat notifier tests", t, func() { + + Convey("Parsing alert notification from settings", func() { + Convey("empty settings should return error", func() { + json := `{ }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "ops", + Type: "hipchat", + Settings: settingsJSON, + } + + _, err := NewHipChatNotifier(model) + So(err, ShouldNotBeNil) + }) + + Convey("from settings", func() { + json := ` + { + "url": "http://google.com" + }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "ops", + Type: "hipchat", + Settings: settingsJSON, + } + + not, err := NewHipChatNotifier(model) + hipchatNotifier := not.(*HipChatNotifier) + + So(err, ShouldBeNil) + So(hipchatNotifier.Name, ShouldEqual, "ops") + So(hipchatNotifier.Type, ShouldEqual, "hipchat") + So(hipchatNotifier.Url, ShouldEqual, "http://google.com") + So(hipchatNotifier.ApiKey, ShouldEqual, "") + So(hipchatNotifier.RoomId, ShouldEqual, "") + }) + + Convey("from settings with Recipient and Mention", func() { + json := ` + { + "url": "http://www.hipchat.com", + "apikey": "1234", + "roomid": "1234" + }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "ops", + Type: "hipchat", + Settings: settingsJSON, + } + + not, err := NewHipChatNotifier(model) + hipchatNotifier := not.(*HipChatNotifier) + + So(err, ShouldBeNil) + So(hipchatNotifier.Name, ShouldEqual, "ops") + So(hipchatNotifier.Type, ShouldEqual, "hipchat") + So(hipchatNotifier.Url, ShouldEqual, "http://www.hipchat.com") + So(hipchatNotifier.ApiKey, ShouldEqual, "1234") + So(hipchatNotifier.RoomId, ShouldEqual, "1234") + }) + + }) + }) +} From cbd1455c4208170afd0cc281df72449a8db73b45 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 7 Feb 2017 22:15:52 +0100 Subject: [PATCH 049/301] fix(api): case insensitive sort for datasources The data source list is case sensitive when sorted. This changes the sort to be case insensitive. The test only tests the handler, not the routing or database query. --- pkg/api/datasources_test.go | 124 ++++++++++++++++++++++++++++++++++++ pkg/api/dtos/models.go | 2 +- 2 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 pkg/api/datasources_test.go diff --git a/pkg/api/datasources_test.go b/pkg/api/datasources_test.go new file mode 100644 index 00000000000..f04eb6076b2 --- /dev/null +++ b/pkg/api/datasources_test.go @@ -0,0 +1,124 @@ +package api + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "path/filepath" + "testing" + + m "github.com/grafana/grafana/pkg/models" + macaron "gopkg.in/macaron.v1" + + "github.com/go-macaron/session" + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/middleware" + . "github.com/smartystreets/goconvey/convey" +) + +const ( + TestOrgID = 1 + TestUserID = 1 +) + +func TestDataSourcesProxy(t *testing.T) { + Convey("Given a user is logged in", t, func() { + loggedInUserScenario("When calling GET on", "/api/datasources/", func(sc *scenarioContext) { + + // Stubs the database query + bus.AddHandler("test", func(query *m.GetDataSourcesQuery) error { + So(query.OrgId, ShouldEqual, TestOrgID) + query.Result = []*m.DataSource{ + {Name: "mmm"}, + {Name: "ZZZ"}, + {Name: "BBB"}, + {Name: "aaa"}, + } + return nil + }) + + // handler func being tested + sc.handlerFunc = GetDataSources + sc.fakeReq("GET", "/api/datasources").exec() + + respJSON := []map[string]interface{}{} + err := json.NewDecoder(sc.resp.Body).Decode(&respJSON) + So(err, ShouldBeNil) + + Convey("should return list of datasources for org sorted alphabetically and case insensitively", func() { + So(respJSON[0]["name"], ShouldEqual, "aaa") + So(respJSON[1]["name"], ShouldEqual, "BBB") + So(respJSON[2]["name"], ShouldEqual, "mmm") + So(respJSON[3]["name"], ShouldEqual, "ZZZ") + }) + }) + }) +} + +func loggedInUserScenario(desc string, url string, fn scenarioFunc) { + Convey(desc+" "+url, func() { + defer bus.ClearBusHandlers() + + sc := &scenarioContext{} + viewsPath, _ := filepath.Abs("../../public/views") + + sc.m = macaron.New() + sc.m.Use(macaron.Renderer(macaron.RenderOptions{ + Directory: viewsPath, + Delims: macaron.Delims{Left: "[[", Right: "]]"}, + })) + + sc.m.Use(middleware.GetContextHandler()) + sc.m.Use(middleware.Sessioner(&session.Options{})) + + sc.defaultHandler = func(c *middleware.Context) { + sc.context = c + sc.context.UserId = TestUserID + sc.context.OrgId = TestOrgID + sc.context.OrgRole = m.ROLE_EDITOR + if sc.handlerFunc != nil { + sc.handlerFunc(sc.context) + } + } + sc.m.SetAutoHead(true) + sc.m.Get(url, sc.defaultHandler) + + fn(sc) + }) +} + +func (sc *scenarioContext) fakeReq(method, url string) *scenarioContext { + sc.resp = httptest.NewRecorder() + req, err := http.NewRequest(method, url, nil) + So(err, ShouldBeNil) + sc.req = req + + return sc +} + +type scenarioContext struct { + m *macaron.Macaron + context *middleware.Context + resp *httptest.ResponseRecorder + apiKey string + authHeader string + handlerFunc handlerFunc + defaultHandler macaron.Handler + + req *http.Request +} + +func (sc *scenarioContext) exec() { + if sc.apiKey != "" { + sc.req.Header.Add("Authorization", "Bearer "+sc.apiKey) + } + + if sc.authHeader != "" { + sc.req.Header.Add("Authorization", sc.authHeader) + } + + sc.m.ServeHTTP(sc.resp, sc.req) +} + +type scenarioFunc func(c *scenarioContext) +type handlerFunc func(c *middleware.Context) diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index 13dbe4ea32a..564d86a25bf 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -91,7 +91,7 @@ func (slice DataSourceList) Len() int { } func (slice DataSourceList) Less(i, j int) bool { - return slice[i].Name < slice[j].Name + return strings.ToLower(slice[i].Name) < strings.ToLower(slice[j].Name) } func (slice DataSourceList) Swap(i, j int) { From 8aa5b62d6dfb45fd800d9c132f9c869e7d15fced Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 8 Feb 2017 00:01:42 +0100 Subject: [PATCH 050/301] fix(panel): case insensitive sort metric sources Sorts the list of metric sources that is used in dropdown for Panel Data Source on the Metrics tab so that it is case insensitive and so that the built data sources are last in the list. --- public/app/core/services/datasource_srv.js | 13 ++++- public/test/specs/datasource_srv_specs.js | 58 ++++++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 public/test/specs/datasource_srv_specs.js diff --git a/public/app/core/services/datasource_srv.js b/public/app/core/services/datasource_srv.js index b8b94cd286d..7c876ebd941 100644 --- a/public/app/core/services/datasource_srv.js +++ b/public/app/core/services/datasource_srv.js @@ -97,10 +97,19 @@ function (angular, _, coreModule, config) { } metricSources.sort(function(a, b) { - if (a.meta.builtIn || a.name > b.name) { + if (a.meta.builtIn) { return 1; } - if (a.name < b.name) { + + if (b.meta.builtIn) { + return -1; + } + + if (a.name.toLowerCase() > b.name.toLowerCase()) { + return 1; + } + + if (a.name.toLowerCase() < b.name.toLowerCase()) { return -1; } return 0; diff --git a/public/test/specs/datasource_srv_specs.js b/public/test/specs/datasource_srv_specs.js new file mode 100644 index 00000000000..be7c054acaa --- /dev/null +++ b/public/test/specs/datasource_srv_specs.js @@ -0,0 +1,58 @@ +define([ + 'app/core/config', + 'app/core/services/datasource_srv' +], function(config) { + 'use strict'; + + describe('datasource_srv', function() { + var _datasourceSrv; + var metricSources; + var templateSrv = {}; + + beforeEach(module('grafana.core')); + beforeEach(module(function($provide) { + $provide.value('templateSrv', templateSrv); + })); + beforeEach(module('grafana.services')); + beforeEach(inject(function(datasourceSrv) { + _datasourceSrv = datasourceSrv; + })); + + describe('when loading metric sources', function() { + var unsortedDatasources = { + 'mmm': { + type: 'test-db', + meta: { metrics: {m: 1} } + }, + '--Mixed--': { + type: 'test-db', + meta: {builtIn: true, metrics: {m: 1} } + }, + 'ZZZ': { + type: 'test-db', + meta: {metrics: {m: 1} } + }, + 'aaa': { + type: 'test-db', + meta: { metrics: {m: 1} } + }, + 'BBB': { + type: 'test-db', + meta: { metrics: {m: 1} } + }, + }; + beforeEach(function() { + config.datasources = unsortedDatasources; + metricSources = _datasourceSrv.getMetricSources({skipVariables: true}); + }); + + it('should return a list of sources sorted case insensitively with builtin sources last', function() { + expect(metricSources[0].name).to.be('aaa'); + expect(metricSources[1].name).to.be('BBB'); + expect(metricSources[2].name).to.be('mmm'); + expect(metricSources[3].name).to.be('ZZZ'); + expect(metricSources[4].name).to.be('--Mixed--'); + }); + }); + }); +}); From 0a17217d59067af08bfc463fc73735bb4ba466be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 8 Feb 2017 08:23:12 +0100 Subject: [PATCH 051/301] docs(): updating links --- docs/Makefile | 2 +- docs/sources/alerting/rules.md | 2 +- docs/sources/guides/basic_concepts.md | 2 +- docs/sources/index.md | 18 +++++++++--------- docs/sources/installation/debian.md | 8 ++++---- docs/sources/installation/rpm.md | 8 ++++---- docs/sources/plugins/development.md | 2 +- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index 0dc223c6130..e5c555f8019 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -24,7 +24,7 @@ docs-watch: docs-build $(DOCKER_RUN_DOCS) $(DOCS_MOUNT) -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" /bin/bash -c "grunt --env=dev-docs && grunt connect --port=3004 & grunt watch --port=3004 --env=dev-docs" publish: docs-build - $(DOCKER_RUN_DOCS) $(DOCS_MOUNT) -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" /bin/bash -c "./publish.sh prod-docs v3.1" + $(DOCKER_RUN_DOCS) $(DOCS_MOUNT) -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" /bin/bash -c "./publish.sh staging-docs root" publish-prod: docs-build $(DOCKER_RUN_DOCS) $(DOCS_MOUNT) -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" /bin/bash -c "./publish.sh prod-docs root" diff --git a/docs/sources/alerting/rules.md b/docs/sources/alerting/rules.md index aa38367b4aa..a2688de3100 100644 --- a/docs/sources/alerting/rules.md +++ b/docs/sources/alerting/rules.md @@ -33,7 +33,7 @@ of core Grafana. Only some data soures are supported right now. They include `Gr ### Clustering We have not implemented clustering yet. So if you run multiple instances of grafana-server -you have to make sure [execute_alerts]({{< relref "docs/installation/configuration.md#alerting" >}}) +you have to make sure [execute_alerts]({{< relref "installation/configuration.md#alerting" >}}) is true on only one instance or otherwise you will get duplicated notifications.
diff --git a/docs/sources/guides/basic_concepts.md b/docs/sources/guides/basic_concepts.md index 0411db6caf2..112b004ec92 100644 --- a/docs/sources/guides/basic_concepts.md +++ b/docs/sources/guides/basic_concepts.md @@ -16,7 +16,7 @@ This document is a “bottom up” introduction to basic concepts in Grafana, an ### Data Source Grafana supports many different storage backends for your time series data (Data Source). Each Data Source has a specific Query Editor that is customized for the features and capabilities that the particular Data Source exposes. -The following datasources are officially supported: [Graphite]({{< relref "docs/features/datasources/graphite.md" >}}), [InfluxDB]({{< relref "docs/features/datasources/influxdb.md" >}}), [OpenTSDB]({{< relref "docs/features/datasources/opentsdb.md" >}}), [Prometheus]({{< relref "docs/features/datasources/prometheus.md" >}}), [Elasticsearch]({{< relref "docs/features/datasources/elasticsearch.md" >}}), [CloudWatch]({{< relref "docs/features/datasources/cloudwatch.md" >}}). +The following datasources are officially supported: [Graphite]({{< relref "features/datasources/graphite.md" >}}), [InfluxDB]({{< relref "features/datasources/influxdb.md" >}}), [OpenTSDB]({{< relref "features/datasources/opentsdb.md" >}}), [Prometheus]({{< relref "features/datasources/prometheus.md" >}}), [Elasticsearch]({{< relref "features/datasources/elasticsearch.md" >}}), [CloudWatch]({{< relref "features/datasources/cloudwatch.md" >}}). The query language and capabilities of each Data Source are obviously very different. You can combine data from multiple Data Sources onto a single Dashboard, but each Panel is tied to a specific Data Source that belongs to a particular Organization. diff --git a/docs/sources/index.md b/docs/sources/index.md index ab2e0f882b8..eb2c491d78e 100644 --- a/docs/sources/index.md +++ b/docs/sources/index.md @@ -24,27 +24,27 @@ other domains including industrial sensors, home automation, weather, and proces - [Installing using Provisioning (Chef, Puppet, Salt, Ansible, etc)](installation/provisioning) - [Nightly Builds](http://grafana.org/builds) -For other platforms Read the [build from source]({{< relref "docs/project/building_from_source.md" >}}) +For other platforms Read the [build from source]({{< relref "project/building_from_source.md" >}}) instructions for more information. ## Configuring Grafana The back-end web server has a number of configuration options. Go the -[Configuration]({{< relref "docs/installation/configuration.md" >}}) page for details on all +[Configuration]({{< relref "installation/configuration.md" >}}) page for details on all those options. ## Getting started -- [Getting Started]({{< relref "docs/guides/getting_started.md" >}}) -- [Basic Concepts]({{< relref "docs/guides/basic_concepts.md" >}}) -- [Screencasts]({{< relref "docs/tutorials/screencasts.md" >}}) +- [Getting Started]({{< relref "guides/getting_started.md" >}}) +- [Basic Concepts]({{< relref "guides/basic_concepts.md" >}}) +- [Screencasts]({{< relref "tutorials/screencasts.md" >}}) ## Data sources guides -- [Graphite]({{< relref "docs/features/datasources/graphite.md" >}}) -- [Elasticsearch]({{< relref "docs/features/datasources/elasticsearch.md" >}}) -- [InfluxDB]({{< relref "docs/features/datasources/influxdb.md" >}}) -- [OpenTSDB]({{< relref "docs/features/datasources/opentsdb.md" >}}) +- [Graphite]({{< relref "features/datasources/graphite.md" >}}) +- [Elasticsearch]({{< relref "features/datasources/elasticsearch.md" >}}) +- [InfluxDB]({{< relref "features/datasources/influxdb.md" >}}) +- [OpenTSDB]({{< relref "features/datasources/opentsdb.md" >}}) diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index 7dd97e69810..9c6140c0c6d 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -114,10 +114,10 @@ those options. ### Adding data sources -- [Graphite]({{< relref "docs/features/datasources/graphite.md" >}}) -- [InfluxDB]({{< relref "docs/features/datasources/influxdb.md" >}}) -- [OpenTSDB]({{< relref "docs/features/datasources/opentsdb.md" >}}) -- [Prometheus]({{< relref "docs/features/datasources/prometheus.md" >}}) +- [Graphite]({{< relref "features/datasources/graphite.md" >}}) +- [InfluxDB]({{< relref "features/datasources/influxdb.md" >}}) +- [OpenTSDB]({{< relref "features/datasources/opentsdb.md" >}}) +- [Prometheus]({{< relref "features/datasources/prometheus.md" >}}) ## Installing from binary tar file diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md index 561cff69b46..63181fc535d 100644 --- a/docs/sources/installation/rpm.md +++ b/docs/sources/installation/rpm.md @@ -121,10 +121,10 @@ those options. ### Adding data sources -- [Graphite]({{< relref "docs/features/datasources/graphite.md" >}}) -- [InfluxDB]({{< relref "docs/features/datasources/influxdb.md" >}}) -- [OpenTSDB]({{< relref "docs/features/datasources/opentsdb.md" >}}) -- [Prometheus]({{< relref "docs/features/datasources/prometheus.md" >}}) +- [Graphite]({{< relref "features/datasources/graphite.md" >}}) +- [InfluxDB]({{< relref "features/datasources/influxdb.md" >}}) +- [OpenTSDB]({{< relref "features/datasources/opentsdb.md" >}}) +- [Prometheus]({{< relref "features/datasources/prometheus.md" >}}) ### Server side image rendering diff --git a/docs/sources/plugins/development.md b/docs/sources/plugins/development.md index edb959e97bc..b4573743a39 100644 --- a/docs/sources/plugins/development.md +++ b/docs/sources/plugins/development.md @@ -40,7 +40,7 @@ and [apps](./apps.md) plugins in the documentation. There are three ways that you can start developing a Grafana plugin. 1. Setup a Grafana development environment. [(described here)](http://docs.grafana.org/project/building_from_source/) and place your plugin in the ```data/plugins``` folder. -2. Install Grafana and place your plugin in the plugins directory which is set in your [config file]({{< relref "docs/installation/configuration.md" >}}). By default this is `/var/lib/grafana/plugins` on Linux systems. +2. Install Grafana and place your plugin in the plugins directory which is set in your [config file]({{< relref "installation/configuration.md" >}}). By default this is `/var/lib/grafana/plugins` on Linux systems. 3. Place your plugin directory anywhere you like and specify it grafana.ini. We encourage people to setup the full Grafana environment so that you can get inspiration from the rest of grafana code base. From 00549f393cedbccf16deca310fc2f3e2a789f69a Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 8 Feb 2017 11:24:49 +0100 Subject: [PATCH 052/301] style(hipchat): improves code style --- pkg/services/alerting/notifiers/hipchat.go | 54 ++++++++++--------- .../app/features/alerting/alert_tab_ctrl.ts | 1 + 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/pkg/services/alerting/notifiers/hipchat.go b/pkg/services/alerting/notifiers/hipchat.go index 8517fa0c9b4..ad649cb084d 100644 --- a/pkg/services/alerting/notifiers/hipchat.go +++ b/pkg/services/alerting/notifiers/hipchat.go @@ -2,13 +2,15 @@ package notifiers import ( "encoding/json" - "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/log" - m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/alerting" "strconv" "strings" - "time" + + "fmt" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/alerting" ) func init() { @@ -20,15 +22,15 @@ func init() { OptionsTemplate: `

HipChat settings

- Hip Chat Url - + Hip Chat Url +
- API Key + API Key
- Room ID + Room ID Check Dasboard" + message := evalContext.GetNotificationTitle() + " in state " + evalContext.GetStateModel().Text + "
Check Dasboard" fields := make([]map[string]interface{}, 0) - fieldLimitCount := 4 message += "
" for index, evt := range evalContext.EvalMatches { message += evt.Metric + " :: " + strconv.FormatFloat(evt.Value.Float64, 'f', -1, 64) + "
" @@ -92,7 +94,7 @@ func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error { "value": evt.Value, "short": true, }) - if index > fieldLimitCount { + if index > maxFieldCount { break } } @@ -105,16 +107,17 @@ func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error { }) } - if evalContext.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok. + if evalContext.Rule.State != models.AlertStateOK { //dont add message when going back to alert state ok. message += " " + evalContext.Rule.Message } //HipChat has a set list of colors + var color string switch evalContext.Rule.State { - case m.AlertStateOK: + case models.AlertStateOK: color = "green" - case m.AlertStateNoData: + case models.AlertStateNoData: color = "grey" - case m.AlertStateAlerting: + case models.AlertStateAlerting: color = "red" } @@ -128,7 +131,7 @@ func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error { "icon": map[string]interface{}{ "url": "http://grafana.org/assets/img/fav32.png", }, - "date": time.Now().Unix(), + "date": evalContext.EndTime.Unix(), } body := map[string]interface{}{ @@ -138,9 +141,10 @@ func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error { "color": color, "card": card, } - hipUrl := this.Url + "/v2/room/" + this.RoomId + "/notification?auth_token=" + this.ApiKey + + hipUrl := fmt.Sprintf("%s/v2/room/%s/notification?auth_token=%s", this.Url, this.RoomId, this.ApiKey) data, _ := json.Marshal(&body) - cmd := &m.SendWebhookSync{Url: hipUrl, Body: string(data)} + cmd := &models.SendWebhookSync{Url: hipUrl, Body: string(data)} if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { this.log.Error("Failed to send hipchat notification", "error", err, "webhook", this.Name) diff --git a/public/app/features/alerting/alert_tab_ctrl.ts b/public/app/features/alerting/alert_tab_ctrl.ts index 89b1c63372f..6be36380144 100644 --- a/public/app/features/alerting/alert_tab_ctrl.ts +++ b/public/app/features/alerting/alert_tab_ctrl.ts @@ -104,6 +104,7 @@ export class AlertTabCtrl { case "webhook": return "fa fa-cubes"; case "pagerduty": return "fa fa-bullhorn"; case "opsgenie": return "fa fa-bell"; + case "hipchat": return "fa fa-mail-forward"; } } From 10508d02384bb12838ae92530a48e5b5d8e6a71f Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 8 Feb 2017 11:28:26 +0100 Subject: [PATCH 053/301] docs(changelog): adds note about closing #6451 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a47b61b9673..946fae9b946 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ * **Elasticsearch**: Term aggregation limit can now be changed in template queries [#7112](https://github.com/grafana/grafana/issues/7112), thx [@FFalcon](https://github.com/FFalcon) * **LINE**: Adds image to notification message [#7417](https://github.com/grafana/grafana/pull/7417), thx [@Erliz](https://github.com/Erliz) * **Dataproxy**: Only allow get that begins with api/ to access Prometheus [#7459](https://github.com/grafana/grafana/pull/7459), thx [@mtanda](https://github.com/mtanda) - +* **Hipchat**: Adds support for sending alert notifications to hipchat [#6451](https://github.com/grafana/grafana/issues/6451), thx [@jregovic](https://github.com/jregovic) ## Tech From fbc3c3dd327fee330e7d77f3703d05c0d58b9526 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 8 Feb 2017 11:57:05 +0100 Subject: [PATCH 054/301] api: removes import alias + some unused fields --- pkg/api/datasources_test.go | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/pkg/api/datasources_test.go b/pkg/api/datasources_test.go index f04eb6076b2..aa3f6106afd 100644 --- a/pkg/api/datasources_test.go +++ b/pkg/api/datasources_test.go @@ -7,7 +7,7 @@ import ( "path/filepath" "testing" - m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/models" macaron "gopkg.in/macaron.v1" "github.com/go-macaron/session" @@ -26,9 +26,9 @@ func TestDataSourcesProxy(t *testing.T) { loggedInUserScenario("When calling GET on", "/api/datasources/", func(sc *scenarioContext) { // Stubs the database query - bus.AddHandler("test", func(query *m.GetDataSourcesQuery) error { + bus.AddHandler("test", func(query *models.GetDataSourcesQuery) error { So(query.OrgId, ShouldEqual, TestOrgID) - query.Result = []*m.DataSource{ + query.Result = []*models.DataSource{ {Name: "mmm"}, {Name: "ZZZ"}, {Name: "BBB"}, @@ -75,7 +75,7 @@ func loggedInUserScenario(desc string, url string, fn scenarioFunc) { sc.context = c sc.context.UserId = TestUserID sc.context.OrgId = TestOrgID - sc.context.OrgRole = m.ROLE_EDITOR + sc.context.OrgRole = models.ROLE_EDITOR if sc.handlerFunc != nil { sc.handlerFunc(sc.context) } @@ -100,23 +100,12 @@ type scenarioContext struct { m *macaron.Macaron context *middleware.Context resp *httptest.ResponseRecorder - apiKey string - authHeader string handlerFunc handlerFunc defaultHandler macaron.Handler - - req *http.Request + req *http.Request } func (sc *scenarioContext) exec() { - if sc.apiKey != "" { - sc.req.Header.Add("Authorization", "Bearer "+sc.apiKey) - } - - if sc.authHeader != "" { - sc.req.Header.Add("Authorization", sc.authHeader) - } - sc.m.ServeHTTP(sc.resp, sc.req) } From bccb6500109fa6ef34ac6731e50299a2fd2fa616 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 8 Feb 2017 12:06:00 +0100 Subject: [PATCH 055/301] Adds Sorting for lists of data sources to 4.2.0 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 946fae9b946..9207be556e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ * **LINE**: Adds image to notification message [#7417](https://github.com/grafana/grafana/pull/7417), thx [@Erliz](https://github.com/Erliz) * **Dataproxy**: Only allow get that begins with api/ to access Prometheus [#7459](https://github.com/grafana/grafana/pull/7459), thx [@mtanda](https://github.com/mtanda) * **Hipchat**: Adds support for sending alert notifications to hipchat [#6451](https://github.com/grafana/grafana/issues/6451), thx [@jregovic](https://github.com/jregovic) +* **Data Sources**: Sorting for lists of data sources in UI is now case insensitive [#7491](https://github.com/grafana/grafana/issues/7491) ## Tech From c05c6ee7a48c0fb6cce765e5ee87ad6830441939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 8 Feb 2017 20:00:39 +0100 Subject: [PATCH 056/301] docs(): minor fix --- docs/sources/administration/cli.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/administration/cli.md b/docs/sources/administration/cli.md index 4f2bbde3606..ebe910503aa 100644 --- a/docs/sources/administration/cli.md +++ b/docs/sources/administration/cli.md @@ -17,7 +17,7 @@ executed on the same machine as grafana runs. The CLI helps you install, upgrade and manage your plugins on the same machine it CLI is running. You can find more information about how to install and manage your plugins at the -[plugin page]({{< relref "docs/plugins/installation.md" >}}). +[plugin page]({{< relref "plugins/installation.md" >}}). ## Admin From aa7292fac61d328788e671935820a7a437d1c1b9 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 7 Feb 2017 17:45:36 +0100 Subject: [PATCH 057/301] mqe: adds support for wildcard and index aliases --- pkg/tsdb/mqe/httpClient.go | 2 +- pkg/tsdb/mqe/mqe.go | 1 + pkg/tsdb/mqe/response_parser.go | 100 +++++++++++++++++++++++---- pkg/tsdb/mqe/response_parser_test.go | 68 ++++++++++++++++-- pkg/tsdb/mqe/types.go | 2 + 5 files changed, 153 insertions(+), 20 deletions(-) diff --git a/pkg/tsdb/mqe/httpClient.go b/pkg/tsdb/mqe/httpClient.go index c4a7807f9cb..d8bf0888a35 100644 --- a/pkg/tsdb/mqe/httpClient.go +++ b/pkg/tsdb/mqe/httpClient.go @@ -86,7 +86,7 @@ func (e *apiClient) spawnWorker(ctx context.Context, id int, jobs chan QueryToSe return } - series, err := e.responseParser.Parse(resp, query.QueryRef) + series, err := e.responseParser.Parse(resp, query) if err != nil { errors <- err return diff --git a/pkg/tsdb/mqe/mqe.go b/pkg/tsdb/mqe/mqe.go index 5d5f0950de5..ae828e95c20 100644 --- a/pkg/tsdb/mqe/mqe.go +++ b/pkg/tsdb/mqe/mqe.go @@ -40,6 +40,7 @@ func init() { type QueryToSend struct { RawQuery string + Metric Metric QueryRef *Query } diff --git a/pkg/tsdb/mqe/response_parser.go b/pkg/tsdb/mqe/response_parser.go index d40106fc197..8cfa7d96cdd 100644 --- a/pkg/tsdb/mqe/response_parser.go +++ b/pkg/tsdb/mqe/response_parser.go @@ -4,9 +4,13 @@ import ( "encoding/json" "io/ioutil" "net/http" + "strconv" + "strings" "fmt" + "regexp" + "github.com/grafana/grafana/pkg/components/null" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/tsdb" @@ -18,6 +22,16 @@ func NewResponseParser() *ResponseParser { } } +var ( + indexAliasPattern *regexp.Regexp + wildcardAliasPattern *regexp.Regexp +) + +func init() { + indexAliasPattern = regexp.MustCompile(`\$(\d)`) + wildcardAliasPattern = regexp.MustCompile(`[*!]`) +} + type MQEResponse struct { Success bool `json:"success"` Name string `json:"name"` @@ -47,7 +61,7 @@ type ResponseParser struct { log log.Logger } -func (parser *ResponseParser) Parse(res *http.Response, queryRef *Query) ([]*tsdb.TimeSeries, error) { +func (parser *ResponseParser) Parse(res *http.Response, queryRef QueryToSend) ([]*tsdb.TimeSeries, error) { body, err := ioutil.ReadAll(res.Body) defer res.Body.Close() if err != nil { @@ -73,22 +87,14 @@ func (parser *ResponseParser) Parse(res *http.Response, queryRef *Query) ([]*tsd var series []*tsdb.TimeSeries for _, body := range data.Body { for _, mqeSerie := range body.Series { - namePrefix := "" - - //append predefined tags to seriename - for key, value := range mqeSerie.Tagset { - if key == "cluster" && queryRef.AddClusterToAlias { - namePrefix += value + " " - } + serie := &tsdb.TimeSeries{ + Tags: map[string]string{}, + Name: parser.formatLegend(body, mqeSerie, queryRef), } for key, value := range mqeSerie.Tagset { - if key == "host" && queryRef.AddHostToAlias { - namePrefix += value + " " - } + serie.Tags[key] = value } - serie := &tsdb.TimeSeries{Name: namePrefix + body.Name} - for i, value := range mqeSerie.Values { timestamp := body.TimeRange.Start + int64(i)*body.TimeRange.Resolution serie.Points = append(serie.Points, tsdb.NewTimePoint(value, float64(timestamp))) @@ -100,3 +106,71 @@ func (parser *ResponseParser) Parse(res *http.Response, queryRef *Query) ([]*tsd return series, nil } + +func (parser *ResponseParser) formatLegend(body MQEResponseSerie, mqeSerie MQESerie, queryToSend QueryToSend) string { + namePrefix := "" + + //append predefined tags to seriename + for key, value := range mqeSerie.Tagset { + if key == "cluster" && queryToSend.QueryRef.AddClusterToAlias { + namePrefix += value + " " + } + } + for key, value := range mqeSerie.Tagset { + if key == "host" && queryToSend.QueryRef.AddHostToAlias { + namePrefix += value + " " + } + } + + return namePrefix + parser.formatName(body, queryToSend) +} + +func (parser *ResponseParser) formatName(body MQEResponseSerie, queryToSend QueryToSend) string { + if indexAliasPattern.MatchString(queryToSend.Metric.Alias) { + return parser.indexAlias(body, queryToSend) + } + + if wildcardAliasPattern.MatchString(queryToSend.Metric.Metric) && wildcardAliasPattern.MatchString(queryToSend.Metric.Alias) { + return parser.wildcardAlias(body, queryToSend) + } + + return body.Name +} + +func (parser *ResponseParser) wildcardAlias(body MQEResponseSerie, queryToSend QueryToSend) string { + regString := strings.Replace(queryToSend.Metric.Metric, `*`, `(.*)`, 1) + reg, err := regexp.Compile(regString) + if err != nil { + return queryToSend.Metric.Alias + } + + matches := reg.FindAllStringSubmatch(queryToSend.RawQuery, -1) + + if len(matches) == 0 || len(matches[0]) < 2 { + return queryToSend.Metric.Alias + } + + return matches[0][1] +} + +func (parser *ResponseParser) indexAlias(body MQEResponseSerie, queryToSend QueryToSend) string { + queryNameParts := strings.Split(body.Name, `.`) + name := indexAliasPattern.ReplaceAllStringFunc(queryToSend.Metric.Alias, func(in string) string { + positionName := strings.TrimSpace(strings.Replace(in, "$", "", 1)) + + pos, err := strconv.Atoi(positionName) + if err != nil { + return "" + } + + for i, part := range queryNameParts { + if i == pos-1 { + return strings.TrimSpace(part) + } + } + + return "" + }) + + return strings.Replace(name, " ", ".", -1) +} diff --git a/pkg/tsdb/mqe/response_parser_test.go b/pkg/tsdb/mqe/response_parser_test.go index 63761841d97..237c25aa8fc 100644 --- a/pkg/tsdb/mqe/response_parser_test.go +++ b/pkg/tsdb/mqe/response_parser_test.go @@ -8,11 +8,12 @@ import ( "io/ioutil" + "github.com/grafana/grafana/pkg/components/null" . "github.com/smartystreets/goconvey/convey" ) var ( - dummieJson string + testJson string ) func TestMQEResponseParser(t *testing.T) { @@ -20,14 +21,17 @@ func TestMQEResponseParser(t *testing.T) { parser := NewResponseParser() Convey("Can parse response", func() { - queryRef := &Query{ - AddClusterToAlias: true, - AddHostToAlias: true, + queryRef := QueryToSend{ + QueryRef: &Query{ + AddClusterToAlias: true, + AddHostToAlias: true, + }, + Metric: Metric{Alias: ""}, } response := &http.Response{ StatusCode: 200, - Body: ioutil.NopCloser(strings.NewReader(dummieJson)), + Body: ioutil.NopCloser(strings.NewReader(testJson)), } res, err := parser.Parse(response, queryRef) So(err, ShouldBeNil) @@ -39,12 +43,64 @@ func TestMQEResponseParser(t *testing.T) { So(res[0].Points[i][0].Float64, ShouldEqual, i+1) So(res[0].Points[i][1].Float64, ShouldEqual, startTime+(i*30000)) } + + }) + + Convey("Can format legend", func() { + mqeSerie := MQESerie{ + Tagset: map[string]string{ + "cluster": "demoapp", + "host": "staples-lab-1", + }, + Values: []null.Float{null.NewFloat(3, true)}, + } + + Convey("with empty alias", func() { + serie := MQEResponseSerie{Name: "os.disk.sda3.weighted_io_time"} + queryRef := QueryToSend{ + QueryRef: &Query{ + AddClusterToAlias: true, + AddHostToAlias: true, + }, + Metric: Metric{Alias: ""}, + } + legend := parser.formatLegend(serie, mqeSerie, queryRef) + So(legend, ShouldEqual, "demoapp staples-lab-1 os.disk.sda3.weighted_io_time") + }) + + Convey("with index alias (ex $2 $3)", func() { + serie := MQEResponseSerie{Name: "os.disk.sda3.weighted_io_time"} + queryRef := QueryToSend{ + QueryRef: &Query{ + AddClusterToAlias: true, + AddHostToAlias: true, + }, + Metric: Metric{Alias: "$2 $3"}, + } + legend := parser.formatLegend(serie, mqeSerie, queryRef) + So(legend, ShouldEqual, "demoapp staples-lab-1 disk.sda3") + }) + + Convey("with wildcard alias", func() { + serie := MQEResponseSerie{Name: "os.disk.sda3.weighted_io_time", Query: "os.disk.*"} + + queryRef := QueryToSend{ + QueryRef: &Query{ + AddClusterToAlias: true, + AddHostToAlias: true, + }, + RawQuery: "os.disk.sda3.weighted_io_time", + Metric: Metric{Alias: "*", Metric: "os.disk.*.weighted_io_time"}, + } + legend := parser.formatLegend(serie, mqeSerie, queryRef) + So(legend, ShouldEqual, "demoapp staples-lab-1 sda3") + }) }) }) } func init() { - dummieJson = `{ + testJson = `{ "success": true, "name": "select", "body": [ diff --git a/pkg/tsdb/mqe/types.go b/pkg/tsdb/mqe/types.go index 4fa2c1d4e7f..0bd436ee9bd 100644 --- a/pkg/tsdb/mqe/types.go +++ b/pkg/tsdb/mqe/types.go @@ -53,6 +53,7 @@ func (q *Query) Build(availableSeries []string) ([]QueryToSend, error) { queriesToSend = append(queriesToSend, QueryToSend{ RawQuery: rawQuery, QueryRef: q, + Metric: metric, }) } else { m := strings.Replace(metric.Metric, "*", ".*", -1) @@ -70,6 +71,7 @@ func (q *Query) Build(availableSeries []string) ([]QueryToSend, error) { queriesToSend = append(queriesToSend, QueryToSend{ RawQuery: rawQuery, QueryRef: q, + Metric: metric, }) } } From c7febca447b5013014d710b04718ac933505a730 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 9 Feb 2017 16:43:57 +0100 Subject: [PATCH 058/301] mqe: fixes broken test for indexalias --- pkg/tsdb/mqe/response_parser.go | 3 ++- pkg/tsdb/mqe/response_parser_test.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/tsdb/mqe/response_parser.go b/pkg/tsdb/mqe/response_parser.go index 8cfa7d96cdd..f3fdb00f0aa 100644 --- a/pkg/tsdb/mqe/response_parser.go +++ b/pkg/tsdb/mqe/response_parser.go @@ -154,7 +154,8 @@ func (parser *ResponseParser) wildcardAlias(body MQEResponseSerie, queryToSend Q } func (parser *ResponseParser) indexAlias(body MQEResponseSerie, queryToSend QueryToSend) string { - queryNameParts := strings.Split(body.Name, `.`) + queryNameParts := strings.Split(queryToSend.Metric.Metric, `.`) + name := indexAliasPattern.ReplaceAllStringFunc(queryToSend.Metric.Alias, func(in string) string { positionName := strings.TrimSpace(strings.Replace(in, "$", "", 1)) diff --git a/pkg/tsdb/mqe/response_parser_test.go b/pkg/tsdb/mqe/response_parser_test.go index 237c25aa8fc..34259aaea48 100644 --- a/pkg/tsdb/mqe/response_parser_test.go +++ b/pkg/tsdb/mqe/response_parser_test.go @@ -75,7 +75,7 @@ func TestMQEResponseParser(t *testing.T) { AddClusterToAlias: true, AddHostToAlias: true, }, - Metric: Metric{Alias: "$2 $3"}, + Metric: Metric{Alias: "$2 $3", Metric: "os.disk.sda3.weighted_io_time"}, } legend := parser.formatLegend(serie, mqeSerie, queryRef) So(legend, ShouldEqual, "demoapp staples-lab-1 disk.sda3") From b22881c71742f693874ae4c792c546b78e345298 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Thu, 9 Feb 2017 14:01:53 -0500 Subject: [PATCH 059/301] redirect user to requested url after login via oauth --- pkg/api/login_oauth.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index 574d08af09f..2bf9c484803 100644 --- a/pkg/api/login_oauth.go +++ b/pkg/api/login_oauth.go @@ -10,6 +10,7 @@ import ( "io/ioutil" "log" "net/http" + "net/url" "golang.org/x/net/context" "golang.org/x/oauth2" @@ -177,5 +178,11 @@ func OAuthLogin(ctx *middleware.Context) { metrics.M_Api_Login_OAuth.Inc(1) + if redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")); len(redirectTo) > 0 { + ctx.SetCookie("redirect_to", "", -1, setting.AppSubUrl+"/") + ctx.Redirect(redirectTo) + return + } + ctx.Redirect(setting.AppSubUrl + "/") } From 5dd961c0f56f941d30a31b86268644fc3bb8c6df Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 10 Feb 2017 07:39:26 +0100 Subject: [PATCH 060/301] settings: remove commented code --- pkg/setting/setting.go | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 9c2ba6ee13c..a5f46170db5 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -455,22 +455,6 @@ func validateStaticRootPath() error { return fmt.Errorf("Failed to detect generated css or javascript files in static root (%s), have you executed default grunt task?", StaticRootPath) } -// func readInstanceName() string { -// hostname, _ := os.Hostname() -// if hostname == "" { -// hostname = "hostname_unknown" -// } -// -// instanceName := Cfg.Section("").Key("instance_name").MustString("") -// if instanceName = "" { -// // set value as it might be used in other places -// Cfg.Section("").Key("instance_name").SetValue(hostname) -// instanceName = hostname -// } -// -// return -// } - func NewConfigContext(args *CommandLineArgs) error { setHomePath(args) loadConfiguration(args) From 38c29d2209508a3bed2e8f25c1c9318e242fa282 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 10 Feb 2017 08:29:44 +0100 Subject: [PATCH 061/301] changelog: adds note about closing #7513 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9207be556e3..6e6464c9bda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ * **SMTP**: Set LocalName to hostname [#7223](https://github.com/grafana/grafana/issues/7223) * **Sidemenu**: Disable sign out in sidemenu for AuthProxyEnabled [#7377](https://github.com/grafana/grafana/pull/7377), thx [@solugebefola](https://github.com/solugebefola) * **Prometheus**: Add support for basic auth in Prometheus tsdb package [#6799](https://github.com/grafana/grafana/issues/6799), thx [@hagen1778](https://github.com/hagen1778) +* **OAuth**: Redirect to original page when logging in with OAuth [#7513](https://github.com/grafana/grafana/issues/7513) # 4.1.2 (unreleased) From 6eda5604d2d750af0423f129ef700dc0674730d7 Mon Sep 17 00:00:00 2001 From: Vladimir Kolobaev Date: Fri, 10 Feb 2017 11:31:51 +0400 Subject: [PATCH 062/301] Full metric description (#7493) graph: add full metric description for graph-legend-alias title --- public/app/plugins/panel/graph/legend.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/graph/legend.js b/public/app/plugins/panel/graph/legend.js index cf652950485..fd5abfb2db2 100644 --- a/public/app/plugins/panel/graph/legend.js +++ b/public/app/plugins/panel/graph/legend.js @@ -167,7 +167,7 @@ function (angular, _, $) { html += ''; html += '
'; - html += '' + _.escape(series.label) + ''; + html += '' + _.escape(series.label) + ''; if (panel.legend.values) { var avg = series.formatValue(series.stats.avg); From 7bf570532f307f4a61ff24f5a70188e5b69f0b31 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 10 Feb 2017 08:34:48 +0100 Subject: [PATCH 063/301] docs: adds note about closing #7493 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e6464c9bda..8373d237096 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ * **Dataproxy**: Only allow get that begins with api/ to access Prometheus [#7459](https://github.com/grafana/grafana/pull/7459), thx [@mtanda](https://github.com/mtanda) * **Hipchat**: Adds support for sending alert notifications to hipchat [#6451](https://github.com/grafana/grafana/issues/6451), thx [@jregovic](https://github.com/jregovic) * **Data Sources**: Sorting for lists of data sources in UI is now case insensitive [#7491](https://github.com/grafana/grafana/issues/7491) +* **Graph**: Add full serie name as title for legends. [#7493](https://github.com/grafana/grafana/pull/7493), thx [@kolobaev](https://github.com/kolobaev) ## Tech From 3d4c3ff00d2cb1378138996e39cacfefe73b6a6b Mon Sep 17 00:00:00 2001 From: xginn8 Date: Fri, 10 Feb 2017 02:54:00 -0500 Subject: [PATCH 064/301] return an empty message if table contains no data #6109 (#7487) --- public/app/plugins/panel/table/module.html | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/table/module.html b/public/app/plugins/panel/table/module.html index 8d6f604dc56..9140a182541 100644 --- a/public/app/plugins/panel/table/module.html +++ b/public/app/plugins/panel/table/module.html @@ -1,6 +1,7 @@ +
-
-
+
+
@@ -20,5 +21,10 @@
+
+ + No datapoints No datapoints returned from metric query + +
From dcf097a8b91f59d1f74dcad6cccabb0d8fe72277 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 10 Feb 2017 08:57:36 +0100 Subject: [PATCH 065/301] changelog: adds note about closing #6109 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8373d237096..18b84f7c1d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ * **Hipchat**: Adds support for sending alert notifications to hipchat [#6451](https://github.com/grafana/grafana/issues/6451), thx [@jregovic](https://github.com/jregovic) * **Data Sources**: Sorting for lists of data sources in UI is now case insensitive [#7491](https://github.com/grafana/grafana/issues/7491) * **Graph**: Add full serie name as title for legends. [#7493](https://github.com/grafana/grafana/pull/7493), thx [@kolobaev](https://github.com/kolobaev) +* **Table**: Add a message when queries returns no data. [#6109](https://github.com/grafana/grafana/issues/6109), thx [@xginn8](https://github.com/xginn8) ## Tech From 0d8cdf0ebebe48d16f5383b64f54a02e0e308761 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 10 Feb 2017 08:58:45 +0100 Subject: [PATCH 066/301] docs(): rebrand updates --- docs/sources/project/cla.md | 85 ------------------------------------- 1 file changed, 85 deletions(-) delete mode 100644 docs/sources/project/cla.md diff --git a/docs/sources/project/cla.md b/docs/sources/project/cla.md deleted file mode 100644 index 4554300249d..00000000000 --- a/docs/sources/project/cla.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -page_title: Grafana CLA -page_description: Grafana Contributor License Agreement -page_keywords: grafana, cla, contribute, documentation ---- - -# Grafana Contributor License Agreement - -## Why is this agreement necessary? -We very much appreciate your wanting to contribute to Grafana, -but we need to add you to the contributors list first. - - -Note that the following agreement is not a transfer of copyright ownership, -this simply is a license agreement for contributions. You also do not change -your rights to use your own contributions for any other purpose. - - -For some background on why contributor license agreements are necessary, -you can read FAQs from many other open source projects: - -- [Django's excellent CLA FAQ](https://www.djangoproject.com/foundation/cla/faq/) -- [A well-written chapter from Karl Fogel's Producing Open Source Software on CLAs](http://producingoss.com/en/copyright-assignment.html) -- [The Wikipedia article on CLAs](http://en.wikipedia.org/wiki/Contributor_license_agreement) - -This is part of the legal framework of the open-source ecosystem that adds some red tape, -but protects both the contributor and the company / foundation behind the project. -It also gives us the option to relicense the code with a more permissive license in the future. - - -If you have more questions, shoot us an [email](mailto:torkel@grafana.org) or drop by #grafana on IRC (freenode). - -Many thanks to [RethinkDB](http://rethinkdb.com) for permission to re-use their CLA! - -## Terms of the Agreement - -
- -This Contributor License Agreement (“Agreement”) is entered into between Coding Instinct AB, -a Swedish corporation (“Grafana,” “we” or “us” etc.) and you (as defined and further identified below). -Accordingly, you hereby agree to the following terms for your past, present and future contributions -submitted to Grafana: - - -**1. Definitions:** -(a) "You" (or "your") shall mean the contribution copyright owner (whether an individual or organization) or legal entity authorized by the copyright owner that is making this Agreement with Grafana. - - -(b) "Contribution(s)" shall mean the code, documentation or other original works of authorship, including any modifications or additions to an existing work, submitted by you to Grafana for inclusion in, or documentation of, any of the products or projects owned or managed by Grafana (the "work(s)"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to Grafana or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Grafana for the purpose of discussing and/or improving the work, but excluding communication that is conspicuously marked or otherwise designated in writing by you as "Not a Contribution." - -**2. Grant of Copyright License.** -You hereby grant to Grafana and to recipients of software distributed by Grafana a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute your contributions and such derivative works. - - -**3. Grant of Patent License.** -With respect to any patents you own, or that you can license without payment to any third party, you hereby grant Grafana a perpetual, irrevocable, non-exclusive, worldwide, no-charge, royalty-free license to: (i) make, have made, use, sell, offer to sell, import, and otherwise distribute and exploit your contributions in whole or in part, alone or in combination with or included in any product, work or materials arising out of or relating to the Works to which your contributions were submitted; and (ii) sublicense these same rights to third parties through multiple levels of sublicensees or other licensing arrangements. - - -**4. Rights.** -Except as set out above, you keep all right, title, and interest in your contribution. The rights that you grant to us under this agreement are effective on the date you first submitted a contribution to us, even if your submission took place before the date you entered this agreement. - - -**5. You represent and warrant that:** -(i) the contributions are an original work and that you can legally grant the rights set out in this agreement; - - -(ii) the contributions and Grafana’s exercise of any license rights granted hereunder, does not and will not, infringe the rights of any third party; - - -(iii) you are not aware of any pending or threatened claims, suits, actions, or charges pertaining to the contributions, including without limitation any claims or allegations that any or all of the contributions infringes, violates, or misappropriate the intellectual property rights of any third party (you further agree that you will notify Grafana immediately if you become aware of any such actual or potential claims, suits, actions, allegations or charges). - - -**6. Employer Rights.** -If your employer(s) has rights to intellectual property that you create that includes your contributions, you represent and warrant that your employer has waived such rights for your contributions to Grafana, or that you have received permission to make contributions on behalf of that employer and that you are authorized to execute this agreement on behalf of your employer. - - -**7. Support.** -You are not expected to provide support for your contributions, except to the extent you desire to provide support. You may provide support for free, for a fee, or not at all. Except as set forth herein, and unless required by applicable law or agreed to in writing, you provide your contributions on an "as is" basis, without warranties or conditions of any kind. - - -**8. Enforcement.** -The failure of either party to enforce its rights under this agreement for any period shall not be construed as a waiver of such rights. No changes or modifications or waivers to this Agreement will be effective unless in writing and signed by both parties. In the event that any provision of this agreement shall be determined to be illegal or unenforceable, that provision will be limited or eliminated to the minimum extent necessary so that this agreement shall otherwise remain in full force and effect and enforceable. This agreement shall be governed by and construed in accordance with the laws of the State of California in the United States without regard to the conflicts of laws provisions thereof. In any action or proceeding to enforce rights under this agreement, the prevailing party will be entitled to recover costs and attorneys’ fees. - - - \ No newline at end of file From 1e26a8e68acab4afe7fc919d6afb82a32832ab98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 10 Feb 2017 08:58:53 +0100 Subject: [PATCH 067/301] docs(): rebrand updates --- docs/sources/project/index.md | 7 +++++++ docs/sources/reference/index.md | 7 +++++++ 2 files changed, 14 insertions(+) create mode 100644 docs/sources/project/index.md create mode 100644 docs/sources/reference/index.md diff --git a/docs/sources/project/index.md b/docs/sources/project/index.md new file mode 100644 index 00000000000..61c7604dcb5 --- /dev/null +++ b/docs/sources/project/index.md @@ -0,0 +1,7 @@ +--- +title: the grafana project +description: grafana project +type: docs +--- + +# Welcome to the grafana project diff --git a/docs/sources/reference/index.md b/docs/sources/reference/index.md new file mode 100644 index 00000000000..aaaabe89b4b --- /dev/null +++ b/docs/sources/reference/index.md @@ -0,0 +1,7 @@ +--- +title: Reference Index +description: Grafana docs reference +type: docs +--- + +# Documentation From 8083079eeaa0e0cd6faeafeadca077849d095c94 Mon Sep 17 00:00:00 2001 From: Vladimir Kolobaev Date: Fri, 10 Feb 2017 13:11:39 +0400 Subject: [PATCH 068/301] graph: set max width for legend tables. ref #2385 --- public/sass/components/_panel_graph.scss | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index 7b44c29f8fa..179049ea220 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -133,6 +133,9 @@ padding-left: 7px; text-align: left; width: 95%; + max-width: 650px; + text-overflow: ellipsis; + overflow: hidden; } .graph-legend-series:nth-child(odd) { @@ -264,7 +267,10 @@ .graph-tooltip-series-name { display: table-cell; padding: 0.15rem; - } + max-width: 650px; + text-overflow: ellipsis; + overflow: hidden; + } .graph-tooltip-value { display: table-cell; From 900230890f5b2a5fcbe374d29aecb196231a48b3 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 10 Feb 2017 10:15:03 +0100 Subject: [PATCH 069/301] changelog: adds note about closing #2385 --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18b84f7c1d5..0688790df2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,8 +16,9 @@ * **Dataproxy**: Only allow get that begins with api/ to access Prometheus [#7459](https://github.com/grafana/grafana/pull/7459), thx [@mtanda](https://github.com/mtanda) * **Hipchat**: Adds support for sending alert notifications to hipchat [#6451](https://github.com/grafana/grafana/issues/6451), thx [@jregovic](https://github.com/jregovic) * **Data Sources**: Sorting for lists of data sources in UI is now case insensitive [#7491](https://github.com/grafana/grafana/issues/7491) -* **Graph**: Add full serie name as title for legends. [#7493](https://github.com/grafana/grafana/pull/7493), thx [@kolobaev](https://github.com/kolobaev) +* **Graph**: Add full series name as title for legends. [#7493](https://github.com/grafana/grafana/pull/7493), thx [@kolobaev](https://github.com/kolobaev) * **Table**: Add a message when queries returns no data. [#6109](https://github.com/grafana/grafana/issues/6109), thx [@xginn8](https://github.com/xginn8) +* **Graph**: Set max width for series names in legend tables. [#2385](https://github.com/grafana/grafana/issues/2385), thx [@kolobaev](https://github.com/kolobaev) ## Tech From 1294b203e69976978402cccdfb3c5ccd7c6ddd74 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 8 Feb 2017 14:20:07 +0100 Subject: [PATCH 070/301] admin: adds paging to global user list Currently there is a limit of 1000 users in the global user list. This change introduces paging so that an admin can see all users and not just the first 1000. Adds a new route to the api - /api/users/search that returns a list of users and a total count. It takes two parameters perpage and page that enable paging. Fixes #7469 --- pkg/api/api.go | 3 +- pkg/api/datasources.go | 8 +- pkg/api/datasources_test.go | 31 ++++- pkg/api/user.go | 36 +++++- pkg/api/user_test.go | 109 ++++++++++++++++++ pkg/models/user.go | 9 +- pkg/services/sqlstore/org_test.go | 4 +- pkg/services/sqlstore/user.go | 15 ++- pkg/services/sqlstore/user_test.go | 45 ++++++++ public/app/core/routes/routes.ts | 1 + public/app/features/admin/admin.ts | 3 +- .../app/features/admin/adminListUsersCtrl.js | 38 ------ .../features/admin/admin_list_users_ctrl.ts | 49 ++++++++ public/app/features/admin/partials/users.html | 97 +++++++++------- public/sass/pages/_admin.scss | 12 ++ 15 files changed, 360 insertions(+), 100 deletions(-) create mode 100644 pkg/api/user_test.go create mode 100644 pkg/services/sqlstore/user_test.go delete mode 100644 public/app/features/admin/adminListUsersCtrl.js create mode 100644 public/app/features/admin/admin_list_users_ctrl.ts diff --git a/pkg/api/api.go b/pkg/api/api.go index 7d3a5563892..2698a2fc001 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -123,6 +123,7 @@ func (hs *HttpServer) registerRoutes() { // users (admin permission required) r.Group("/users", func() { r.Get("/", wrap(SearchUsers)) + r.Get("/search", wrap(SearchUsersWithPaging)) r.Get("/:id", wrap(GetUserById)) r.Get("/:id/orgs", wrap(GetUserOrgList)) // query parameters /users/lookup?loginOrEmail=admin@example.com @@ -195,7 +196,7 @@ func (hs *HttpServer) registerRoutes() { // Data sources r.Group("/datasources", func() { - r.Get("/", GetDataSources) + r.Get("/", wrap(GetDataSources)) r.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), AddDataSource) r.Put("/:id", bind(m.UpdateDataSourceCommand{}), wrap(UpdateDataSource)) r.Delete("/:id", DeleteDataSource) diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index 43f4d308ed6..54656d70e5d 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -11,12 +11,11 @@ import ( "github.com/grafana/grafana/pkg/util" ) -func GetDataSources(c *middleware.Context) { +func GetDataSources(c *middleware.Context) Response { query := m.GetDataSourcesQuery{OrgId: c.OrgId} if err := bus.Dispatch(&query); err != nil { - c.JsonApiErr(500, "Failed to query datasources", err) - return + return ApiError(500, "Failed to query datasources", err) } result := make(dtos.DataSourceList, 0) @@ -46,7 +45,8 @@ func GetDataSources(c *middleware.Context) { } sort.Sort(result) - c.JSON(200, result) + + return Json(200, &result) } func GetDataSourceById(c *middleware.Context) Response { diff --git a/pkg/api/datasources_test.go b/pkg/api/datasources_test.go index aa3f6106afd..5ae752bea91 100644 --- a/pkg/api/datasources_test.go +++ b/pkg/api/datasources_test.go @@ -59,7 +59,9 @@ func loggedInUserScenario(desc string, url string, fn scenarioFunc) { Convey(desc+" "+url, func() { defer bus.ClearBusHandlers() - sc := &scenarioContext{} + sc := &scenarioContext{ + url: url, + } viewsPath, _ := filepath.Abs("../../public/views") sc.m = macaron.New() @@ -71,16 +73,18 @@ func loggedInUserScenario(desc string, url string, fn scenarioFunc) { sc.m.Use(middleware.GetContextHandler()) sc.m.Use(middleware.Sessioner(&session.Options{})) - sc.defaultHandler = func(c *middleware.Context) { + sc.defaultHandler = wrap(func(c *middleware.Context) Response { sc.context = c sc.context.UserId = TestUserID sc.context.OrgId = TestOrgID sc.context.OrgRole = models.ROLE_EDITOR if sc.handlerFunc != nil { - sc.handlerFunc(sc.context) + return sc.handlerFunc(sc.context) } - } - sc.m.SetAutoHead(true) + + return nil + }) + sc.m.Get(url, sc.defaultHandler) fn(sc) @@ -96,6 +100,20 @@ func (sc *scenarioContext) fakeReq(method, url string) *scenarioContext { return sc } +func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map[string]string) *scenarioContext { + sc.resp = httptest.NewRecorder() + req, err := http.NewRequest(method, url, nil) + q := req.URL.Query() + for k, v := range queryParams { + q.Add(k, v) + } + req.URL.RawQuery = q.Encode() + So(err, ShouldBeNil) + sc.req = req + + return sc +} + type scenarioContext struct { m *macaron.Macaron context *middleware.Context @@ -103,6 +121,7 @@ type scenarioContext struct { handlerFunc handlerFunc defaultHandler macaron.Handler req *http.Request + url string } func (sc *scenarioContext) exec() { @@ -110,4 +129,4 @@ func (sc *scenarioContext) exec() { } type scenarioFunc func(c *scenarioContext) -type handlerFunc func(c *middleware.Context) +type handlerFunc func(c *middleware.Context) Response diff --git a/pkg/api/user.go b/pkg/api/user.go index 7bce599d692..9a978503761 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -210,14 +210,46 @@ func ChangeUserPassword(c *middleware.Context, cmd m.ChangeUserPasswordCommand) // GET /api/users func SearchUsers(c *middleware.Context) Response { - query := m.SearchUsersQuery{Query: "", Page: 0, Limit: 1000} - if err := bus.Dispatch(&query); err != nil { + query, err := searchUser(c) + if err != nil { + return ApiError(500, "Failed to fetch users", err) + } + + return Json(200, query.Result.Users) +} + +// GET /api/paged-users +func SearchUsersWithPaging(c *middleware.Context) Response { + query, err := searchUser(c) + if err != nil { return ApiError(500, "Failed to fetch users", err) } return Json(200, query.Result) } +func searchUser(c *middleware.Context) (*m.SearchUsersQuery, error) { + perPage := c.QueryInt("perpage") + if perPage <= 0 { + perPage = 1000 + } + page := c.QueryInt("page") + + if page < 1 { + page = 1 + } + + query := &m.SearchUsersQuery{Query: "", Page: page, Limit: perPage} + if err := bus.Dispatch(query); err != nil { + return nil, err + } + + query.Result.Page = page + query.Result.PerPage = perPage + + return query, nil +} + func SetHelpFlag(c *middleware.Context) Response { flag := c.ParamsInt64(":id") diff --git a/pkg/api/user_test.go b/pkg/api/user_test.go new file mode 100644 index 00000000000..6aa9dd9adbf --- /dev/null +++ b/pkg/api/user_test.go @@ -0,0 +1,109 @@ +package api + +import ( + "testing" + + "github.com/grafana/grafana/pkg/models" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" + . "github.com/smartystreets/goconvey/convey" +) + +func TestUserApiEndpoint(t *testing.T) { + Convey("Given a user is logged in", t, func() { + mockResult := models.SearchUserQueryResult{ + Users: []*models.UserSearchHitDTO{ + {Name: "user1"}, + {Name: "user2"}, + }, + TotalCount: 2, + } + + loggedInUserScenario("When calling GET on", "/api/users", func(sc *scenarioContext) { + var sentLimit int + var sendPage int + bus.AddHandler("test", func(query *models.SearchUsersQuery) error { + query.Result = mockResult + + sentLimit = query.Limit + sendPage = query.Page + + return nil + }) + + sc.handlerFunc = SearchUsers + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + + So(sentLimit, ShouldEqual, 1000) + So(sendPage, ShouldEqual, 1) + + respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) + So(err, ShouldBeNil) + So(len(respJSON.MustArray()), ShouldEqual, 2) + }) + + loggedInUserScenario("When calling GET with page and limit querystring parameters on", "/api/users", func(sc *scenarioContext) { + var sentLimit int + var sendPage int + bus.AddHandler("test", func(query *models.SearchUsersQuery) error { + query.Result = mockResult + + sentLimit = query.Limit + sendPage = query.Page + + return nil + }) + + sc.handlerFunc = SearchUsers + sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec() + + So(sentLimit, ShouldEqual, 10) + So(sendPage, ShouldEqual, 2) + }) + + loggedInUserScenario("When calling GET on", "/api/users/search", func(sc *scenarioContext) { + var sentLimit int + var sendPage int + bus.AddHandler("test", func(query *models.SearchUsersQuery) error { + query.Result = mockResult + + sentLimit = query.Limit + sendPage = query.Page + + return nil + }) + + sc.handlerFunc = SearchUsersWithPaging + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + + So(sentLimit, ShouldEqual, 1000) + So(sendPage, ShouldEqual, 1) + + respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) + So(err, ShouldBeNil) + + So(respJSON.Get("totalCount").MustInt(), ShouldEqual, 2) + So(len(respJSON.Get("users").MustArray()), ShouldEqual, 2) + }) + + loggedInUserScenario("When calling GET with page and perpage querystring parameters on", "/api/users/search", func(sc *scenarioContext) { + var sentLimit int + var sendPage int + bus.AddHandler("test", func(query *models.SearchUsersQuery) error { + query.Result = mockResult + + sentLimit = query.Limit + sendPage = query.Page + + return nil + }) + + sc.handlerFunc = SearchUsersWithPaging + sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec() + + So(sentLimit, ShouldEqual, 10) + So(sendPage, ShouldEqual, 2) + }) + }) +} diff --git a/pkg/models/user.go b/pkg/models/user.go index e14f4486ba3..e0a36be8c0a 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -130,7 +130,14 @@ type SearchUsersQuery struct { Page int Limit int - Result []*UserSearchHitDTO + Result SearchUserQueryResult +} + +type SearchUserQueryResult struct { + TotalCount int64 `json:"totalCount"` + Users []*UserSearchHitDTO `json:"users"` + Page int `json:"page"` + PerPage int `json:"perPage"` } type GetUserOrgListQuery struct { diff --git a/pkg/services/sqlstore/org_test.go b/pkg/services/sqlstore/org_test.go index f52175c2e5c..e7c718fc9a8 100644 --- a/pkg/services/sqlstore/org_test.go +++ b/pkg/services/sqlstore/org_test.go @@ -63,8 +63,8 @@ func TestAccountDataAccess(t *testing.T) { err := SearchUsers(&query) So(err, ShouldBeNil) - So(query.Result[0].Email, ShouldEqual, "ac1@test.com") - So(query.Result[1].Email, ShouldEqual, "ac2@test.com") + So(query.Result.Users[0].Email, ShouldEqual, "ac1@test.com") + So(query.Result.Users[1].Email, ShouldEqual, "ac2@test.com") }) Convey("Given an added org user", func() { diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index b26a9153f55..791a38f2d01 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -344,12 +344,21 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error { } func SearchUsers(query *m.SearchUsersQuery) error { - query.Result = make([]*m.UserSearchHitDTO, 0) + query.Result = m.SearchUserQueryResult{ + Users: make([]*m.UserSearchHitDTO, 0), + } sess := x.Table("user") sess.Where("email LIKE ?", query.Query+"%") - sess.Limit(query.Limit, query.Limit*query.Page) + offset := query.Limit * (query.Page - 1) + sess.Limit(query.Limit, offset) sess.Cols("id", "email", "name", "login", "is_admin") - err := sess.Find(&query.Result) + if err := sess.Find(&query.Result.Users); err != nil { + return err + } + + user := m.User{} + count, err := x.Count(&user) + query.Result.TotalCount = count return err } diff --git a/pkg/services/sqlstore/user_test.go b/pkg/services/sqlstore/user_test.go new file mode 100644 index 00000000000..53a50f9631c --- /dev/null +++ b/pkg/services/sqlstore/user_test.go @@ -0,0 +1,45 @@ +package sqlstore + +import ( + "fmt" + "testing" + + . "github.com/smartystreets/goconvey/convey" + + "github.com/grafana/grafana/pkg/models" +) + +func TestUserDataAccess(t *testing.T) { + + Convey("Testing DB", t, func() { + InitTestDB(t) + + var err error + for i := 0; i < 5; i++ { + err = CreateUser(&models.CreateUserCommand{ + Email: fmt.Sprint("user", i, "@test.com"), + Name: fmt.Sprint("user", i), + Login: fmt.Sprint("user", i), + }) + So(err, ShouldBeNil) + } + + Convey("Can return the first page of users and a total count", func() { + query := models.SearchUsersQuery{Query: "", Page: 1, Limit: 3} + err = SearchUsers(&query) + + So(err, ShouldBeNil) + So(len(query.Result.Users), ShouldEqual, 3) + So(query.Result.TotalCount, ShouldEqual, 5) + }) + + Convey("Can return the second page of users and a total count", func() { + query := models.SearchUsersQuery{Query: "", Page: 2, Limit: 3} + err = SearchUsers(&query) + + So(err, ShouldBeNil) + So(len(query.Result.Users), ShouldEqual, 2) + So(query.Result.TotalCount, ShouldEqual, 5) + }) + }) +} diff --git a/public/app/core/routes/routes.ts b/public/app/core/routes/routes.ts index 28e1dcf1cd1..3ccf89bae01 100644 --- a/public/app/core/routes/routes.ts +++ b/public/app/core/routes/routes.ts @@ -113,6 +113,7 @@ function setupAngularRoutes($routeProvider, $locationProvider) { .when('/admin/users', { templateUrl: 'public/app/features/admin/partials/users.html', controller : 'AdminListUsersCtrl', + controllerAs: 'ctrl', resolve: loadAdminBundle, }) .when('/admin/users/create', { diff --git a/public/app/features/admin/admin.ts b/public/app/features/admin/admin.ts index b93fd07a059..6809e4c54ab 100644 --- a/public/app/features/admin/admin.ts +++ b/public/app/features/admin/admin.ts @@ -1,4 +1,4 @@ -import './adminListUsersCtrl'; +import AdminListUsersCtrl from './admin_list_users_ctrl'; import './adminListOrgsCtrl'; import './adminEditOrgCtrl'; import './adminEditUserCtrl'; @@ -37,3 +37,4 @@ export class AdminStatsCtrl { coreModule.controller('AdminSettingsCtrl', AdminSettingsCtrl); coreModule.controller('AdminHomeCtrl', AdminHomeCtrl); coreModule.controller('AdminStatsCtrl', AdminStatsCtrl); +coreModule.controller('AdminListUsersCtrl', AdminListUsersCtrl); diff --git a/public/app/features/admin/adminListUsersCtrl.js b/public/app/features/admin/adminListUsersCtrl.js deleted file mode 100644 index 721adcfb98c..00000000000 --- a/public/app/features/admin/adminListUsersCtrl.js +++ /dev/null @@ -1,38 +0,0 @@ -define([ - 'angular', -], -function (angular) { - 'use strict'; - - var module = angular.module('grafana.controllers'); - - module.controller('AdminListUsersCtrl', function($scope, backendSrv) { - - $scope.init = function() { - $scope.getUsers(); - }; - - $scope.getUsers = function() { - backendSrv.get('/api/users').then(function(users) { - $scope.users = users; - }); - }; - - $scope.deleteUser = function(user) { - $scope.appEvent('confirm-modal', { - title: 'Delete', - text: 'Do you want to delete ' + user.login + '?', - icon: 'fa-trash', - yesText: 'Delete', - onConfirm: function() { - backendSrv.delete('/api/admin/users/' + user.id).then(function() { - $scope.getUsers(); - }); - } - }); - }; - - $scope.init(); - - }); -}); diff --git a/public/app/features/admin/admin_list_users_ctrl.ts b/public/app/features/admin/admin_list_users_ctrl.ts new file mode 100644 index 00000000000..1347457bc06 --- /dev/null +++ b/public/app/features/admin/admin_list_users_ctrl.ts @@ -0,0 +1,49 @@ +/// + +export default class AdminListUsersCtrl { + users: any; + pages = []; + perPage = 1000; + page = 1; + totalPages: number; + showPaging = false; + + /** @ngInject */ + constructor(private $scope, private backendSrv) { + this.getUsers(); + } + + getUsers() { + this.backendSrv.get(`/api/users/search?perpage=${this.perPage}&page=${this.page}`).then((result) => { + this.users = result.users; + this.page = result.page; + this.perPage = result.perPage; + this.totalPages = Math.ceil(result.totalCount / result.perPage); + this.showPaging = this.totalPages > 1; + this.pages = []; + + for (var i = 1; i < this.totalPages+1; i++) { + this.pages.push({ page: i, current: i === this.page}); + } + }); + } + + navigateToPage(page) { + this.page = page.page; + this.getUsers(); + } + + deleteUser(user) { + this.$scope.appEvent('confirm-modal', { + title: 'Delete', + text: 'Do you want to delete ' + user.login + '?', + icon: 'fa-trash', + yesText: 'Delete', + onConfirm: () => { + this.backendSrv.delete('/api/admin/users/' + user.id).then(() => { + this.getUsers(); + }); + } + }); + } +} diff --git a/public/app/features/admin/partials/users.html b/public/app/features/admin/partials/users.html index a1c86391088..57714380c67 100644 --- a/public/app/features/admin/partials/users.html +++ b/public/app/features/admin/partials/users.html @@ -1,49 +1,62 @@ - - - Users - + + + Users +
- +
+ + + + + + + + + + + + + + + + + + + + + -
IdNameLoginEmailGrafana Admin
{{user.id}}{{user.name}}{{user.login}}{{user.email}}{{user.isAdmin}} + + + Edit + +    + + + +
- - - - - - - - - - - - - - - - - - - - -
IdNameLoginEmailGrafana Admin
{{user.id}}{{user.name}}{{user.login}}{{user.email}}{{user.isAdmin}} - - - Edit - -    - - - -
+ +
+ +
+
    +
  1. + +
  2. +
+
diff --git a/public/sass/pages/_admin.scss b/public/sass/pages/_admin.scss index 30bc3ca8b34..b2be062849d 100644 --- a/public/sass/pages/_admin.scss +++ b/public/sass/pages/_admin.scss @@ -8,3 +8,15 @@ td.admin-settings-key { padding-left: 20px; } +.admin-list-table { + margin-bottom: 20px; +} + +.admin-list-paging { + float: right; + li { + display: inline-block; + padding-left: 10px; + margin-bottom: 5px; + } +} From 3c7cf3f7282baceb061744846d8aa584a0eea079 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 10 Feb 2017 14:42:19 +0100 Subject: [PATCH 071/301] docs: kairosdb is no longer supported by default --- docs/sources/guides/getting_started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/guides/getting_started.md b/docs/sources/guides/getting_started.md index dde8283e79d..d4ead7d8577 100644 --- a/docs/sources/guides/getting_started.md +++ b/docs/sources/guides/getting_started.md @@ -38,7 +38,7 @@ The image above shows you the top header for a Dashboard. ## Dashboards, Panels, Rows, the building blocks of Grafana... -Dashboards are at the core of what Grafana is all about. Dashboards are composed of individual Panels arranged on a number of Rows. Grafana ships with a variety of Panels. Grafana makes it easy to construct the right queries, and customize the display properties so that you can create the perfect Dashboard for your need. Each Panel can interact with data from any configured Grafana Data Source (currently InfluxDB, Graphite, OpenTSDB, and KairosDB). The [Basic Concepts](/guides/basic_concepts) guide explores these key ideas in detail. +Dashboards are at the core of what Grafana is all about. Dashboards are composed of individual Panels arranged on a number of Rows. Grafana ships with a variety of Panels. Grafana makes it easy to construct the right queries, and customize the display properties so that you can create the perfect Dashboard for your need. Each Panel can interact with data from any configured Grafana Data Source (currently InfluxDB, Graphite, OpenTSDB, Prometheus and Cloudwatch). The [Basic Concepts](/guides/basic_concepts) guide explores these key ideas in detail. ## Adding & Editing Graphs and Panels From 28d93b574d7018f1099ffc3b8a7ac137d532c4ca Mon Sep 17 00:00:00 2001 From: huydx Date: Fri, 10 Feb 2017 12:31:55 +0900 Subject: [PATCH 072/301] (feat) support max connection setting for database configuration --- conf/defaults.ini | 5 +++++ pkg/services/sqlstore/sqlstore.go | 16 +++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 358847724ab..4606de9212e 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -73,6 +73,11 @@ password = # Example: mysql://user:secret@host:port/database url = +# Max conn setting default is 0 (mean not set) +max_conn = +max_idle_conn = +max_open_conn = + # For "postgres", use either "disable", "require" or "verify-full" # For "mysql", use either "true", "false", or "skip-verify". ssl_mode = disable diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index 5c44fe85f50..0d16d57bcde 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -29,6 +29,9 @@ type DatabaseConfig struct { ClientKeyPath string ClientCertPath string ServerCertName string + MaxConn int + MaxOpenConn int + MaxIdleConn int } var ( @@ -150,7 +153,15 @@ func getEngine() (*xorm.Engine, error) { } sqlog.Info("Initializing DB", "dbtype", DbCfg.Type) - return xorm.NewEngine(DbCfg.Type, cnnstr) + engine, err := xorm.NewEngine(DbCfg.Type, cnnstr) + if err != nil { + return nil, err + } else { + engine.SetMaxConns(DbCfg.MaxConn) + engine.SetMaxOpenConns(DbCfg.MaxOpenConn) + engine.SetMaxIdleConns(DbCfg.MaxIdleConn) + } + return engine, nil } func LoadConfig() { @@ -177,6 +188,9 @@ func LoadConfig() { DbCfg.Host = sec.Key("host").String() DbCfg.Name = sec.Key("name").String() DbCfg.User = sec.Key("user").String() + DbCfg.MaxConn = sec.Key("max_conn").MustInt(0) + DbCfg.MaxOpenConn = sec.Key("max_open_conn").MustInt(0) + DbCfg.MaxIdleConn = sec.Key("max_idle_conn").MustInt(0) if len(DbCfg.Pwd) == 0 { DbCfg.Pwd = sec.Key("password").String() } From 5b7b3fef647379e25de3fc10056082dbb0391886 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 10 Feb 2017 15:29:43 +0100 Subject: [PATCH 073/301] conf: adds sample values for db conn settings --- conf/sample.ini | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/conf/sample.ini b/conf/sample.ini index ce9344e1d4f..eaedfcc6f5e 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -82,6 +82,12 @@ # For "sqlite3" only, path relative to data_path setting ;path = grafana.db +# Max conn setting default is 0 (mean not set) +;max_conn = +;max_idle_conn = +;max_open_conn = + + #################################### Session #################################### [session] # Either "memory", "file", "redis", "mysql", "postgres", default is "file" From 143cbe921fb072785165872ed81fed381bb4347a Mon Sep 17 00:00:00 2001 From: huydx Date: Fri, 10 Feb 2017 11:11:36 +0900 Subject: [PATCH 074/301] (feat) support datasource delete by name api --- pkg/api/api.go | 3 ++- pkg/api/datasources.go | 23 +++++++++++++++++++++-- pkg/models/datasource.go | 7 ++++++- pkg/services/sqlstore/datasource.go | 13 +++++++++++-- pkg/services/sqlstore/datasource_test.go | 14 +++++++++++--- 5 files changed, 51 insertions(+), 9 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 7d3a5563892..d9fee8a0187 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -198,7 +198,8 @@ func (hs *HttpServer) registerRoutes() { r.Get("/", GetDataSources) r.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), AddDataSource) r.Put("/:id", bind(m.UpdateDataSourceCommand{}), wrap(UpdateDataSource)) - r.Delete("/:id", DeleteDataSource) + r.Delete("/:id", DeleteDataSourceById) + r.Delete("/name/:name", DeleteDataSourceByName) r.Get("/:id", wrap(GetDataSourceById)) r.Get("/name/:name", wrap(GetDataSourceByName)) }, reqOrgAdmin) diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index 43f4d308ed6..455195aacb0 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -68,7 +68,7 @@ func GetDataSourceById(c *middleware.Context) Response { return Json(200, &dtos) } -func DeleteDataSource(c *middleware.Context) { +func DeleteDataSourceById(c *middleware.Context) { id := c.ParamsInt64(":id") if id <= 0 { @@ -76,7 +76,26 @@ func DeleteDataSource(c *middleware.Context) { return } - cmd := &m.DeleteDataSourceCommand{Id: id, OrgId: c.OrgId} + cmd := &m.DeleteDataSourceByIdCommand{Id: id, OrgId: c.OrgId} + + err := bus.Dispatch(cmd) + if err != nil { + c.JsonApiErr(500, "Failed to delete datasource", err) + return + } + + c.JsonOK("Data source deleted") +} + +func DeleteDataSourceByName(c *middleware.Context) { + name := c.Params(":name") + + if name == "" { + c.JsonApiErr(400, "Missing valid datasource name", nil) + return + } + + cmd := &m.DeleteDataSourceByNameCommand{Name: name, OrgId: c.OrgId} err := bus.Dispatch(cmd) if err != nil { diff --git a/pkg/models/datasource.go b/pkg/models/datasource.go index 4a90edd9bfd..804880a5d10 100644 --- a/pkg/models/datasource.go +++ b/pkg/models/datasource.go @@ -120,11 +120,16 @@ type UpdateDataSourceCommand struct { Id int64 `json:"-"` } -type DeleteDataSourceCommand struct { +type DeleteDataSourceByIdCommand struct { Id int64 OrgId int64 } +type DeleteDataSourceByNameCommand struct { + Name string + OrgId int64 +} + // --------------------- // QUERIES diff --git a/pkg/services/sqlstore/datasource.go b/pkg/services/sqlstore/datasource.go index 57abaf35083..de838163681 100644 --- a/pkg/services/sqlstore/datasource.go +++ b/pkg/services/sqlstore/datasource.go @@ -13,7 +13,8 @@ import ( func init() { bus.AddHandler("sql", GetDataSources) bus.AddHandler("sql", AddDataSource) - bus.AddHandler("sql", DeleteDataSource) + bus.AddHandler("sql", DeleteDataSourceById) + bus.AddHandler("sql", DeleteDataSourceByName) bus.AddHandler("sql", UpdateDataSource) bus.AddHandler("sql", GetDataSourceById) bus.AddHandler("sql", GetDataSourceByName) @@ -50,7 +51,7 @@ func GetDataSources(query *m.GetDataSourcesQuery) error { return sess.Find(&query.Result) } -func DeleteDataSource(cmd *m.DeleteDataSourceCommand) error { +func DeleteDataSourceById(cmd *m.DeleteDataSourceByIdCommand) error { return inTransaction(func(sess *xorm.Session) error { var rawSql = "DELETE FROM data_source WHERE id=? and org_id=?" _, err := sess.Exec(rawSql, cmd.Id, cmd.OrgId) @@ -58,6 +59,14 @@ func DeleteDataSource(cmd *m.DeleteDataSourceCommand) error { }) } +func DeleteDataSourceByName(cmd *m.DeleteDataSourceByNameCommand) error { + return inTransaction(func(sess *xorm.Session) error { + var rawSql = "DELETE FROM data_source WHERE name=? and org_id=?" + _, err := sess.Exec(rawSql, cmd.Name, cmd.OrgId) + return err + }) +} + func AddDataSource(cmd *m.AddDataSourceCommand) error { return inTransaction(func(sess *xorm.Session) error { diff --git a/pkg/services/sqlstore/datasource_test.go b/pkg/services/sqlstore/datasource_test.go index 35752eeaafc..2749a3cc426 100644 --- a/pkg/services/sqlstore/datasource_test.go +++ b/pkg/services/sqlstore/datasource_test.go @@ -79,8 +79,16 @@ func TestDataAccess(t *testing.T) { ds := query.Result[0] - Convey("Can delete datasource", func() { - err := DeleteDataSource(&m.DeleteDataSourceCommand{Id: ds.Id, OrgId: ds.OrgId}) + Convey("Can delete datasource by id", func() { + err := DeleteDataSourceById(&m.DeleteDataSourceByIdCommand{Id: ds.Id, OrgId: ds.OrgId}) + So(err, ShouldBeNil) + + GetDataSources(&query) + So(len(query.Result), ShouldEqual, 0) + }) + + Convey("Can delete datasource by name", func() { + err := DeleteDataSourceByName(&m.DeleteDataSourceByNameCommand{Name: ds.Name, OrgId: ds.OrgId}) So(err, ShouldBeNil) GetDataSources(&query) @@ -88,7 +96,7 @@ func TestDataAccess(t *testing.T) { }) Convey("Can not delete datasource with wrong orgId", func() { - err := DeleteDataSource(&m.DeleteDataSourceCommand{Id: ds.Id, OrgId: 123123}) + err := DeleteDataSourceById(&m.DeleteDataSourceByIdCommand{Id: ds.Id, OrgId: 123123}) So(err, ShouldBeNil) GetDataSources(&query) From e80f67326424a085f21b029a42eaa82de6b95c0f Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 10 Feb 2017 15:56:28 +0100 Subject: [PATCH 075/301] changelog: adds note about closing issues --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0688790df2e..7b515930a4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ * **Graph**: Add full series name as title for legends. [#7493](https://github.com/grafana/grafana/pull/7493), thx [@kolobaev](https://github.com/kolobaev) * **Table**: Add a message when queries returns no data. [#6109](https://github.com/grafana/grafana/issues/6109), thx [@xginn8](https://github.com/xginn8) * **Graph**: Set max width for series names in legend tables. [#2385](https://github.com/grafana/grafana/issues/2385), thx [@kolobaev](https://github.com/kolobaev) +* **Database**: Allow max db connection pool configuration [#7427](https://github.com/grafana/grafana/issues/7427), thx [@huydx](https://github.com/huydx) +* **Datasources** Delete datsource by name [#7476](https://github.com/grafana/grafana/issues/7476), thx [@huydx](https://github.com/huydx) ## Tech From f069aae5763e3f0779c3af9148c78f8f79174d6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 25 Dec 2016 20:57:08 +0000 Subject: [PATCH 076/301] Add Pushover alert notifications Pushover is a service for getting real-time notifications on your mobile devices and desktop computers: https://pushover.net --- pkg/metrics/metrics.go | 2 + pkg/services/alerting/notifiers/pushover.go | 177 ++++++++++++++++++ .../alerting/notifiers/pushover_test.go | 58 ++++++ .../app/features/alerting/alert_tab_ctrl.ts | 1 + 4 files changed, 238 insertions(+) create mode 100644 pkg/services/alerting/notifiers/pushover.go create mode 100644 pkg/services/alerting/notifiers/pushover_test.go diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 1020f28f874..e60a967f3fa 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -50,6 +50,7 @@ var ( M_Alerting_Notification_Sent_OpsGenie Counter M_Alerting_Notification_Sent_Telegram Counter M_Alerting_Notification_Sent_Sensu Counter + M_Alerting_Notification_Sent_Pushover Counter M_Aws_CloudWatch_GetMetricStatistics Counter M_Aws_CloudWatch_ListMetrics Counter @@ -120,6 +121,7 @@ func initMetricVars(settings *MetricSettings) { M_Alerting_Notification_Sent_Telegram = RegCounter("alerting.notifications_sent", "type", "telegram") M_Alerting_Notification_Sent_Sensu = RegCounter("alerting.notifications_sent", "type", "sensu") M_Alerting_Notification_Sent_LINE = RegCounter("alerting.notifications_sent", "type", "LINE") + M_Alerting_Notification_Sent_Pushover = RegCounter("alerting.notifications_sent", "type", "pushover") M_Aws_CloudWatch_GetMetricStatistics = RegCounter("aws.cloudwatch.get_metric_statistics") M_Aws_CloudWatch_ListMetrics = RegCounter("aws.cloudwatch.list_metrics") diff --git a/pkg/services/alerting/notifiers/pushover.go b/pkg/services/alerting/notifiers/pushover.go new file mode 100644 index 00000000000..c68ad421c62 --- /dev/null +++ b/pkg/services/alerting/notifiers/pushover.go @@ -0,0 +1,177 @@ +package notifiers + +import ( + "fmt" + "net/url" + "strconv" + + "github.com/grafana/grafana/pkg/bus" + "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" +) + +const PUSHOVER_ENDPOINT = "https://api.pushover.net/1/messages.json" + +func init() { + alerting.RegisterNotifier(&alerting.NotifierPlugin{ + Type: "pushover", + Name: "Pushover", + Description: "Sends HTTP POST request to the Pushover API", + Factory: NewPushoverNotifier, + OptionsTemplate: ` +

Pushover settings

+
+ API Token + +
+
+ User key(s) + +
+
+ Device(s) (optional) + +
+
+ Priority + +
+
+ Retry + + Expire + +
+
+ Sound + +
+ `, + }) +} + +func NewPushoverNotifier(model *m.AlertNotification) (alerting.Notifier, error) { + userKey := model.Settings.Get("userKey").MustString() + apiToken := model.Settings.Get("apiToken").MustString() + device := model.Settings.Get("device").MustString() + priority, _ := strconv.Atoi(model.Settings.Get("priority").MustString()) + retry, _ := strconv.Atoi(model.Settings.Get("retry").MustString()) + expire, _ := strconv.Atoi(model.Settings.Get("expire").MustString()) + sound := model.Settings.Get("sound").MustString() + + if userKey == "" { + return nil, alerting.ValidationError{Reason: "User key not given"} + } + if apiToken == "" { + return nil, alerting.ValidationError{Reason: "API token not given"} + } + return &PushoverNotifier{ + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + UserKey: userKey, + ApiToken: apiToken, + Priority: priority, + Retry: retry, + Expire: expire, + Device: device, + Sound: sound, + log: log.New("alerting.notifier.pushover"), + }, nil +} + +type PushoverNotifier struct { + NotifierBase + UserKey string + ApiToken string + Priority int + Retry int + Expire int + Device string + Sound string + log log.Logger +} + +func (this *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error { + metrics.M_Alerting_Notification_Sent_Pushover.Inc(1) + ruleUrl, err := evalContext.GetRuleUrl() + if err != nil { + this.log.Error("Failed get rule link", "error", err) + return err + } + message := evalContext.Rule.Message + for idx, evt := range evalContext.EvalMatches { + message += fmt.Sprintf("\n%s: %v", evt.Metric, evt.Value) + if idx > 4 { + break + } + } + if evalContext.Error != nil { + message += fmt.Sprintf("\nError message %s", evalContext.Error.Error()) + } + q := url.Values{} + q.Add("user", this.UserKey) + q.Add("token", this.ApiToken) + q.Add("priority", strconv.Itoa(this.Priority)) + if this.Priority == 2 { + q.Add("retry", strconv.Itoa(this.Retry)) + q.Add("expire", strconv.Itoa(this.Expire)) + } + if this.Device != "" { + q.Add("device", this.Device) + } + if this.Sound != "default" { + q.Add("sound", this.Sound) + } + q.Add("title", evalContext.GetNotificationTitle()) + q.Add("url", ruleUrl) + q.Add("url_title", "Show dashboard with alert") + q.Add("message", message) + q.Add("html", "1") + + cmd := &m.SendWebhookSync{ + Url: PUSHOVER_ENDPOINT, + HttpMethod: "POST", + HttpHeader: map[string]string{"Content-Type": "application/x-www-form-urlencoded"}, + Body: q.Encode(), + } + + if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { + this.log.Error("Failed to send pushover notification", "error", err, "webhook", this.Name) + return err + } + + return nil +} diff --git a/pkg/services/alerting/notifiers/pushover_test.go b/pkg/services/alerting/notifiers/pushover_test.go new file mode 100644 index 00000000000..a3bac73e065 --- /dev/null +++ b/pkg/services/alerting/notifiers/pushover_test.go @@ -0,0 +1,58 @@ +package notifiers + +import ( + "testing" + + "github.com/grafana/grafana/pkg/components/simplejson" + m "github.com/grafana/grafana/pkg/models" + . "github.com/smartystreets/goconvey/convey" +) + +func TestPushoverNotifier(t *testing.T) { + Convey("Pushover notifier tests", t, func() { + + Convey("Parsing alert notification from settings", func() { + Convey("empty settings should return error", func() { + json := `{ }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "Pushover", + Type: "pushover", + Settings: settingsJSON, + } + + _, err := NewPushoverNotifier(model) + So(err, ShouldNotBeNil) + }) + + Convey("from settings", func() { + json := ` + { + "apiToken": "4SrUFQL4A5V5TQ1z5Pg9nxHXPXSTve", + "userKey": "tzNZYf36y0ohWwXo4XoUrB61rz1A4o", + "priority": "1", + "sound": "pushover" + }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "Pushover", + Type: "pushover", + Settings: settingsJSON, + } + + not, err := NewPushoverNotifier(model) + pushoverNotifier := not.(*PushoverNotifier) + + So(err, ShouldBeNil) + So(pushoverNotifier.Name, ShouldEqual, "Pushover") + So(pushoverNotifier.Type, ShouldEqual, "pushover") + So(pushoverNotifier.ApiToken, ShouldEqual, "4SrUFQL4A5V5TQ1z5Pg9nxHXPXSTve") + So(pushoverNotifier.UserKey, ShouldEqual, "tzNZYf36y0ohWwXo4XoUrB61rz1A4o") + So(pushoverNotifier.Priority, ShouldEqual, 1) + So(pushoverNotifier.Sound, ShouldEqual, "pushover") + }) + }) + }) +} diff --git a/public/app/features/alerting/alert_tab_ctrl.ts b/public/app/features/alerting/alert_tab_ctrl.ts index 6be36380144..b21e5363ad8 100644 --- a/public/app/features/alerting/alert_tab_ctrl.ts +++ b/public/app/features/alerting/alert_tab_ctrl.ts @@ -105,6 +105,7 @@ export class AlertTabCtrl { case "pagerduty": return "fa fa-bullhorn"; case "opsgenie": return "fa fa-bell"; case "hipchat": return "fa fa-mail-forward"; + case "pushover": return "fa fa-mobile"; } } From 193d468ed3ed2ceff274d17c36c82a817f38a4eb Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 8 Feb 2017 14:20:07 +0100 Subject: [PATCH 077/301] admin: adds paging to global user list Currently there is a limit of 1000 users in the global user list. This change introduces paging so that an admin can see all users and not just the first 1000. Adds a new route to the api - /api/users/search that returns a list of users and a total count. It takes two parameters perpage and page that enable paging. Fixes #7469 --- pkg/api/api.go | 3 +- pkg/api/datasources.go | 8 +- pkg/api/datasources_test.go | 31 ++++- pkg/api/user.go | 36 +++++- pkg/api/user_test.go | 109 ++++++++++++++++++ pkg/models/user.go | 9 +- pkg/services/sqlstore/org_test.go | 4 +- pkg/services/sqlstore/user.go | 15 ++- pkg/services/sqlstore/user_test.go | 45 ++++++++ public/app/core/routes/routes.ts | 1 + public/app/features/admin/admin.ts | 3 +- .../app/features/admin/adminListUsersCtrl.js | 38 ------ .../features/admin/admin_list_users_ctrl.ts | 49 ++++++++ public/app/features/admin/partials/users.html | 97 +++++++++------- public/sass/pages/_admin.scss | 12 ++ 15 files changed, 360 insertions(+), 100 deletions(-) create mode 100644 pkg/api/user_test.go create mode 100644 pkg/services/sqlstore/user_test.go delete mode 100644 public/app/features/admin/adminListUsersCtrl.js create mode 100644 public/app/features/admin/admin_list_users_ctrl.ts diff --git a/pkg/api/api.go b/pkg/api/api.go index d9fee8a0187..026bae2d894 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -123,6 +123,7 @@ func (hs *HttpServer) registerRoutes() { // users (admin permission required) r.Group("/users", func() { r.Get("/", wrap(SearchUsers)) + r.Get("/search", wrap(SearchUsersWithPaging)) r.Get("/:id", wrap(GetUserById)) r.Get("/:id/orgs", wrap(GetUserOrgList)) // query parameters /users/lookup?loginOrEmail=admin@example.com @@ -195,7 +196,7 @@ func (hs *HttpServer) registerRoutes() { // Data sources r.Group("/datasources", func() { - r.Get("/", GetDataSources) + r.Get("/", wrap(GetDataSources)) r.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), AddDataSource) r.Put("/:id", bind(m.UpdateDataSourceCommand{}), wrap(UpdateDataSource)) r.Delete("/:id", DeleteDataSourceById) diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index 455195aacb0..ddf8681c3c8 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -11,12 +11,11 @@ import ( "github.com/grafana/grafana/pkg/util" ) -func GetDataSources(c *middleware.Context) { +func GetDataSources(c *middleware.Context) Response { query := m.GetDataSourcesQuery{OrgId: c.OrgId} if err := bus.Dispatch(&query); err != nil { - c.JsonApiErr(500, "Failed to query datasources", err) - return + return ApiError(500, "Failed to query datasources", err) } result := make(dtos.DataSourceList, 0) @@ -46,7 +45,8 @@ func GetDataSources(c *middleware.Context) { } sort.Sort(result) - c.JSON(200, result) + + return Json(200, &result) } func GetDataSourceById(c *middleware.Context) Response { diff --git a/pkg/api/datasources_test.go b/pkg/api/datasources_test.go index aa3f6106afd..5ae752bea91 100644 --- a/pkg/api/datasources_test.go +++ b/pkg/api/datasources_test.go @@ -59,7 +59,9 @@ func loggedInUserScenario(desc string, url string, fn scenarioFunc) { Convey(desc+" "+url, func() { defer bus.ClearBusHandlers() - sc := &scenarioContext{} + sc := &scenarioContext{ + url: url, + } viewsPath, _ := filepath.Abs("../../public/views") sc.m = macaron.New() @@ -71,16 +73,18 @@ func loggedInUserScenario(desc string, url string, fn scenarioFunc) { sc.m.Use(middleware.GetContextHandler()) sc.m.Use(middleware.Sessioner(&session.Options{})) - sc.defaultHandler = func(c *middleware.Context) { + sc.defaultHandler = wrap(func(c *middleware.Context) Response { sc.context = c sc.context.UserId = TestUserID sc.context.OrgId = TestOrgID sc.context.OrgRole = models.ROLE_EDITOR if sc.handlerFunc != nil { - sc.handlerFunc(sc.context) + return sc.handlerFunc(sc.context) } - } - sc.m.SetAutoHead(true) + + return nil + }) + sc.m.Get(url, sc.defaultHandler) fn(sc) @@ -96,6 +100,20 @@ func (sc *scenarioContext) fakeReq(method, url string) *scenarioContext { return sc } +func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map[string]string) *scenarioContext { + sc.resp = httptest.NewRecorder() + req, err := http.NewRequest(method, url, nil) + q := req.URL.Query() + for k, v := range queryParams { + q.Add(k, v) + } + req.URL.RawQuery = q.Encode() + So(err, ShouldBeNil) + sc.req = req + + return sc +} + type scenarioContext struct { m *macaron.Macaron context *middleware.Context @@ -103,6 +121,7 @@ type scenarioContext struct { handlerFunc handlerFunc defaultHandler macaron.Handler req *http.Request + url string } func (sc *scenarioContext) exec() { @@ -110,4 +129,4 @@ func (sc *scenarioContext) exec() { } type scenarioFunc func(c *scenarioContext) -type handlerFunc func(c *middleware.Context) +type handlerFunc func(c *middleware.Context) Response diff --git a/pkg/api/user.go b/pkg/api/user.go index 7bce599d692..9a978503761 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -210,14 +210,46 @@ func ChangeUserPassword(c *middleware.Context, cmd m.ChangeUserPasswordCommand) // GET /api/users func SearchUsers(c *middleware.Context) Response { - query := m.SearchUsersQuery{Query: "", Page: 0, Limit: 1000} - if err := bus.Dispatch(&query); err != nil { + query, err := searchUser(c) + if err != nil { + return ApiError(500, "Failed to fetch users", err) + } + + return Json(200, query.Result.Users) +} + +// GET /api/paged-users +func SearchUsersWithPaging(c *middleware.Context) Response { + query, err := searchUser(c) + if err != nil { return ApiError(500, "Failed to fetch users", err) } return Json(200, query.Result) } +func searchUser(c *middleware.Context) (*m.SearchUsersQuery, error) { + perPage := c.QueryInt("perpage") + if perPage <= 0 { + perPage = 1000 + } + page := c.QueryInt("page") + + if page < 1 { + page = 1 + } + + query := &m.SearchUsersQuery{Query: "", Page: page, Limit: perPage} + if err := bus.Dispatch(query); err != nil { + return nil, err + } + + query.Result.Page = page + query.Result.PerPage = perPage + + return query, nil +} + func SetHelpFlag(c *middleware.Context) Response { flag := c.ParamsInt64(":id") diff --git a/pkg/api/user_test.go b/pkg/api/user_test.go new file mode 100644 index 00000000000..6aa9dd9adbf --- /dev/null +++ b/pkg/api/user_test.go @@ -0,0 +1,109 @@ +package api + +import ( + "testing" + + "github.com/grafana/grafana/pkg/models" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" + . "github.com/smartystreets/goconvey/convey" +) + +func TestUserApiEndpoint(t *testing.T) { + Convey("Given a user is logged in", t, func() { + mockResult := models.SearchUserQueryResult{ + Users: []*models.UserSearchHitDTO{ + {Name: "user1"}, + {Name: "user2"}, + }, + TotalCount: 2, + } + + loggedInUserScenario("When calling GET on", "/api/users", func(sc *scenarioContext) { + var sentLimit int + var sendPage int + bus.AddHandler("test", func(query *models.SearchUsersQuery) error { + query.Result = mockResult + + sentLimit = query.Limit + sendPage = query.Page + + return nil + }) + + sc.handlerFunc = SearchUsers + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + + So(sentLimit, ShouldEqual, 1000) + So(sendPage, ShouldEqual, 1) + + respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) + So(err, ShouldBeNil) + So(len(respJSON.MustArray()), ShouldEqual, 2) + }) + + loggedInUserScenario("When calling GET with page and limit querystring parameters on", "/api/users", func(sc *scenarioContext) { + var sentLimit int + var sendPage int + bus.AddHandler("test", func(query *models.SearchUsersQuery) error { + query.Result = mockResult + + sentLimit = query.Limit + sendPage = query.Page + + return nil + }) + + sc.handlerFunc = SearchUsers + sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec() + + So(sentLimit, ShouldEqual, 10) + So(sendPage, ShouldEqual, 2) + }) + + loggedInUserScenario("When calling GET on", "/api/users/search", func(sc *scenarioContext) { + var sentLimit int + var sendPage int + bus.AddHandler("test", func(query *models.SearchUsersQuery) error { + query.Result = mockResult + + sentLimit = query.Limit + sendPage = query.Page + + return nil + }) + + sc.handlerFunc = SearchUsersWithPaging + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + + So(sentLimit, ShouldEqual, 1000) + So(sendPage, ShouldEqual, 1) + + respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) + So(err, ShouldBeNil) + + So(respJSON.Get("totalCount").MustInt(), ShouldEqual, 2) + So(len(respJSON.Get("users").MustArray()), ShouldEqual, 2) + }) + + loggedInUserScenario("When calling GET with page and perpage querystring parameters on", "/api/users/search", func(sc *scenarioContext) { + var sentLimit int + var sendPage int + bus.AddHandler("test", func(query *models.SearchUsersQuery) error { + query.Result = mockResult + + sentLimit = query.Limit + sendPage = query.Page + + return nil + }) + + sc.handlerFunc = SearchUsersWithPaging + sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec() + + So(sentLimit, ShouldEqual, 10) + So(sendPage, ShouldEqual, 2) + }) + }) +} diff --git a/pkg/models/user.go b/pkg/models/user.go index e14f4486ba3..e0a36be8c0a 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -130,7 +130,14 @@ type SearchUsersQuery struct { Page int Limit int - Result []*UserSearchHitDTO + Result SearchUserQueryResult +} + +type SearchUserQueryResult struct { + TotalCount int64 `json:"totalCount"` + Users []*UserSearchHitDTO `json:"users"` + Page int `json:"page"` + PerPage int `json:"perPage"` } type GetUserOrgListQuery struct { diff --git a/pkg/services/sqlstore/org_test.go b/pkg/services/sqlstore/org_test.go index f52175c2e5c..e7c718fc9a8 100644 --- a/pkg/services/sqlstore/org_test.go +++ b/pkg/services/sqlstore/org_test.go @@ -63,8 +63,8 @@ func TestAccountDataAccess(t *testing.T) { err := SearchUsers(&query) So(err, ShouldBeNil) - So(query.Result[0].Email, ShouldEqual, "ac1@test.com") - So(query.Result[1].Email, ShouldEqual, "ac2@test.com") + So(query.Result.Users[0].Email, ShouldEqual, "ac1@test.com") + So(query.Result.Users[1].Email, ShouldEqual, "ac2@test.com") }) Convey("Given an added org user", func() { diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index b26a9153f55..791a38f2d01 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -344,12 +344,21 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error { } func SearchUsers(query *m.SearchUsersQuery) error { - query.Result = make([]*m.UserSearchHitDTO, 0) + query.Result = m.SearchUserQueryResult{ + Users: make([]*m.UserSearchHitDTO, 0), + } sess := x.Table("user") sess.Where("email LIKE ?", query.Query+"%") - sess.Limit(query.Limit, query.Limit*query.Page) + offset := query.Limit * (query.Page - 1) + sess.Limit(query.Limit, offset) sess.Cols("id", "email", "name", "login", "is_admin") - err := sess.Find(&query.Result) + if err := sess.Find(&query.Result.Users); err != nil { + return err + } + + user := m.User{} + count, err := x.Count(&user) + query.Result.TotalCount = count return err } diff --git a/pkg/services/sqlstore/user_test.go b/pkg/services/sqlstore/user_test.go new file mode 100644 index 00000000000..53a50f9631c --- /dev/null +++ b/pkg/services/sqlstore/user_test.go @@ -0,0 +1,45 @@ +package sqlstore + +import ( + "fmt" + "testing" + + . "github.com/smartystreets/goconvey/convey" + + "github.com/grafana/grafana/pkg/models" +) + +func TestUserDataAccess(t *testing.T) { + + Convey("Testing DB", t, func() { + InitTestDB(t) + + var err error + for i := 0; i < 5; i++ { + err = CreateUser(&models.CreateUserCommand{ + Email: fmt.Sprint("user", i, "@test.com"), + Name: fmt.Sprint("user", i), + Login: fmt.Sprint("user", i), + }) + So(err, ShouldBeNil) + } + + Convey("Can return the first page of users and a total count", func() { + query := models.SearchUsersQuery{Query: "", Page: 1, Limit: 3} + err = SearchUsers(&query) + + So(err, ShouldBeNil) + So(len(query.Result.Users), ShouldEqual, 3) + So(query.Result.TotalCount, ShouldEqual, 5) + }) + + Convey("Can return the second page of users and a total count", func() { + query := models.SearchUsersQuery{Query: "", Page: 2, Limit: 3} + err = SearchUsers(&query) + + So(err, ShouldBeNil) + So(len(query.Result.Users), ShouldEqual, 2) + So(query.Result.TotalCount, ShouldEqual, 5) + }) + }) +} diff --git a/public/app/core/routes/routes.ts b/public/app/core/routes/routes.ts index 28e1dcf1cd1..3ccf89bae01 100644 --- a/public/app/core/routes/routes.ts +++ b/public/app/core/routes/routes.ts @@ -113,6 +113,7 @@ function setupAngularRoutes($routeProvider, $locationProvider) { .when('/admin/users', { templateUrl: 'public/app/features/admin/partials/users.html', controller : 'AdminListUsersCtrl', + controllerAs: 'ctrl', resolve: loadAdminBundle, }) .when('/admin/users/create', { diff --git a/public/app/features/admin/admin.ts b/public/app/features/admin/admin.ts index b93fd07a059..6809e4c54ab 100644 --- a/public/app/features/admin/admin.ts +++ b/public/app/features/admin/admin.ts @@ -1,4 +1,4 @@ -import './adminListUsersCtrl'; +import AdminListUsersCtrl from './admin_list_users_ctrl'; import './adminListOrgsCtrl'; import './adminEditOrgCtrl'; import './adminEditUserCtrl'; @@ -37,3 +37,4 @@ export class AdminStatsCtrl { coreModule.controller('AdminSettingsCtrl', AdminSettingsCtrl); coreModule.controller('AdminHomeCtrl', AdminHomeCtrl); coreModule.controller('AdminStatsCtrl', AdminStatsCtrl); +coreModule.controller('AdminListUsersCtrl', AdminListUsersCtrl); diff --git a/public/app/features/admin/adminListUsersCtrl.js b/public/app/features/admin/adminListUsersCtrl.js deleted file mode 100644 index 721adcfb98c..00000000000 --- a/public/app/features/admin/adminListUsersCtrl.js +++ /dev/null @@ -1,38 +0,0 @@ -define([ - 'angular', -], -function (angular) { - 'use strict'; - - var module = angular.module('grafana.controllers'); - - module.controller('AdminListUsersCtrl', function($scope, backendSrv) { - - $scope.init = function() { - $scope.getUsers(); - }; - - $scope.getUsers = function() { - backendSrv.get('/api/users').then(function(users) { - $scope.users = users; - }); - }; - - $scope.deleteUser = function(user) { - $scope.appEvent('confirm-modal', { - title: 'Delete', - text: 'Do you want to delete ' + user.login + '?', - icon: 'fa-trash', - yesText: 'Delete', - onConfirm: function() { - backendSrv.delete('/api/admin/users/' + user.id).then(function() { - $scope.getUsers(); - }); - } - }); - }; - - $scope.init(); - - }); -}); diff --git a/public/app/features/admin/admin_list_users_ctrl.ts b/public/app/features/admin/admin_list_users_ctrl.ts new file mode 100644 index 00000000000..1347457bc06 --- /dev/null +++ b/public/app/features/admin/admin_list_users_ctrl.ts @@ -0,0 +1,49 @@ +/// + +export default class AdminListUsersCtrl { + users: any; + pages = []; + perPage = 1000; + page = 1; + totalPages: number; + showPaging = false; + + /** @ngInject */ + constructor(private $scope, private backendSrv) { + this.getUsers(); + } + + getUsers() { + this.backendSrv.get(`/api/users/search?perpage=${this.perPage}&page=${this.page}`).then((result) => { + this.users = result.users; + this.page = result.page; + this.perPage = result.perPage; + this.totalPages = Math.ceil(result.totalCount / result.perPage); + this.showPaging = this.totalPages > 1; + this.pages = []; + + for (var i = 1; i < this.totalPages+1; i++) { + this.pages.push({ page: i, current: i === this.page}); + } + }); + } + + navigateToPage(page) { + this.page = page.page; + this.getUsers(); + } + + deleteUser(user) { + this.$scope.appEvent('confirm-modal', { + title: 'Delete', + text: 'Do you want to delete ' + user.login + '?', + icon: 'fa-trash', + yesText: 'Delete', + onConfirm: () => { + this.backendSrv.delete('/api/admin/users/' + user.id).then(() => { + this.getUsers(); + }); + } + }); + } +} diff --git a/public/app/features/admin/partials/users.html b/public/app/features/admin/partials/users.html index a1c86391088..57714380c67 100644 --- a/public/app/features/admin/partials/users.html +++ b/public/app/features/admin/partials/users.html @@ -1,49 +1,62 @@ - - - Users - + + + Users +
- +
+ + + + + + + + + + + + + + + + + + + + + -
IdNameLoginEmailGrafana Admin
{{user.id}}{{user.name}}{{user.login}}{{user.email}}{{user.isAdmin}} + + + Edit + +    + + + +
- - - - - - - - - - - - - - - - - - - - -
IdNameLoginEmailGrafana Admin
{{user.id}}{{user.name}}{{user.login}}{{user.email}}{{user.isAdmin}} - - - Edit - -    - - - -
+ +
+ +
+
    +
  1. + +
  2. +
+
diff --git a/public/sass/pages/_admin.scss b/public/sass/pages/_admin.scss index 30bc3ca8b34..b2be062849d 100644 --- a/public/sass/pages/_admin.scss +++ b/public/sass/pages/_admin.scss @@ -8,3 +8,15 @@ td.admin-settings-key { padding-left: 20px; } +.admin-list-table { + margin-bottom: 20px; +} + +.admin-list-paging { + float: right; + li { + display: inline-block; + padding-left: 10px; + margin-bottom: 5px; + } +} From 89fdcc84de63fb4bb18dac908f14ff1588923da7 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 13 Feb 2017 13:52:42 +0100 Subject: [PATCH 078/301] docs: release 4.1.2 --- CHANGELOG.md | 3 ++- docs/sources/installation/debian.md | 6 +++--- docs/sources/installation/rpm.md | 8 ++++---- packaging/publish/publish_both.sh | 4 ++-- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b515930a4e..d62249d74e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,6 @@ * **LINE**: Adds image to notification message [#7417](https://github.com/grafana/grafana/pull/7417), thx [@Erliz](https://github.com/Erliz) * **Dataproxy**: Only allow get that begins with api/ to access Prometheus [#7459](https://github.com/grafana/grafana/pull/7459), thx [@mtanda](https://github.com/mtanda) * **Hipchat**: Adds support for sending alert notifications to hipchat [#6451](https://github.com/grafana/grafana/issues/6451), thx [@jregovic](https://github.com/jregovic) -* **Data Sources**: Sorting for lists of data sources in UI is now case insensitive [#7491](https://github.com/grafana/grafana/issues/7491) * **Graph**: Add full series name as title for legends. [#7493](https://github.com/grafana/grafana/pull/7493), thx [@kolobaev](https://github.com/kolobaev) * **Table**: Add a message when queries returns no data. [#6109](https://github.com/grafana/grafana/issues/6109), thx [@xginn8](https://github.com/xginn8) * **Graph**: Set max width for series names in legend tables. [#2385](https://github.com/grafana/grafana/issues/2385), thx [@kolobaev](https://github.com/kolobaev) @@ -39,6 +38,8 @@ ### Bugfixes * **Table**: Fixes broken annotation rendering mode in the table panel [#7268](https://github.com/grafana/grafana/issues/7268) +* **Data Sources**: Sorting for lists of data sources in UI is now case insensitive [#7491](https://github.com/grafana/grafana/issues/7491) +* **Admin**: Support more then 1000 users in global users list [#7469](https://github.com/grafana/grafana/issues/7469) # 4.1.1 (2017-01-11) diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index 9c6140c0c6d..fb5c649ad1d 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -15,14 +15,14 @@ weight = 1 Description | Download ------------ | ------------- -Stable for Debian-based Linux | [4.1.1 (x86-64 deb)](https://grafanarel.s3.amazonaws.com/builds/grafana_4.1.1-1484211277_amd64.deb) +Stable for Debian-based Linux | [4.1.2 (x86-64 deb)](https://grafanarel.s3.amazonaws.com/builds/grafana_4.1.2-1486989747_amd64.deb) ## Install Stable ``` -$ wget https://grafanarel.s3.amazonaws.com/builds/grafana_4.1.1-1484211277_amd64.deb +$ wget https://grafanarel.s3.amazonaws.com/builds/grafana_4.1.2-1486989747_amd64.deb $ sudo apt-get install -y adduser libfontconfig -$ sudo dpkg -i grafana_4.1.1-1484211277_amd64.deb +$ sudo dpkg -i grafana_4.1.2-1486989747_amd64.deb ``` ## APT Repository diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md index 63181fc535d..56125a4951a 100644 --- a/docs/sources/installation/rpm.md +++ b/docs/sources/installation/rpm.md @@ -15,24 +15,24 @@ weight = 2 Description | Download ------------ | ------------- -Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [4.1.1 (x86-64 rpm)](https://grafanarel.s3.amazonaws.com/builds/grafana-4.1.1-1484211277.x86_64.rpm) +Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [4.1.2 (x86-64 rpm)](https://grafanarel.s3.amazonaws.com/builds/grafana-4.1.2-1486989747.x86_64.rpm) ## Install Stable You can install Grafana using Yum directly. - $ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-4.1.1-1484211277.x86_64.rpm + $ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-4.1.2-1486989747.x86_64.rpm Or install manually using `rpm`. #### On CentOS / Fedora / Redhat: $ sudo yum install initscripts fontconfig - $ sudo rpm -Uvh grafana-4.1.1-1484211277.x86_64.rpm + $ sudo rpm -Uvh grafana-4.1.2-1486989747.x86_64.rpm #### On OpenSuse: - $ sudo rpm -i --nodeps grafana-4.1.1-1484211277.x86_64.rpm + $ sudo rpm -i --nodeps grafana-4.1.2-1486989747.x86_64.rpm ## Install via YUM Repository diff --git a/packaging/publish/publish_both.sh b/packaging/publish/publish_both.sh index 2f24ff6b3ea..3a70c6d9133 100755 --- a/packaging/publish/publish_both.sh +++ b/packaging/publish/publish_both.sh @@ -1,6 +1,6 @@ #! /usr/bin/env bash -deb_ver=4.1.0-1484127817 -rpm_ver=4.1.0-1484127817 +deb_ver=4.1.2-1486989747 +rpm_ver=4.1.2-1486989747 wget https://grafanarel.s3.amazonaws.com/builds/grafana_${deb_ver}_amd64.deb From c7560edba52a29c189c60c6f4ed0093b820119e2 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 13 Feb 2017 14:17:26 +0100 Subject: [PATCH 079/301] docs: release 4.1.2 for windows --- docs/sources/installation/windows.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/installation/windows.md b/docs/sources/installation/windows.md index 0036a50694d..73816d98a40 100644 --- a/docs/sources/installation/windows.md +++ b/docs/sources/installation/windows.md @@ -13,7 +13,7 @@ weight = 3 Description | Download ------------ | ------------- -Latest stable package for Windows | [grafana.4.1.1.windows-x64.zip](https://grafanarel.s3.amazonaws.com/builds/grafana-4.1.1.windows-x64.zip) +Latest stable package for Windows | [grafana.4.1.2.windows-x64.zip](https://grafanarel.s3.amazonaws.com/builds/grafana-4.1.2.windows-x64.zip) ## Configure From 2c68c071c00c37ab4ab42ba8a6d5cd318881389b Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 13 Feb 2017 15:11:45 +0100 Subject: [PATCH 080/301] changelog: update release of 2017-02-13 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d62249d74e0..c10b14a128a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,7 @@ * **Prometheus**: Add support for basic auth in Prometheus tsdb package [#6799](https://github.com/grafana/grafana/issues/6799), thx [@hagen1778](https://github.com/hagen1778) * **OAuth**: Redirect to original page when logging in with OAuth [#7513](https://github.com/grafana/grafana/issues/7513) -# 4.1.2 (unreleased) +# 4.1.2 (2017-02-13) ### Bugfixes * **Table**: Fixes broken annotation rendering mode in the table panel [#7268](https://github.com/grafana/grafana/issues/7268) From 2ad2b96133283e35158edc993db8806e69bb28ef Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 13 Feb 2017 15:26:10 +0100 Subject: [PATCH 081/301] build: avoid progress bar update on yarn install this will save us ~100TB of unneeded data in the long term. --- Makefile | 2 +- appveyor.yml | 2 +- scripts/build/build.sh | 2 +- scripts/circle-test.sh | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index dbb345473ec..c52e707c4b1 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ deps-go: go run build.go setup deps-js: - yarn install --pure-lockfile + yarn install --pure-lockfile --no-progress deps: deps-go deps-js diff --git a/appveyor.yml b/appveyor.yml index 30ccabb38d7..d0c1bfb0eaa 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -13,7 +13,7 @@ install: # install nodejs and npm - ps: Install-Product node $env:nodejs_version - npm install -g yarn - - yarn install --pure-lockfile + - yarn install --pure-lockfile --no-progress - npm install -g grunt-cli - appveyor DownloadFile https://storage.googleapis.com/golang/go%GOVERSION%.windows-amd64.zip - 7z x go%GOVERSION%.windows-amd64.zip -y -oC:\ > NUL diff --git a/scripts/build/build.sh b/scripts/build/build.sh index 9674c8a0b8c..6e1b2cbe5b9 100755 --- a/scripts/build/build.sh +++ b/scripts/build/build.sh @@ -31,7 +31,7 @@ else go run build.go -buildNumber=${CIRCLE_BUILD_NUM} build fi -yarn install --pure-lockfile +yarn install --pure-lockfile --no-progress source /etc/profile.d/rvm.sh rvm use 2.1.9 --default diff --git a/scripts/circle-test.sh b/scripts/circle-test.sh index 726ba528363..7b1b0c6ca30 100755 --- a/scripts/circle-test.sh +++ b/scripts/circle-test.sh @@ -14,7 +14,7 @@ cd /home/ubuntu/.go_workspace/src/github.com/grafana/grafana rm -rf node_modules npm install -g yarn -yarn install --pure-lockfile +yarn install --pure-lockfile --no-progress exit_if_fail npm test From fc0de84701285d715694ed1f02bb0be1d0950ef2 Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 13 Feb 2017 18:19:14 -0800 Subject: [PATCH 082/301] update influx logo --- .../datasource/influxdb/img/influxdb_logo.svg | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/public/app/plugins/datasource/influxdb/img/influxdb_logo.svg b/public/app/plugins/datasource/influxdb/img/influxdb_logo.svg index 6fad144f8c7..5316a4c6c7c 100644 --- a/public/app/plugins/datasource/influxdb/img/influxdb_logo.svg +++ b/public/app/plugins/datasource/influxdb/img/influxdb_logo.svg @@ -1,15 +1,26 @@ - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + From b46fac34d9bdaa7bb54d962406c52f8bbc8fb958 Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 13 Feb 2017 18:38:13 -0800 Subject: [PATCH 083/301] replace elastic logo --- .../elasticsearch/img/elasticsearch.svg | 12 ++++++++++++ .../datasource/elasticsearch/img/logo_large.png | Bin 22570 -> 0 bytes .../datasource/elasticsearch/plugin.json | 4 ++-- 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100755 public/app/plugins/datasource/elasticsearch/img/elasticsearch.svg delete mode 100644 public/app/plugins/datasource/elasticsearch/img/logo_large.png diff --git a/public/app/plugins/datasource/elasticsearch/img/elasticsearch.svg b/public/app/plugins/datasource/elasticsearch/img/elasticsearch.svg new file mode 100755 index 00000000000..7c3078468a0 --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/img/elasticsearch.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/public/app/plugins/datasource/elasticsearch/img/logo_large.png b/public/app/plugins/datasource/elasticsearch/img/logo_large.png deleted file mode 100644 index 5ded1d8f4386220953cbeca7c96bc385602722da..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22570 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 M07*qoM6N<$f`GRF(*OVf diff --git a/public/app/plugins/datasource/elasticsearch/plugin.json b/public/app/plugins/datasource/elasticsearch/plugin.json index d4b07ac8e00..62d6a93b3b0 100644 --- a/public/app/plugins/datasource/elasticsearch/plugin.json +++ b/public/app/plugins/datasource/elasticsearch/plugin.json @@ -11,8 +11,8 @@ }, "keywords": ["elasticsearch"], "logos": { - "small": "img/logo_large.png", - "large": "img/logo_large.png" + "small": "img/elasticsearch.svg", + "large": "img/elasticsearch.svg" }, "links": [ {"name": "elastic.co", "url": "https://www.elastic.co/products/elasticsearch"} From 2749dd8711cf77cce04340c18b157202f86c8ad7 Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 13 Feb 2017 18:47:01 -0800 Subject: [PATCH 084/301] elasticsearch svg --- .../elasticsearch/img/elasticsearch.svg | 80 ++++++++++++++++--- 1 file changed, 68 insertions(+), 12 deletions(-) diff --git a/public/app/plugins/datasource/elasticsearch/img/elasticsearch.svg b/public/app/plugins/datasource/elasticsearch/img/elasticsearch.svg index 7c3078468a0..e9034bcf3c4 100755 --- a/public/app/plugins/datasource/elasticsearch/img/elasticsearch.svg +++ b/public/app/plugins/datasource/elasticsearch/img/elasticsearch.svg @@ -1,12 +1,68 @@ - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From a1e071d3c565c273ab153e836b8d43cca845701f Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 13 Feb 2017 18:47:59 -0800 Subject: [PATCH 085/301] missing space --- .../app/plugins/datasource/elasticsearch/img/elasticsearch.svg | 1 - 1 file changed, 1 deletion(-) diff --git a/public/app/plugins/datasource/elasticsearch/img/elasticsearch.svg b/public/app/plugins/datasource/elasticsearch/img/elasticsearch.svg index e9034bcf3c4..33a4d656061 100755 --- a/public/app/plugins/datasource/elasticsearch/img/elasticsearch.svg +++ b/public/app/plugins/datasource/elasticsearch/img/elasticsearch.svg @@ -1,4 +1,3 @@ - Date: Mon, 13 Feb 2017 23:30:15 -0800 Subject: [PATCH 086/301] Adding more physics units --- public/app/core/utils/kbn.js | 44 ++++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/public/app/core/utils/kbn.js b/public/app/core/utils/kbn.js index 182ab0e309a..819206bf94a 100644 --- a/public/app/core/utils/kbn.js +++ b/public/app/core/utils/kbn.js @@ -459,11 +459,19 @@ function($, _) { kbn.valueFormats.humidity = kbn.formatBuilders.fixedUnit('%H'); // Pressure - kbn.valueFormats.pressurembar = kbn.formatBuilders.fixedUnit('mbar'); + kbn.valueFormats.pressurebar = kbn.formatBuilders.decimalSIPrefix('bar'); + kbn.valueFormats.pressurembar = kbn.formatBuilders.decimalSIPrefix('bar', -1); + kbn.valueFormats.pressurekbar = kbn.formatBuilders.decimalSIPrefix('bar', 1); kbn.valueFormats.pressurehpa = kbn.formatBuilders.fixedUnit('hPa'); kbn.valueFormats.pressurehg = kbn.formatBuilders.fixedUnit('"Hg'); kbn.valueFormats.pressurepsi = kbn.formatBuilders.scaledUnits(1000, [' psi', ' ksi', ' Mpsi']); + // Force + kbn.valueFormats.forceNm = kbn.formatBuilders.decimalSIPrefix('Nm'); + kbn.valueFormats.forcekNm = kbn.formatBuilders.decimalSIPrefix('Nm', 1); + kbn.valueFormats.forceN = kbn.formatBuilders.decimalSIPrefix('N'); + kbn.valueFormats.forcekN = kbn.formatBuilders.decimalSIPrefix('N', 1); + // Length kbn.valueFormats.lengthm = kbn.formatBuilders.decimalSIPrefix('m'); kbn.valueFormats.lengthmm = kbn.formatBuilders.decimalSIPrefix('m', -1); @@ -480,6 +488,13 @@ function($, _) { kbn.valueFormats.litre = kbn.formatBuilders.decimalSIPrefix('L'); kbn.valueFormats.mlitre = kbn.formatBuilders.decimalSIPrefix('L', -1); kbn.valueFormats.m3 = kbn.formatBuilders.decimalSIPrefix('m3'); + kbn.valueFormats.gallons = kbn.formatBuilders.fixedUnit('gal'); + + // Flow + kbn.valueFormats.flowgpm = kbn.formatBuilders.fixedUnit('gpm'); + kbn.valueFormats.flowcms = kbn.formatBuilders.fixedUnit('cms'); + kbn.valueFormats.flowcfs = kbn.formatBuilders.fixedUnit('cfs'); + kbn.valueFormats.flowcfm = kbn.formatBuilders.fixedUnit('cfm'); // Time kbn.valueFormats.hertz = kbn.formatBuilders.decimalSIPrefix('Hz'); @@ -790,9 +805,10 @@ function($, _) { { text: 'volume', submenu: [ - {text: 'millilitre', value: 'mlitre'}, - {text: 'litre', value: 'litre' }, - {text: 'cubic metre', value: 'm3' }, + {text: 'millilitre', value: 'mlitre' }, + {text: 'litre', value: 'litre' }, + {text: 'cubic metre', value: 'm3' }, + {text: 'gallons', value: 'gallons'}, ] }, { @@ -827,10 +843,30 @@ function($, _) { text: 'pressure', submenu: [ {text: 'Millibars', value: 'pressurembar'}, + {text: 'Bars', value: 'pressurebar' }, + {text: 'Kilobars', value: 'pressurekbar'}, {text: 'Hectopascals', value: 'pressurehpa' }, {text: 'Inches of mercury', value: 'pressurehg' }, {text: 'PSI', value: 'pressurepsi' }, ] + }, + { + text: 'force', + submenu: [ + {text: 'Newton-meters (Nm)', value: 'forceNm' }, + {text: 'Kilonewton-meters (kNm)', value: 'forcekNm' }, + {text: 'Newtons (N)', value: 'forceN' }, + {text: 'Kilonewtons (kN)', value: 'forcekN' }, + ] + }, + { + text: 'flow', + submenu: [ + {text: 'Gallons/min (gpm)', value: 'flowgpm' }, + {text: 'Cubic meters/sec (cms)', value: 'flowcms' }, + {text: 'Cubic feet/sec (cfs)', value: 'flowcfs' }, + {text: 'Cubic feet/min (cfm)', value: 'flowcfm' }, + ] } ]; }; From 1c8509ea58cd1be46a29f9aff1fe3c4d3fb57ec9 Mon Sep 17 00:00:00 2001 From: xginn8 Date: Tue, 14 Feb 2017 02:49:43 -0500 Subject: [PATCH 087/301] wrap text in annotations modal (#7549) annotation: wrap text in drop_element, fixes #7542 --- public/sass/mixins/_drop_element.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/public/sass/mixins/_drop_element.scss b/public/sass/mixins/_drop_element.scss index 290e49f4cad..0f7eda19efe 100644 --- a/public/sass/mixins/_drop_element.scss +++ b/public/sass/mixins/_drop_element.scss @@ -12,6 +12,7 @@ color: $theme-color; padding: 0.65rem; font-size: $font-size-sm; + word-wrap: break-word; max-width: 20rem; &:before { From 0523830716b4fdced7727fd7d38d43065f304926 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 14 Feb 2017 08:56:00 +0100 Subject: [PATCH 088/301] changelog: add note about closing #7542 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c10b14a128a..769d099f702 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ * **Sidemenu**: Disable sign out in sidemenu for AuthProxyEnabled [#7377](https://github.com/grafana/grafana/pull/7377), thx [@solugebefola](https://github.com/solugebefola) * **Prometheus**: Add support for basic auth in Prometheus tsdb package [#6799](https://github.com/grafana/grafana/issues/6799), thx [@hagen1778](https://github.com/hagen1778) * **OAuth**: Redirect to original page when logging in with OAuth [#7513](https://github.com/grafana/grafana/issues/7513) +* **Annotations**: Wrap text in annotations tooltip [#7549](https://github.com/grafana/grafana/pull/7549), thx [@xginn8](https://github.com/xginn8) # 4.1.2 (2017-02-13) From 6761661e106de004b97f4f63995ab0301f1f5b8c Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 14 Feb 2017 10:03:23 +0100 Subject: [PATCH 089/301] changelog: adds note about closing #7542 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 769d099f702..316939bac54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,7 +33,7 @@ * **Sidemenu**: Disable sign out in sidemenu for AuthProxyEnabled [#7377](https://github.com/grafana/grafana/pull/7377), thx [@solugebefola](https://github.com/solugebefola) * **Prometheus**: Add support for basic auth in Prometheus tsdb package [#6799](https://github.com/grafana/grafana/issues/6799), thx [@hagen1778](https://github.com/hagen1778) * **OAuth**: Redirect to original page when logging in with OAuth [#7513](https://github.com/grafana/grafana/issues/7513) -* **Annotations**: Wrap text in annotations tooltip [#7549](https://github.com/grafana/grafana/pull/7549), thx [@xginn8](https://github.com/xginn8) +* **Annotations**: Wrap text in annotations tooltip [#7542](https://github.com/grafana/grafana/pull/7542), thx [@xginn8](https://github.com/xginn8) # 4.1.2 (2017-02-13) From 9f80e8c03dcf2e219ac0259530fbdab3dcdcb78f Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 14 Feb 2017 10:06:31 +0100 Subject: [PATCH 090/301] changelog: rearrange closed issues --- CHANGELOG.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 316939bac54..1b92c861bce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,25 +1,27 @@ # 4.2.0 (unreleased) ## Enhancements -* **Alerting**: Added Telegram alert notifier [#7098](https://github.com/grafana/grafana/pull/7098), thx [@leonoff](https://github.com/leonoff) +* **Telegram**: Added Telegram alert notifier [#7098](https://github.com/grafana/grafana/pull/7098), thx [@leonoff](https://github.com/leonoff) * **Templating**: Make $__interval and $__interval_ms global built in variables that can be used in by any datasource (in panel queries), closes [#7190](https://github.com/grafana/grafana/issues/7190), closes [#6582](https://github.com/grafana/grafana/issues/6582) * **S3 Image Store**: External s3 image store (used in alert notifications) now support AWS IAM Roles, closes [#6985](https://github.com/grafana/grafana/issues/6985), [#7058](https://github.com/grafana/grafana/issues/7058) thx [@mtanda](https://github.com/mtanda) -* **Optimzation**: Never issue refresh event when Grafana tab is not visible [#7218](https://github.com/grafana/grafana/issues/7218), thx [@mtanda](https://github.com/mtanda) -* **Browser History**: Browser back/forward now works time ranges / zoom, [#7259](https://github.com/grafana/grafana/issues/7259) * **SingleStat**: Implements diff aggregation method for singlestat [#7234](https://github.com/grafana/grafana/issues/7234), thx [@oliverpool](https://github.com/oliverpool) * **Dataproxy**: Added setting to enable more verbose logging in dataproxy [#7209](https://github.com/grafana/grafana/pull/7209), thx [@Ricky-N](https://github.com/Ricky-N) * **Alerting**: Better information about why an alert triggered [#7035](https://github.com/grafana/grafana/issues/7035) * **LINE**: Add LINE as alerting notification channel [#7301](https://github.com/grafana/grafana/pull/7301), thx [@huydx](https://github.com/huydx) +* **LINE**: Adds image to notification message [#7417](https://github.com/grafana/grafana/pull/7417), thx [@Erliz](https://github.com/Erliz) +* **Hipchat**: Adds support for sending alert notifications to hipchat [#6451](https://github.com/grafana/grafana/issues/6451), thx [@jregovic](https://github.com/jregovic) + +## Minor Enhancements +* **Optimzation**: Never issue refresh event when Grafana tab is not visible [#7218](https://github.com/grafana/grafana/issues/7218), thx [@mtanda](https://github.com/mtanda) +* **Browser History**: Browser back/forward now works time ranges / zoom, [#7259](https://github.com/grafana/grafana/issues/7259) * **Elasticsearch**: Support for Min Doc Count options in Terms aggregation [#7324](https://github.com/grafana/grafana/pull/7324), thx [@lpic10](https://github.com/lpic10) * **Elasticsearch**: Term aggregation limit can now be changed in template queries [#7112](https://github.com/grafana/grafana/issues/7112), thx [@FFalcon](https://github.com/FFalcon) -* **LINE**: Adds image to notification message [#7417](https://github.com/grafana/grafana/pull/7417), thx [@Erliz](https://github.com/Erliz) -* **Dataproxy**: Only allow get that begins with api/ to access Prometheus [#7459](https://github.com/grafana/grafana/pull/7459), thx [@mtanda](https://github.com/mtanda) -* **Hipchat**: Adds support for sending alert notifications to hipchat [#6451](https://github.com/grafana/grafana/issues/6451), thx [@jregovic](https://github.com/jregovic) * **Graph**: Add full series name as title for legends. [#7493](https://github.com/grafana/grafana/pull/7493), thx [@kolobaev](https://github.com/kolobaev) * **Table**: Add a message when queries returns no data. [#6109](https://github.com/grafana/grafana/issues/6109), thx [@xginn8](https://github.com/xginn8) * **Graph**: Set max width for series names in legend tables. [#2385](https://github.com/grafana/grafana/issues/2385), thx [@kolobaev](https://github.com/kolobaev) * **Database**: Allow max db connection pool configuration [#7427](https://github.com/grafana/grafana/issues/7427), thx [@huydx](https://github.com/huydx) * **Datasources** Delete datsource by name [#7476](https://github.com/grafana/grafana/issues/7476), thx [@huydx](https://github.com/huydx) +* **Dataproxy**: Only allow get that begins with api/ to access Prometheus [#7459](https://github.com/grafana/grafana/pull/7459), thx [@mtanda](https://github.com/mtanda) ## Tech From 0c9f664ea088859282566403a2d47abecabd37df Mon Sep 17 00:00:00 2001 From: Ryuichi Sakai Date: Sat, 4 Feb 2017 18:42:00 +0900 Subject: [PATCH 091/301] Make timeout for snapshot creation configurable --- .../app/features/dashboard/partials/shareModal.html | 13 +++++++++++++ public/app/features/dashboard/shareSnapshotCtrl.js | 3 ++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard/partials/shareModal.html b/public/app/features/dashboard/partials/shareModal.html index bab75a7f523..f9bf5ccf4ad 100644 --- a/public/app/features/dashboard/partials/shareModal.html +++ b/public/app/features/dashboard/partials/shareModal.html @@ -148,6 +148,19 @@
+
+ +
+ + +
-
- - -
+ + + +
From 902cf5f8899ab31a7fd143fbcb61d29c5238ceac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 15 Feb 2017 14:26:35 +0100 Subject: [PATCH 112/301] fix(): google analytics fix --- pkg/api/frontendsettings.go | 1 + public/app/core/services/analytics.js | 32 ++++++++++++++++++--------- public/views/index.html | 12 ---------- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 3690784375d..c0e27897ff4 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -141,6 +141,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro "authProxyEnabled": setting.AuthProxyEnabled, "ldapEnabled": setting.LdapEnabled, "alertingEnabled": setting.AlertingEnabled, + "googleAnalyticsId": setting.GoogleAnalyticsId, "buildInfo": map[string]interface{}{ "version": setting.BuildVersion, "commit": setting.BuildCommit, diff --git a/public/app/core/services/analytics.js b/public/app/core/services/analytics.js index 982cdd23d1d..a20a11bb2b6 100644 --- a/public/app/core/services/analytics.js +++ b/public/app/core/services/analytics.js @@ -1,27 +1,39 @@ define([ 'angular', - '../core_module', + 'jquery', + 'app/core/core_module', + 'app/core/config', ], -function(angular, coreModule) { +function(angular, $, coreModule, config) { 'use strict'; coreModule.default.service('googleAnalyticsSrv', function($rootScope, $location) { - var first = true; + + function gaInit() { + $.getScript('https://www.google-analytics.com/analytics.js'); // jQuery shortcut + var ga = window.ga = window.ga || function () { (ga.q = ga.q || []).push(arguments); }; ga.l = +new Date; + ga('create', config.googleAnalyticsId, 'auto'); + return ga; + } this.init = function() { + $rootScope.$on('$viewContentLoaded', function() { - // skip first - if (first) { - first = false; - return; - } - window.ga('send', 'pageview', { page: $location.url() }); + var track = { page: $location.url() }; + + var ga = window.ga || gaInit(); + + ga('set', track); + ga('send', 'pageview'); }); + }; }).run(function(googleAnalyticsSrv) { - if (window.ga) { + + if (config.googleAnalyticsId) { googleAnalyticsSrv.init(); } + }); }); diff --git a/public/views/index.html b/public/views/index.html index 8d231ed2b68..55fe9408a73 100644 --- a/public/views/index.html +++ b/public/views/index.html @@ -91,18 +91,6 @@ - [[if .GoogleAnalyticsId]] - - [[end]] - [[if .GoogleTagManagerId]]