浏览代码

Merge branch 'master' into react-panels

Torkel Ödegaard 7 年之前
父节点
当前提交
0b794ff685
共有 100 个文件被更改,包括 5686 次插入1518 次删除
  1. 1 1
      .bra.toml
  2. 125 18
      .circleci/config.yml
  3. 3 0
      .dockerignore
  4. 3 3
      .github/CONTRIBUTING.md
  5. 2 1
      .gitignore
  6. 0 13
      .jscs.json
  7. 0 37
      .jshintrc
  8. 61 2
      CHANGELOG.md
  9. 82 0
      Dockerfile
  10. 12 3
      Gopkg.lock
  11. 1 1
      Gopkg.toml
  12. 0 1
      Gruntfile.js
  13. 10 1
      Makefile
  14. 1 1
      NOTICE.md
  15. 31 19
      README.md
  16. 8 8
      ROADMAP.md
  17. 16 18
      build.go
  18. 19 0
      conf/defaults.ini
  19. 5 0
      conf/ldap.toml
  20. 4 0
      conf/sample.ini
  21. 10 5
      devenv/README.md
  22. 1 1
      devenv/bulk-dashboards/bulk-dashboards.yaml
  23. 33 2
      devenv/datasources.yaml
  24. 0 592
      devenv/dev-dashboards/dashboard_with_rows.json
  25. 28 50
      devenv/dev-dashboards/datasource_tests_mssql_fakedata.json
  26. 606 116
      devenv/dev-dashboards/datasource_tests_mssql_unittest.json
  27. 20 47
      devenv/dev-dashboards/datasource_tests_mysql_fakedata.json
  28. 288 178
      devenv/dev-dashboards/datasource_tests_mysql_unittest.json
  29. 36 51
      devenv/dev-dashboards/datasource_tests_postgres_fakedata.json
  30. 416 162
      devenv/dev-dashboards/datasource_tests_postgres_unittest.json
  31. 1558 0
      devenv/dev-dashboards/panel_tests_graph.json
  32. 574 0
      devenv/dev-dashboards/panel_tests_singlestat.json
  33. 453 0
      devenv/dev-dashboards/panel_tests_table.json
  34. 3 3
      devenv/dev-dashboards/testdata_alerts.json
  35. 15 9
      devenv/setup.sh
  36. 2 1
      docker/blocks/nginx_proxy/Dockerfile
  37. 3 0
      docker/blocks/nginx_proxy/htpasswd
  38. 20 1
      docker/blocks/nginx_proxy/nginx.conf
  39. 85 0
      docker/blocks/openldap/ldap_dev.toml
  40. 2 5
      docker/blocks/openldap/notes.md
  41. 2 1
      docs/sources/alerting/notifications.md
  42. 2 0
      docs/sources/features/datasources/cloudwatch.md
  43. 3 3
      docs/sources/features/datasources/elasticsearch.md
  44. 8 2
      docs/sources/features/datasources/mssql.md
  45. 7 1
      docs/sources/features/datasources/mysql.md
  46. 10 2
      docs/sources/features/datasources/postgres.md
  47. 26 0
      docs/sources/features/datasources/prometheus.md
  48. 2 2
      docs/sources/guides/basic_concepts.md
  49. 0 1
      docs/sources/http_api/alerting.md
  50. 1 1
      docs/sources/http_api/dashboard.md
  51. 1 1
      docs/sources/http_api/folder.md
  52. 286 0
      docs/sources/http_api/playlist.md
  53. 33 0
      docs/sources/http_api/user.md
  54. 118 8
      docs/sources/installation/configuration.md
  55. 8 3
      docs/sources/installation/docker.md
  56. 13 2
      docs/sources/installation/ldap.md
  57. 8 11
      docs/sources/project/building_from_source.md
  58. 29 9
      docs/sources/reference/templating.md
  59. 1 1
      jest.config.js
  60. 0 40
      karma.conf.js
  61. 9 21
      package.json
  62. 52 0
      packaging/docker/Dockerfile
  63. 43 0
      packaging/docker/README.md
  64. 13 0
      packaging/docker/build-deploy.sh
  65. 25 0
      packaging/docker/build.sh
  66. 16 0
      packaging/docker/custom/Dockerfile
  67. 6 0
      packaging/docker/deploy_to_k8s.sh
  68. 24 0
      packaging/docker/push_to_docker_hub.sh
  69. 88 0
      packaging/docker/run.sh
  70. 2 2
      pkg/api/api.go
  71. 16 2
      pkg/api/datasources.go
  72. 7 1
      pkg/api/login.go
  73. 2 2
      pkg/api/metrics.go
  74. 1 0
      pkg/api/playlist.go
  75. 9 2
      pkg/api/pluginproxy/ds_proxy.go
  76. 28 8
      pkg/api/pluginproxy/ds_proxy_test.go
  77. 15 0
      pkg/api/user.go
  78. 12 3
      pkg/components/imguploader/webdavuploader.go
  79. 13 0
      pkg/components/imguploader/webdavuploader_test.go
  80. 7 0
      pkg/login/ext_user.go
  81. 14 3
      pkg/login/ldap.go
  82. 6 3
      pkg/login/ldap_settings.go
  83. 42 8
      pkg/login/ldap_test.go
  84. 8 0
      pkg/metrics/metrics.go
  85. 1 0
      pkg/models/models.go
  86. 1 1
      pkg/models/playlist.go
  87. 9 8
      pkg/models/user_auth.go
  88. 3 0
      pkg/plugins/datasource_plugin.go
  89. 1 2
      pkg/services/alerting/notifier.go
  90. 1 1
      pkg/services/alerting/notifiers/slack.go
  91. 3 3
      pkg/services/provisioning/datasources/config_reader.go
  92. 14 0
      pkg/services/provisioning/datasources/config_reader_test.go
  93. 1 1
      pkg/services/provisioning/datasources/datasources.go
  94. 25 0
      pkg/services/provisioning/datasources/testdata/multiple-org-default/config.yaml
  95. 1 0
      pkg/services/sqlstore/alert.go
  96. 13 2
      pkg/services/sqlstore/alert_test.go
  97. 2 1
      pkg/services/sqlstore/dashboard_test.go
  98. 40 1
      pkg/services/sqlstore/migrations/user_mig.go
  99. 11 5
      pkg/services/sqlstore/migrator/migrator.go
  100. 7 0
      pkg/services/sqlstore/migrator/types.go

+ 1 - 1
.bra.toml

@@ -9,7 +9,7 @@ watch_dirs = [
 	"$WORKDIR/public/views",
 	"$WORKDIR/public/views",
 	"$WORKDIR/conf",
 	"$WORKDIR/conf",
 ]
 ]
-watch_exts = [".go", ".ini", ".toml"]
+watch_exts = [".go", ".ini", ".toml", ".template.html"]
 build_delay = 1500
 build_delay = 1500
 cmds = [
 cmds = [
   ["go", "run", "build.go", "-dev", "build-server"],
   ["go", "run", "build.go", "-dev", "build-server"],

+ 125 - 18
.circleci/config.yml

@@ -5,9 +5,11 @@ aliases:
       ignore: /.*/
       ignore: /.*/
     tags:
     tags:
       only: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/
       only: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/
-  - &filter-not-release
+  - &filter-not-release-or-master
     tags:
     tags:
       ignore: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/
       ignore: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/
+    branches:
+      ignore: master
   - &filter-only-master
   - &filter-only-master
     branches:
     branches:
       only: master
       only: master
@@ -89,7 +91,7 @@ jobs:
           name: run linters
           name: run linters
           command: 'gometalinter.v2 --enable-gc --vendor --deadline 10m --disable-all --enable=deadcode --enable=ineffassign --enable=structcheck --enable=unconvert --enable=varcheck ./...'
           command: 'gometalinter.v2 --enable-gc --vendor --deadline 10m --disable-all --enable=deadcode --enable=ineffassign --enable=structcheck --enable=unconvert --enable=varcheck ./...'
       - run:
       - run:
-          name: run go vet 
+          name: run go vet
           command: 'go vet ./pkg/...'
           command: 'go vet ./pkg/...'
 
 
   test-frontend:
   test-frontend:
@@ -102,6 +104,7 @@ jobs:
       - run:
       - run:
           name: yarn install
           name: yarn install
           command: 'yarn install --pure-lockfile --no-progress'
           command: 'yarn install --pure-lockfile --no-progress'
+          no_output_timeout: 15m
       - save_cache:
       - save_cache:
           key: dependency-cache-{{ checksum "yarn.lock" }}
           key: dependency-cache-{{ checksum "yarn.lock" }}
           paths:
           paths:
@@ -144,6 +147,12 @@ jobs:
       - run:
       - run:
           name: sign packages
           name: sign packages
           command: './scripts/build/sign_packages.sh'
           command: './scripts/build/sign_packages.sh'
+      - run:
+          name: verify signed packages
+          command: |
+            mkdir -p ~/.rpmdb/pubkeys
+            curl -s https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana > ~/.rpmdb/pubkeys/grafana.key
+            ./scripts/build/verify_signed_packages.sh dist/*.rpm
       - run:
       - run:
           name: sha-sum packages
           name: sha-sum packages
           command: 'go run build.go sha-dist'
           command: 'go run build.go sha-dist'
@@ -156,8 +165,65 @@ jobs:
             - dist/grafana*
             - dist/grafana*
             - scripts/*.sh
             - scripts/*.sh
             - scripts/publish
             - scripts/publish
-      - store_artifacts:
-          path: dist
+
+  build:
+    docker:
+     - image: grafana/build-container:1.0.0
+    working_directory: /go/src/github.com/grafana/grafana
+    steps:
+      - checkout
+      - run:
+          name: prepare build tools
+          command: '/tmp/bootstrap.sh'
+      - run:
+          name: build and package grafana
+          command: './scripts/build/build.sh'
+      - run:
+          name: sign packages
+          command: './scripts/build/sign_packages.sh'
+      - run:
+          name: sha-sum packages
+          command: 'go run build.go sha-dist'
+      - persist_to_workspace:
+          root: .
+          paths:
+            - dist/grafana*
+
+  grafana-docker-master:
+    docker:
+      - image: docker:stable-git
+    steps:
+      - checkout
+      - attach_workspace:
+          at: .
+      - setup_remote_docker
+      - run: docker info
+      - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
+      - run: cd packaging/docker && ./build-deploy.sh "master-${CIRCLE_SHA1}"
+
+  grafana-docker-pr:
+    docker:
+      - image: docker:stable-git
+    steps:
+      - checkout
+      - attach_workspace:
+          at: .
+      - setup_remote_docker
+      - run: docker info
+      - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
+      - run: cd packaging/docker && ./build.sh "${CIRCLE_SHA1}"
+
+  grafana-docker-release:
+      docker:
+        - image: docker:stable-git
+      steps:
+        - checkout
+        - attach_workspace:
+            at: .
+        - setup_remote_docker
+        - run: docker info
+        - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
+        - run: cd packaging/docker && ./build-deploy.sh "${CIRCLE_TAG}"
 
 
   build-enterprise:
   build-enterprise:
     docker:
     docker:
@@ -213,9 +279,6 @@ jobs:
       - run:
       - run:
           name: Trigger Windows build
           name: Trigger Windows build
           command: './scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} ${CIRCLE_SHA1} master'
           command: './scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} ${CIRCLE_SHA1} master'
-      - run:
-          name: Trigger Docker build
-          command: './scripts/trigger_docker_build.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN} master-$(echo "${CIRCLE_SHA1}" | cut -b1-7)'
       - run:
       - run:
           name: Publish to Grafana.com
           name: Publish to Grafana.com
           command: |
           command: |
@@ -237,30 +300,27 @@ jobs:
       - run:
       - run:
           name: Trigger Windows build
           name: Trigger Windows build
           command: './scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} ${CIRCLE_SHA1} release'
           command: './scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} ${CIRCLE_SHA1} release'
-      - run:
-          name: Trigger Docker build
-          command: './scripts/trigger_docker_build.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN} ${CIRCLE_TAG}'
 
 
 workflows:
 workflows:
   version: 2
   version: 2
-  test-and-build:
+  build-master:
     jobs:
     jobs:
       - build-all:
       - build-all:
           filters: *filter-only-master
           filters: *filter-only-master
       - build-enterprise:
       - build-enterprise:
           filters: *filter-only-master
           filters: *filter-only-master
       - codespell:
       - codespell:
-          filters: *filter-not-release
+          filters: *filter-only-master
       - gometalinter:
       - gometalinter:
-          filters: *filter-not-release
+          filters: *filter-only-master
       - test-frontend:
       - test-frontend:
-          filters: *filter-not-release
+          filters: *filter-only-master
       - test-backend:
       - test-backend:
-          filters: *filter-not-release
+          filters: *filter-only-master
       - mysql-integration-test:
       - mysql-integration-test:
-          filters: *filter-not-release
+          filters: *filter-only-master
       - postgres-integration-test:
       - postgres-integration-test:
-          filters: *filter-not-release
+          filters: *filter-only-master
       - deploy-master:
       - deploy-master:
           requires:
           requires:
             - build-all
             - build-all
@@ -270,7 +330,17 @@ workflows:
             - gometalinter
             - gometalinter
             - mysql-integration-test
             - mysql-integration-test
             - postgres-integration-test
             - postgres-integration-test
-          filters: *filter-only-master           
+          filters: *filter-only-master
+      - grafana-docker-master:
+          requires:
+            - build-all
+            - test-backend
+            - test-frontend
+            - codespell
+            - gometalinter
+            - mysql-integration-test
+            - postgres-integration-test
+          filters: *filter-only-master
       - deploy-enterprise-master:
       - deploy-enterprise-master:
           requires:
           requires:
             - build-all
             - build-all
@@ -309,3 +379,40 @@ workflows:
             - mysql-integration-test
             - mysql-integration-test
             - postgres-integration-test
             - postgres-integration-test
           filters: *filter-only-release
           filters: *filter-only-release
+      - grafana-docker-release:
+          requires:
+            - build-all
+            - test-backend
+            - test-frontend
+            - codespell
+            - gometalinter
+            - mysql-integration-test
+            - postgres-integration-test
+          filters: *filter-only-release
+
+  build-branches-and-prs:
+      jobs:
+        - build:
+            filters: *filter-not-release-or-master
+        - codespell:
+            filters: *filter-not-release-or-master
+        - gometalinter:
+            filters: *filter-not-release-or-master
+        - test-frontend:
+            filters: *filter-not-release-or-master
+        - test-backend:
+            filters: *filter-not-release-or-master
+        - mysql-integration-test:
+            filters: *filter-not-release-or-master
+        - postgres-integration-test:
+            filters: *filter-not-release-or-master
+        - grafana-docker-pr:
+            requires:
+              - build
+              - test-backend
+              - test-frontend
+              - codespell
+              - gometalinter
+              - mysql-integration-test
+              - postgres-integration-test
+            filters: *filter-not-release-or-master

+ 3 - 0
.dockerignore

@@ -3,9 +3,12 @@
 .git
 .git
 .gitignore
 .gitignore
 .github
 .github
+.vscode
+bin
 data*
 data*
 dist
 dist
 docker
 docker
+Dockerfile
 docs
 docs
 dump.rdb
 dump.rdb
 node_modules
 node_modules

+ 3 - 3
.github/CONTRIBUTING.md

@@ -2,12 +2,12 @@ Follow the setup guide in README.md
 
 
 ### Rebuild frontend assets on source change
 ### Rebuild frontend assets on source change
 ```
 ```
-grunt && grunt watch
+yarn watch
 ```
 ```
 
 
 ### Rerun tests on source change
 ### Rerun tests on source change
 ```
 ```
-grunt karma:dev
+yarn jest
 ```
 ```
 
 
 ### Run tests for backend assets before commit
 ### Run tests for backend assets before commit
@@ -17,6 +17,6 @@ test -z "$(gofmt -s -l . | grep -v -E 'vendor/(github.com|golang.org|gopkg.in)'
 
 
 ### Run tests for frontend assets before commit
 ### Run tests for frontend assets before commit
 ```
 ```
-npm test
+yarn test
 go test -v ./pkg/...
 go test -v ./pkg/...
 ```
 ```

+ 2 - 1
.gitignore

@@ -58,6 +58,7 @@ debug.test
 /examples/*/dist
 /examples/*/dist
 /packaging/**/*.rpm
 /packaging/**/*.rpm
 /packaging/**/*.deb
 /packaging/**/*.deb
+/packaging/**/*.tar.gz
 
 
 # Ignore OSX indexing
 # Ignore OSX indexing
 .DS_Store
 .DS_Store
@@ -70,4 +71,4 @@ debug.test
 /vendor/**/appengine*
 /vendor/**/appengine*
 *.orig
 *.orig
 
 
-/devenv/dashboards/bulk-testing/*.json
+/devenv/bulk-dashboards/*.json

+ 0 - 13
.jscs.json

@@ -1,13 +0,0 @@
-{
-    "disallowImplicitTypeConversion": ["string"],
-    "disallowKeywords": ["with"],
-    "disallowMultipleLineBreaks": true,
-    "disallowMixedSpacesAndTabs": true,
-    "disallowTrailingWhitespace": true,
-    "requireSpacesInFunctionExpression": {
-        "beforeOpeningCurlyBrace": true
-    },
-    "disallowSpacesInsideArrayBrackets": true,
-    "disallowSpacesInsideParentheses": true,
-    "validateIndentation": 2
-}

+ 0 - 37
.jshintrc

@@ -1,37 +0,0 @@
-{
-  "browser": true,
-  "esversion": 6,
-  "bitwise":false,
-  "curly": true,
-  "eqnull": true,
-  "strict": false,
-  "devel": true,
-  "eqeqeq": true,
-  "forin": false,
-  "immed": true,
-  "supernew": true,
-  "expr": true,
-  "indent": 2,
-  "latedef": false,
-  "newcap": true,
-  "noarg": true,
-  "noempty": true,
-  "undef": true,
-  "boss": true,
-  "trailing": true,
-  "laxbreak": true,
-  "laxcomma": true,
-  "sub": true,
-  "unused": true,
-  "maxdepth": 6,
-  "maxlen": 140,
-
-  "globals": {
-    "System": true,
-    "Promise": true,
-    "define": true,
-    "require": true,
-    "Chromath": false,
-    "setImmediate": true
-  }
-}

+ 61 - 2
CHANGELOG.md

@@ -1,28 +1,87 @@
 # 5.3.0 (unreleased)
 # 5.3.0 (unreleased)
 
 
+* **OAuth**: Gitlab OAuth with support for filter by groups [#5623](https://github.com/grafana/grafana/issues/5623), thx [@BenoitKnecht](https://github.com/BenoitKnecht)
 * **Dataproxy**: Pass configured/auth headers to a Datasource [#10971](https://github.com/grafana/grafana/issues/10971), thx [@mrsiano](https://github.com/mrsiano)
 * **Dataproxy**: Pass configured/auth headers to a Datasource [#10971](https://github.com/grafana/grafana/issues/10971), thx [@mrsiano](https://github.com/mrsiano)
 * **Cleanup**: Make temp file time to live configurable [#11607](https://github.com/grafana/grafana/issues/11607), thx [@xapon](https://github.com/xapon)
 * **Cleanup**: Make temp file time to live configurable [#11607](https://github.com/grafana/grafana/issues/11607), thx [@xapon](https://github.com/xapon)
+* **LDAP**: Define Grafana Admin permission in ldap group mappings [#2469](https://github.com/grafana/grafana/issues/2496), PR [#12622](https://github.com/grafana/grafana/issues/12622)
+* **Cloudwatch**: CloudWatch GetMetricData support [#11487](https://github.com/grafana/grafana/issues/11487), thx [@mtanda](https://github.com/mtanda)
+* **Configuration**: Allow auto-assigning users to specific organization (other than Main. Org) [#1823](https://github.com/grafana/grafana/issues/1823) [#12801](https://github.com/grafana/grafana/issues/12801), thx [@gzzo](https://github.com/gzzo) and [@ofosos](https://github.com/ofosos)
+* **Profile**: List teams that the user is member of in current/active organization [#12476](https://github.com/grafana/grafana/issues/12476)
+* **LDAP**: Client certificates support [#12805](https://github.com/grafana/grafana/issues/12805), thx [@nyxi](https://github.com/nyxi)
+* **Postgres**: TimescaleDB support, e.g. use `time_bucket` for grouping by time when option enabled [#12680](https://github.com/grafana/grafana/pull/12680), thx [svenklemm](https://github.com/svenklemm)
 
 
 ### Minor
 ### Minor
 
 
 * **Api**: Delete nonexistent datasource should return 404 [#12313](https://github.com/grafana/grafana/issues/12313), thx [@AustinWinstanley](https://github.com/AustinWinstanley)
 * **Api**: Delete nonexistent datasource should return 404 [#12313](https://github.com/grafana/grafana/issues/12313), thx [@AustinWinstanley](https://github.com/AustinWinstanley)
 * **Dashboard**: Fix selecting current dashboard from search should not reload dashboard [#12248](https://github.com/grafana/grafana/issues/12248)
 * **Dashboard**: Fix selecting current dashboard from search should not reload dashboard [#12248](https://github.com/grafana/grafana/issues/12248)
+* **Dashboard**: Use uid when linking to dashboards internally in a dashboard [#10705](https://github.com/grafana/grafana/issues/10705)
 * **Singlestat**: Make colorization of prefix and postfix optional in singlestat [#11892](https://github.com/grafana/grafana/pull/11892), thx [@ApsOps](https://github.com/ApsOps)
 * **Singlestat**: Make colorization of prefix and postfix optional in singlestat [#11892](https://github.com/grafana/grafana/pull/11892), thx [@ApsOps](https://github.com/ApsOps)
-* **Table**: Make table sorting stable when null values exist [#12362](https://github.com/grafana/grafana/pull/12362), thx [@bz2](https://github.com/bz2)
 * **Prometheus**: Fix graph panel bar width issue in aligned prometheus queries [#12379](https://github.com/grafana/grafana/issues/12379)
 * **Prometheus**: Fix graph panel bar width issue in aligned prometheus queries [#12379](https://github.com/grafana/grafana/issues/12379)
 * **Prometheus**: Heatmap - fix unhandled error when some points are missing [#12484](https://github.com/grafana/grafana/issues/12484)
 * **Prometheus**: Heatmap - fix unhandled error when some points are missing [#12484](https://github.com/grafana/grafana/issues/12484)
+* **Prometheus**: Add $__interval, $__interval_ms, $__range, $__range_s & $__range_ms support for dashboard and template queries [#12597](https://github.com/grafana/grafana/issues/12597) [#12882](https://github.com/grafana/grafana/issues/12882), thx [@roidelapluie](https://github.com/roidelapluie)
 * **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda)
 * **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda)
+* **Variables**: Limit amount of queries executed when updating variable that other variable(s) are dependent on [#11890](https://github.com/grafana/grafana/issues/11890)
+* **Variables**: Support query variable refresh when another variable referenced in `Regex` field change its value [#12952](https://github.com/grafana/grafana/issues/12952), thx [@franciscocpg](https://github.com/franciscocpg)
+* **Variables**: Support variables in query variable `Custom all value` field [#12965](https://github.com/grafana/grafana/issues/12965), thx [@franciscocpg](https://github.com/franciscocpg)
+* **Postgres/MySQL/MSSQL**: New $__unixEpochGroup and $__unixEpochGroupAlias macros [#12892](https://github.com/grafana/grafana/issues/12892), thx [@svenklemm](https://github.com/svenklemm)
+* **Postgres/MySQL/MSSQL**: Add previous fill mode to $__timeGroup macro which will fill in previously seen value when point is missing [#12756](https://github.com/grafana/grafana/issues/12756), thx [@svenklemm](https://github.com/svenklemm)
 * **Postgres/MySQL/MSSQL**: Use floor rounding in $__timeGroup macro function [#12460](https://github.com/grafana/grafana/issues/12460), thx [@svenklemm](https://github.com/svenklemm)
 * **Postgres/MySQL/MSSQL**: Use floor rounding in $__timeGroup macro function [#12460](https://github.com/grafana/grafana/issues/12460), thx [@svenklemm](https://github.com/svenklemm)
+* **Postgres/MySQL/MSSQL**: Use metric column as prefix when returning multiple value columns [#12727](https://github.com/grafana/grafana/issues/12727), thx [@svenklemm](https://github.com/svenklemm)
+* **Postgres/MySQL/MSSQL**: New $__timeGroupAlias macro. Postgres $__timeGroup no longer automatically adds time column alias [#12749](https://github.com/grafana/grafana/issues/12749), thx [@svenklemm](https://github.com/svenklemm)
+* **Postgres/MySQL/MSSQL**: Escape single quotes in variables [#12785](https://github.com/grafana/grafana/issues/12785), thx [@eMerzh](https://github.com/eMerzh)
 * **MySQL/MSSQL**: Use datetime format instead of epoch for $__timeFilter, $__timeFrom and $__timeTo macros [#11618](https://github.com/grafana/grafana/issues/11618) [#11619](https://github.com/grafana/grafana/issues/11619), thx [@AustinWinstanley](https://github.com/AustinWinstanley)
 * **MySQL/MSSQL**: Use datetime format instead of epoch for $__timeFilter, $__timeFrom and $__timeTo macros [#11618](https://github.com/grafana/grafana/issues/11618) [#11619](https://github.com/grafana/grafana/issues/11619), thx [@AustinWinstanley](https://github.com/AustinWinstanley)
+* **Postgres**: Escape ssl mode parameter in connectionstring [#12644](https://github.com/grafana/grafana/issues/12644), thx [@yogyrahmawan](https://github.com/yogyrahmawan)
 * **Github OAuth**: Allow changes of user info at Github to be synched to Grafana when signing in [#11818](https://github.com/grafana/grafana/issues/11818), thx [@rwaweber](https://github.com/rwaweber)
 * **Github OAuth**: Allow changes of user info at Github to be synched to Grafana when signing in [#11818](https://github.com/grafana/grafana/issues/11818), thx [@rwaweber](https://github.com/rwaweber)
 * **Alerting**: Fix diff and percent_diff reducers [#11563](https://github.com/grafana/grafana/issues/11563), thx [@jessetane](https://github.com/jessetane)
 * **Alerting**: Fix diff and percent_diff reducers [#11563](https://github.com/grafana/grafana/issues/11563), thx [@jessetane](https://github.com/jessetane)
+* **Alerting**: Fix rendering timeout which could cause notifications to not be sent due to rendering timing out [#12151](https://github.com/grafana/grafana/issues/12151)
+* **Cloudwatch**: Improved error handling [#12489](https://github.com/grafana/grafana/issues/12489), thx [@mtanda](https://github.com/mtanda)
+* **Cloudwatch**: AppSync metrics and dimensions [#12300](https://github.com/grafana/grafana/issues/12300), thx [@franciscocpg](https://github.com/franciscocpg)
+* **Cloudwatch**: Direct Connect metrics and dimensions [#12762](https://github.com/grafana/grafana/pulls/12762), thx [@mindriot88](https://github.com/mindriot88)
+* **Cloudwatch**: Added BurstBalance metric to list of AWS RDS metrics [#12561](https://github.com/grafana/grafana/pulls/12561), thx [@activeshadow](https://github.com/activeshadow)
+* **Cloudwatch**: Add new Redshift metrics and dimensions [#12063](https://github.com/grafana/grafana/pulls/12063), thx [@A21z](https://github.com/A21z)
+* **Table**: Adjust header contrast for the light theme [#12668](https://github.com/grafana/grafana/issues/12668)
+* **Table**: Fix link color when using light theme and thresholds in use [#12766](https://github.com/grafana/grafana/issues/12766)
+om/grafana/grafana/issues/12668)
+* **Table**: Fix for useless horizontal scrollbar for table panel [#9964](https://github.com/grafana/grafana/issues/9964)
+* **Table**: Make table sorting stable when null values exist [#12362](https://github.com/grafana/grafana/pull/12362), thx [@bz2](https://github.com/bz2)
+* **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731)
+* **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj)
+* **Units**: Change units to include characters for power of 2 and 3 [#12744](https://github.com/grafana/grafana/pull/12744), thx [@Worty](https://github.com/Worty)
+* **Units**: Polish złoty currency [#12691](https://github.com/grafana/grafana/pull/12691), thx [@mwegrzynek](https://github.com/mwegrzynek)
+* **Graph**: Option to hide series from tooltip [#3341](https://github.com/grafana/grafana/issues/3341), thx [@mtanda](https://github.com/mtanda)
+* **UI**: Fix iOS home screen "app" icon and Windows 10 app experience [#12752](https://github.com/grafana/grafana/issues/12752), thx [@andig](https://github.com/andig)
+* **Datasource**: Fix UI issue with secret fields after updating datasource [#11270](https://github.com/grafana/grafana/issues/11270)
+* **Plugins**: Convert URL-like text to links in plugins readme [#12843](https://github.com/grafana/grafana/pull/12843), thx [pgiraud](https://github.com/pgiraud)
+* **Docker**: Make it possible to set a specific plugin url [#12861](https://github.com/grafana/grafana/pull/12861), thx [ClementGautier](https://github.com/ClementGautier)
+* **Graphite**: Fix for quoting of int function parameters (when using variables) [#11927](https://github.com/grafana/grafana/pull/11927)
+* **InfluxDB**: Support timeFilter in query templating for InfluxDB [#12598](https://github.com/grafana/grafana/pull/12598), thx [kichristensen](https://github.com/kichristensen)
+* **Provisioning**: Should allow one default datasource per organisation [#12229](https://github.com/grafana/grafana/issues/12229)
+* **Heatmap**: Fix broken tooltip and crosshair on Firefox [#12486](https://github.com/grafana/grafana/issues/12486)
+
+### Breaking changes
+
+* Postgres datasource no longer automatically adds time column alias when using the $__timeGroup alias. However, there's code in place which should make this change backward compatible and shouldn't create any issues.
+
+### New experimental features
+
+These are new features that's still being worked on and are in an experimental phase. We incourage users to try these out and provide any feedback in related issue.
+
+* **Dashboard**: Auto fit dashboard panels to optimize space used for current TV / Monitor [#12768](https://github.com/grafana/grafana/issues/12768)
+
+### Tech
+
+* **Frontend**: Convert all Frontend Karma tests to Jest tests [#12224](https://github.com/grafana/grafana/issues/12224)
 
 
-# 5.2.2 (unreleased)
+# 5.2.2 (2018-07-25)
 
 
 ### Minor
 ### Minor
 
 
 * **Prometheus**: Fix graph panel bar width issue in aligned prometheus queries [#12379](https://github.com/grafana/grafana/issues/12379)
 * **Prometheus**: Fix graph panel bar width issue in aligned prometheus queries [#12379](https://github.com/grafana/grafana/issues/12379)
 * **Dashboard**: Dashboard links not updated when changing variables [#12506](https://github.com/grafana/grafana/issues/12506)
 * **Dashboard**: Dashboard links not updated when changing variables [#12506](https://github.com/grafana/grafana/issues/12506)
+* **Postgres/MySQL/MSSQL**: Fix connection leak [#12636](https://github.com/grafana/grafana/issues/12636) [#9827](https://github.com/grafana/grafana/issues/9827)
+* **Plugins**: Fix loading of external plugins [#12551](https://github.com/grafana/grafana/issues/12551)
+* **Dashboard**: Remove unwanted scrollbars in embedded panels [#12589](https://github.com/grafana/grafana/issues/12589)
+* **Prometheus**: Prevent error using $__interval_ms in query [#12533](https://github.com/grafana/grafana/pull/12533), thx [@mtanda](https://github.com/mtanda)
 
 
 # 5.2.1 (2018-06-29)
 # 5.2.1 (2018-06-29)
 
 

+ 82 - 0
Dockerfile

@@ -0,0 +1,82 @@
+# Golang build container
+FROM golang:1.10
+
+WORKDIR $GOPATH/src/github.com/grafana/grafana
+
+COPY Gopkg.toml Gopkg.lock ./
+COPY vendor vendor
+
+ARG DEP_ENSURE=""
+RUN if [ ! -z "${DEP_ENSURE}" ]; then \
+      go get -u github.com/golang/dep/cmd/dep && \
+      dep ensure --vendor-only; \
+    fi
+
+COPY pkg pkg
+COPY build.go build.go
+COPY package.json package.json
+
+RUN go run build.go build
+
+# Node build container
+FROM node:8
+
+WORKDIR /usr/src/app/
+
+COPY package.json yarn.lock ./
+RUN yarn install --pure-lockfile --no-progress
+
+COPY Gruntfile.js tsconfig.json tslint.json ./
+COPY public public
+COPY scripts scripts
+COPY emails emails
+
+ENV NODE_ENV production
+RUN ./node_modules/.bin/grunt build
+
+# Final container
+FROM debian:stretch-slim
+
+ARG GF_UID="472"
+ARG GF_GID="472"
+
+ENV PATH=/usr/share/grafana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
+    GF_PATHS_CONFIG="/etc/grafana/grafana.ini" \
+    GF_PATHS_DATA="/var/lib/grafana" \
+    GF_PATHS_HOME="/usr/share/grafana" \
+    GF_PATHS_LOGS="/var/log/grafana" \
+    GF_PATHS_PLUGINS="/var/lib/grafana/plugins" \
+    GF_PATHS_PROVISIONING="/etc/grafana/provisioning"
+
+WORKDIR $GF_PATHS_HOME
+
+RUN apt-get update && apt-get install -qq -y libfontconfig ca-certificates && \
+    apt-get autoremove -y && \
+    rm -rf /var/lib/apt/lists/*
+
+COPY conf ./conf
+
+RUN mkdir -p "$GF_PATHS_HOME/.aws" && \
+    groupadd -r -g $GF_GID grafana && \
+    useradd -r -u $GF_UID -g grafana grafana && \
+    mkdir -p "$GF_PATHS_PROVISIONING/datasources" \
+             "$GF_PATHS_PROVISIONING/dashboards" \
+             "$GF_PATHS_LOGS" \
+             "$GF_PATHS_PLUGINS" \
+             "$GF_PATHS_DATA" && \
+    cp "$GF_PATHS_HOME/conf/sample.ini" "$GF_PATHS_CONFIG" && \
+    cp "$GF_PATHS_HOME/conf/ldap.toml" /etc/grafana/ldap.toml && \
+    chown -R grafana:grafana "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" && \
+    chmod 777 "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS"
+
+COPY --from=0 /go/src/github.com/grafana/grafana/bin/linux-amd64/grafana-server /go/src/github.com/grafana/grafana/bin/linux-amd64/grafana-cli ./bin/
+COPY --from=1 /usr/src/app/public ./public
+COPY --from=1 /usr/src/app/tools ./tools
+COPY tools/phantomjs/render.js ./tools/phantomjs/render.js
+
+EXPOSE 3000
+
+COPY ./packaging/docker/run.sh /run.sh
+
+USER grafana
+ENTRYPOINT [ "/run.sh" ]

+ 12 - 3
Gopkg.lock

@@ -32,6 +32,7 @@
     "aws/credentials/ec2rolecreds",
     "aws/credentials/ec2rolecreds",
     "aws/credentials/endpointcreds",
     "aws/credentials/endpointcreds",
     "aws/credentials/stscreds",
     "aws/credentials/stscreds",
+    "aws/csm",
     "aws/defaults",
     "aws/defaults",
     "aws/ec2metadata",
     "aws/ec2metadata",
     "aws/endpoints",
     "aws/endpoints",
@@ -43,6 +44,8 @@
     "internal/shareddefaults",
     "internal/shareddefaults",
     "private/protocol",
     "private/protocol",
     "private/protocol/ec2query",
     "private/protocol/ec2query",
+    "private/protocol/eventstream",
+    "private/protocol/eventstream/eventstreamapi",
     "private/protocol/query",
     "private/protocol/query",
     "private/protocol/query/queryutil",
     "private/protocol/query/queryutil",
     "private/protocol/rest",
     "private/protocol/rest",
@@ -54,8 +57,8 @@
     "service/s3",
     "service/s3",
     "service/sts"
     "service/sts"
   ]
   ]
-  revision = "c7cd1ebe87257cde9b65112fc876b0339ea0ac30"
-  version = "v1.13.49"
+  revision = "fde4ded7becdeae4d26bf1212916aabba79349b4"
+  version = "v1.14.12"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
@@ -424,6 +427,12 @@
   revision = "1744e2970ca51c86172c8190fadad617561ed6e7"
   revision = "1744e2970ca51c86172c8190fadad617561ed6e7"
   version = "v1.0.0"
   version = "v1.0.0"
 
 
+[[projects]]
+  branch = "master"
+  name = "github.com/shurcooL/sanitized_anchor_name"
+  packages = ["."]
+  revision = "86672fcb3f950f35f2e675df2240550f2a50762f"
+
 [[projects]]
 [[projects]]
   name = "github.com/smartystreets/assertions"
   name = "github.com/smartystreets/assertions"
   packages = [
   packages = [
@@ -670,6 +679,6 @@
 [solve-meta]
 [solve-meta]
   analyzer-name = "dep"
   analyzer-name = "dep"
   analyzer-version = 1
   analyzer-version = 1
-  inputs-digest = "85cc057e0cc074ab5b43bd620772d63d51e07b04e8782fcfe55e6929d2fc40f7"
+  inputs-digest = "cb8e7fd81f23ec987fc4d5dd9d31ae0f1164bc2f30cbea2fe86e0d97dd945beb"
   solver-name = "gps-cdcl"
   solver-name = "gps-cdcl"
   solver-version = 1
   solver-version = 1

+ 1 - 1
Gopkg.toml

@@ -36,7 +36,7 @@ ignored = [
 
 
 [[constraint]]
 [[constraint]]
   name = "github.com/aws/aws-sdk-go"
   name = "github.com/aws/aws-sdk-go"
-  version = "1.12.65"
+  version = "1.13.56"
 
 
 [[constraint]]
 [[constraint]]
   branch = "master"
   branch = "master"

+ 0 - 1
Gruntfile.js

@@ -1,4 +1,3 @@
-/* jshint node:true */
 'use strict';
 'use strict';
 module.exports = function (grunt) {
 module.exports = function (grunt) {
   var os = require('os');
   var os = require('os');

+ 10 - 1
Makefile

@@ -24,6 +24,15 @@ build-js:
 
 
 build: build-go build-js
 build: build-go build-js
 
 
+build-docker-dev:
+	@echo "\033[92mInfo:\033[0m the frontend code is expected to be built already."
+	go run build.go -goos linux -pkg-arch amd64 ${OPT} build package-only latest
+	cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
+	cd packaging/docker && docker build --tag grafana/grafana:dev .
+
+build-docker-full:
+	docker build --tag grafana/grafana:dev .
+
 test-go:
 test-go:
 	go test -v ./pkg/...
 	go test -v ./pkg/...
 
 
@@ -36,4 +45,4 @@ run:
 	./bin/grafana-server
 	./bin/grafana-server
 
 
 protoc:
 protoc:
-	protoc -I pkg/tsdb/models pkg/tsdb/models/*.proto --go_out=plugins=grpc:pkg/tsdb/models/.
+	protoc -I pkg/tsdb/models pkg/tsdb/models/*.proto --go_out=plugins=grpc:pkg/tsdb/models/.

+ 1 - 1
NOTICE.md

@@ -1,5 +1,5 @@
 
 
-Copyright 2014-2017 Grafana Labs
+Copyright 2014-2018 Grafana Labs
 
 
 This software is based on Kibana: 
 This software is based on Kibana: 
 Copyright 2012-2013 Elasticsearch BV
 Copyright 2012-2013 Elasticsearch BV

+ 31 - 19
README.md

@@ -43,7 +43,7 @@ To build the assets, rebuild on file change, and serve them by Grafana's webserv
 ```bash
 ```bash
 npm install -g yarn
 npm install -g yarn
 yarn install --pure-lockfile
 yarn install --pure-lockfile
-npm run watch
+yarn watch
 ```
 ```
 
 
 Build the assets, rebuild on file change with Hot Module Replacement (HMR), and serve them by webpack-dev-server (http://localhost:3333):
 Build the assets, rebuild on file change with Hot Module Replacement (HMR), and serve them by webpack-dev-server (http://localhost:3333):
@@ -54,14 +54,9 @@ env GRAFANA_THEME=light yarn start
 ```
 ```
 Note: HMR for Angular is not supported. If you edit files in the Angular part of the app, the whole page will reload.
 Note: HMR for Angular is not supported. If you edit files in the Angular part of the app, the whole page will reload.
 
 
-Run tests 
+Run tests
 ```bash
 ```bash
-npm run jest
-```
-
-Run karma tests
-```bash
-npm run karma
+yarn jest
 ```
 ```
 
 
 ### Recompile backend on source change
 ### Recompile backend on source change
@@ -74,6 +69,15 @@ bra run
 
 
 Open grafana in your browser (default: `http://localhost:3000`) and login with admin user (default: `user/pass = admin/admin`).
 Open grafana in your browser (default: `http://localhost:3000`) and login with admin user (default: `user/pass = admin/admin`).
 
 
+### Building a docker image (on linux/amd64)
+
+This builds a docker image from your local sources:
+
+1. Build the frontend `go run build.go build-frontend`
+2. Build the docker image `make build-docker-dev`
+
+The resulting image will be tagged as `grafana/grafana:dev`
+
 ### Dev config
 ### Dev config
 
 
 Create a custom.ini in the conf directory to override default configuration options.
 Create a custom.ini in the conf directory to override default configuration options.
@@ -89,30 +93,38 @@ In your custom.ini uncomment (remove the leading `;`) sign. And set `app_mode =
 #### Frontend
 #### Frontend
 Execute all frontend tests
 Execute all frontend tests
 ```bash
 ```bash
-npm run test
+yarn test
 ```
 ```
 
 
-Writing & watching frontend tests (we have two test runners)
+Writing & watching frontend tests
 
 
-- jest for all new tests that do not require browser context (React+more)
-   - Start watcher: `npm run jest`
-   - Jest will run all test files that end with the name ".jest.ts"
-- karma + mocha is used for testing angularjs components. We do want to migrate these test to jest over time (if possible).
-  - Start watcher: `npm run karma`
-  - Karma+Mocha runs all files that end with the name "_specs.ts".
+- Start watcher: `yarn jest`
+- Jest will run all test files that end with the name ".test.ts"
 
 
 #### Backend
 #### Backend
 ```bash
 ```bash
 # Run Golang tests using sqlite3 as database (default)
 # Run Golang tests using sqlite3 as database (default)
-go test ./pkg/... 
+go test ./pkg/...
 
 
 # Run Golang tests using mysql as database - convenient to use /docker/blocks/mysql_tests
 # Run Golang tests using mysql as database - convenient to use /docker/blocks/mysql_tests
-GRAFANA_TEST_DB=mysql go test ./pkg/... 
+GRAFANA_TEST_DB=mysql go test ./pkg/...
 
 
 # Run Golang tests using postgres as database - convenient to use /docker/blocks/postgres_tests
 # Run Golang tests using postgres as database - convenient to use /docker/blocks/postgres_tests
-GRAFANA_TEST_DB=postgres go test ./pkg/... 
+GRAFANA_TEST_DB=postgres go test ./pkg/...
 ```
 ```
 
 
+## Building custom docker image
+
+You can build a custom image using Docker, which doesn't require installing any dependencies besides docker itself.
+```bash
+git clone https://github.com/grafana/grafana
+cd grafana
+docker build -t grafana:dev .
+docker run -d --name=grafana -p 3000:3000 grafana:dev
+```
+
+Open grafana in your browser (default: `http://localhost:3000`) and login with admin user (default: `user/pass = admin/admin`).
+
 ## Contribute
 ## Contribute
 
 
 If you have any idea for an improvement or found a bug, do not hesitate to open an issue.
 If you have any idea for an improvement or found a bug, do not hesitate to open an issue.

+ 8 - 8
ROADMAP.md

@@ -1,9 +1,10 @@
-# Roadmap (2018-06-26)
+# Roadmap (2018-08-07)
 
 
 This roadmap is a tentative plan for the core development team. Things change constantly as PRs come in and priorities change. 
 This roadmap is a tentative plan for the core development team. Things change constantly as PRs come in and priorities change. 
 But it will give you an idea of our current vision and plan. 
 But it will give you an idea of our current vision and plan. 
   
   
 ### Short term (1-2 months)
 ### Short term (1-2 months)
+  - PRs & Bugs
   - Multi-Stat panel
   - Multi-Stat panel
   - Metrics & Log Explore UI 
   - Metrics & Log Explore UI 
  
  
@@ -11,17 +12,16 @@ But it will give you an idea of our current vision and plan.
   - React Panels 
   - React Panels 
   - Change visualization (panel type) on the fly. 
   - Change visualization (panel type) on the fly. 
   - Templating Query Editor UI Plugin hook
   - Templating Query Editor UI Plugin hook
+  - Backend plugins
   
   
 ### Long term (4 - 8 months)
 ### Long term (4 - 8 months)
-
-- Alerting improvements (silence, per series tracking, etc)
-- Progress on React migration
+ - Alerting improvements (silence, per series tracking, etc)
+ - Progress on React migration
 
 
 ### In a distant future far far away
 ### In a distant future far far away
-
-- Meta queries 
-- Integrated light weight TSDB
-- Web socket & live data sources
+ - Meta queries 
+ - Integrated light weight TSDB
+ - Web socket & live data sources
 
 
 ### Outside contributions
 ### Outside contributions
 We know this is being worked on right now by contributors (and we hope to merge it when it's ready). 
 We know this is being worked on right now by contributors (and we hope to merge it when it's ready). 

+ 16 - 18
build.go

@@ -64,6 +64,10 @@ func main() {
 
 
 	readVersionFromPackageJson()
 	readVersionFromPackageJson()
 
 
+	if pkgArch == "" {
+		pkgArch = goarch
+	}
+
 	log.Printf("Version: %s, Linux Version: %s, Package Iteration: %s\n", version, linuxPackageVersion, linuxPackageIteration)
 	log.Printf("Version: %s, Linux Version: %s, Package Iteration: %s\n", version, linuxPackageVersion, linuxPackageIteration)
 
 
 	if flag.NArg() == 0 {
 	if flag.NArg() == 0 {
@@ -105,10 +109,17 @@ func main() {
 
 
 		case "package":
 		case "package":
 			grunt(gruntBuildArg("build")...)
 			grunt(gruntBuildArg("build")...)
-			packageGrafana()
+			grunt(gruntBuildArg("package")...)
+			if goos == "linux" {
+				createLinuxPackages()
+			}
 
 
 		case "package-only":
 		case "package-only":
-			packageGrafana()
+			grunt(gruntBuildArg("package")...)
+			if goos == "linux" {
+				createLinuxPackages()
+			}
+
 
 
 		case "pkg-rpm":
 		case "pkg-rpm":
 			grunt(gruntBuildArg("release")...)
 			grunt(gruntBuildArg("release")...)
@@ -133,22 +144,6 @@ func main() {
 	}
 	}
 }
 }
 
 
-func packageGrafana() {
-	platformArg := fmt.Sprintf("--platform=%v", goos)
-	previousPkgArch := pkgArch
-	if pkgArch == "" {
-		pkgArch = goarch
-	}
-	postProcessArgs := gruntBuildArg("package")
-	postProcessArgs = append(postProcessArgs, platformArg)
-	grunt(postProcessArgs...)
-	pkgArch = previousPkgArch
-
-	if goos == "linux" {
-		createLinuxPackages()
-	}
-}
-
 func makeLatestDistCopies() {
 func makeLatestDistCopies() {
 	files, err := ioutil.ReadDir("dist")
 	files, err := ioutil.ReadDir("dist")
 	if err != nil {
 	if err != nil {
@@ -330,6 +325,7 @@ func createPackage(options linuxPackageOptions) {
 	name := "grafana"
 	name := "grafana"
 	if enterprise {
 	if enterprise {
 		name += "-enterprise"
 		name += "-enterprise"
+		args = append(args, "--replaces", "grafana")
 	}
 	}
 	args = append(args, "--name", name)
 	args = append(args, "--name", name)
 
 
@@ -403,6 +399,8 @@ func gruntBuildArg(task string) []string {
 	if phjsToRelease != "" {
 	if phjsToRelease != "" {
 		args = append(args, fmt.Sprintf("--phjsToRelease=%v", phjsToRelease))
 		args = append(args, fmt.Sprintf("--phjsToRelease=%v", phjsToRelease))
 	}
 	}
+	args = append(args, fmt.Sprintf("--platform=%v", goos))
+
 	return args
 	return args
 }
 }
 
 

+ 19 - 0
conf/defaults.ini

@@ -213,6 +213,9 @@ allow_org_create = false
 # Set to true to automatically assign new users to the default organization (id 1)
 # Set to true to automatically assign new users to the default organization (id 1)
 auto_assign_org = true
 auto_assign_org = true
 
 
+# Set this value to automatically add new users to the provided organization (if auto_assign_org above is set to true)
+auto_assign_org_id = 1
+
 # Default role new users will be automatically assigned (if auto_assign_org above is set to true)
 # Default role new users will be automatically assigned (if auto_assign_org above is set to true)
 auto_assign_org_role = Viewer
 auto_assign_org_role = Viewer
 
 
@@ -267,6 +270,18 @@ api_url = https://api.github.com/user
 team_ids =
 team_ids =
 allowed_organizations =
 allowed_organizations =
 
 
+#################################### GitLab Auth #########################
+[auth.gitlab]
+enabled = false
+allow_sign_up = true
+client_id = some_id
+client_secret = some_secret
+scopes = api
+auth_url = https://gitlab.com/oauth/authorize
+token_url = https://gitlab.com/oauth/token
+api_url = https://gitlab.com/api/v4
+allowed_groups =
+
 #################################### Google Auth #########################
 #################################### Google Auth #########################
 [auth.google]
 [auth.google]
 enabled = false
 enabled = false
@@ -311,6 +326,10 @@ token_url =
 api_url =
 api_url =
 team_ids =
 team_ids =
 allowed_organizations =
 allowed_organizations =
+tls_skip_verify_insecure = false
+tls_client_cert =
+tls_client_key =
+tls_client_ca =
 
 
 #################################### Basic Auth ##########################
 #################################### Basic Auth ##########################
 [auth.basic]
 [auth.basic]

+ 5 - 0
conf/ldap.toml

@@ -15,6 +15,9 @@ start_tls = false
 ssl_skip_verify = false
 ssl_skip_verify = false
 # set to the path to your root CA certificate or leave unset to use system defaults
 # set to the path to your root CA certificate or leave unset to use system defaults
 # root_ca_cert = "/path/to/certificate.crt"
 # root_ca_cert = "/path/to/certificate.crt"
+# Authentication against LDAP servers requiring client certificates
+# client_cert = "/path/to/client.crt"
+# client_key = "/path/to/client.key"
 
 
 # Search user bind dn
 # Search user bind dn
 bind_dn = "cn=admin,dc=grafana,dc=org"
 bind_dn = "cn=admin,dc=grafana,dc=org"
@@ -72,6 +75,8 @@ email =  "email"
 [[servers.group_mappings]]
 [[servers.group_mappings]]
 group_dn = "cn=admins,dc=grafana,dc=org"
 group_dn = "cn=admins,dc=grafana,dc=org"
 org_role = "Admin"
 org_role = "Admin"
+# To make user an instance admin  (Grafana Admin) uncomment line below
+# grafana_admin = true
 # The Grafana organization database id, optional, if left out the default org (id 1) will be used
 # The Grafana organization database id, optional, if left out the default org (id 1) will be used
 # org_id = 1
 # org_id = 1
 
 

+ 4 - 0
conf/sample.ini

@@ -272,6 +272,10 @@ log_queries =
 ;api_url = https://foo.bar/user
 ;api_url = https://foo.bar/user
 ;team_ids =
 ;team_ids =
 ;allowed_organizations =
 ;allowed_organizations =
+;tls_skip_verify_insecure = false
+;tls_client_cert =
+;tls_client_key =
+;tls_client_ca =
 
 
 #################################### Grafana.com Auth ####################
 #################################### Grafana.com Auth ####################
 [auth.grafana_com]
 [auth.grafana_com]

+ 10 - 5
devenv/README.md

@@ -1,11 +1,16 @@
 This folder contains useful scripts and configuration for...
 This folder contains useful scripts and configuration for...
 
 
-* Configuring datasources in Grafana
-* Provision example dashboards in Grafana
-* Run preconfiured datasources as docker containers
-
-want to know more? run setup!
+* Configuring dev datasources in Grafana
+* Configuring dev & test scenarios dashboards.
 
 
 ```bash
 ```bash
 ./setup.sh
 ./setup.sh
 ```
 ```
+
+After restarting grafana server there should now be a number of datasources named `gdev-<type>` provisioned as well as a dashboard folder named `gdev dashboards`. This folder contains dashboard & panel features tests dashboards. 
+
+# Dev dashboards
+
+Please update these dashboards or make new ones as new panels & dashboards features are developed or new bugs are found. The dashboards are located in the `devenv/dev-dashboards` folder. 
+
+

+ 1 - 1
devenv/bulk-dashboards/bulk-dashboards.yaml

@@ -5,5 +5,5 @@ providers:
    folder: 'Bulk dashboards'
    folder: 'Bulk dashboards'
    type: file
    type: file
    options:
    options:
-     path: devenv/dashboards/bulk-testing
+     path: devenv/bulk-dashboards
 
 

+ 33 - 2
devenv/datasources.yaml

@@ -14,6 +14,9 @@ datasources:
     isDefault: true
     isDefault: true
     url: http://localhost:9090
     url: http://localhost:9090
 
 
+  - name: gdev-testdata
+    type: testdata
+
   - name: gdev-influxdb
   - name: gdev-influxdb
     type: influxdb
     type: influxdb
     access: proxy
     access: proxy
@@ -48,19 +51,46 @@ datasources:
     user: grafana
     user: grafana
     password: password
     password: password
 
 
+  - name: gdev-mysql-ds-tests
+    type: mysql
+    url: localhost:3306
+    database: grafana_ds_tests
+    user: grafana
+    password: password
+
   - name: gdev-mssql
   - name: gdev-mssql
     type: mssql
     type: mssql
     url: localhost:1433
     url: localhost:1433
     database: grafana
     database: grafana
     user: grafana
     user: grafana
-    password: "Password!"
+    secureJsonData:
+      password: Password!
+
+  - name: gdev-mssql-ds-tests
+    type: mssql
+    url: localhost:1433
+    database: grafanatest
+    user: grafana
+    secureJsonData:
+      password: Password!
 
 
   - name: gdev-postgres
   - name: gdev-postgres
     type: postgres
     type: postgres
     url: localhost:5432
     url: localhost:5432
     database: grafana
     database: grafana
     user: grafana
     user: grafana
-    password: password
+    secureJsonData:
+      password: password
+    jsonData:
+      sslmode: "disable"
+
+  - name: gdev-postgres-ds-tests
+    type: postgres
+    url: localhost:5432
+    database: grafanadstest
+    user: grafanatest
+    secureJsonData:
+      password: grafanatest
     jsonData:
     jsonData:
       sslmode: "disable"
       sslmode: "disable"
 
 
@@ -71,3 +101,4 @@ datasources:
       authType: credentials
       authType: credentials
       defaultRegion: eu-west-2
       defaultRegion: eu-west-2
 
 
+

+ 0 - 592
devenv/dev-dashboards/dashboard_with_rows.json

@@ -1,592 +0,0 @@
-{
-  "annotations": {
-    "list": [
-      {
-        "builtIn": 1,
-        "datasource": "-- Grafana --",
-        "enable": true,
-        "hide": true,
-        "iconColor": "rgba(0, 211, 255, 1)",
-        "name": "Annotations & Alerts",
-        "type": "dashboard"
-      }
-    ]
-  },
-  "editable": true,
-  "gnetId": null,
-  "graphTooltip": 0,
-  "id": 59,
-  "links": [],
-  "panels": [
-    {
-      "collapsed": false,
-      "gridPos": {
-        "h": 1,
-        "w": 24,
-        "x": 0,
-        "y": 0
-      },
-      "id": 9,
-      "panels": [],
-      "title": "Row title",
-      "type": "row"
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "Prometheus",
-      "fill": 1,
-      "gridPos": {
-        "h": 4,
-        "w": 12,
-        "x": 0,
-        "y": 1
-      },
-      "id": 12,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "nullPointMode": "null",
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "go_goroutines",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "Panel Title",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "Prometheus",
-      "fill": 1,
-      "gridPos": {
-        "h": 4,
-        "w": 12,
-        "x": 12,
-        "y": 1
-      },
-      "id": 5,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "nullPointMode": "null",
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "go_goroutines",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "Panel Title",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "collapsed": false,
-      "gridPos": {
-        "h": 1,
-        "w": 24,
-        "x": 0,
-        "y": 5
-      },
-      "id": 7,
-      "panels": [],
-      "title": "Row",
-      "type": "row"
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "Prometheus",
-      "fill": 1,
-      "gridPos": {
-        "h": 4,
-        "w": 12,
-        "x": 0,
-        "y": 6
-      },
-      "id": 2,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "nullPointMode": "null",
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "go_goroutines",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "Panel Title",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "Prometheus",
-      "fill": 1,
-      "gridPos": {
-        "h": 4,
-        "w": 12,
-        "x": 12,
-        "y": 6
-      },
-      "id": 13,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "nullPointMode": "null",
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "go_goroutines",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "Panel Title",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "collapsed": false,
-      "gridPos": {
-        "h": 1,
-        "w": 24,
-        "x": 0,
-        "y": 10
-      },
-      "id": 11,
-      "panels": [],
-      "title": "Row title",
-      "type": "row"
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "Prometheus",
-      "fill": 1,
-      "gridPos": {
-        "h": 4,
-        "w": 12,
-        "x": 0,
-        "y": 11
-      },
-      "id": 4,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "nullPointMode": "null",
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "go_goroutines",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "Panel Title",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "Prometheus",
-      "fill": 1,
-      "gridPos": {
-        "h": 4,
-        "w": 12,
-        "x": 12,
-        "y": 11
-      },
-      "id": 3,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "nullPointMode": "null",
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "go_goroutines",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "Panel Title",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    }
-  ],
-  "schemaVersion": 16,
-  "style": "dark",
-  "tags": [],
-  "templating": {
-    "list": []
-  },
-  "time": {
-    "from": "now-30m",
-    "to": "now"
-  },
-  "timepicker": {
-    "refresh_intervals": [
-      "5s",
-      "10s",
-      "30s",
-      "1m",
-      "5m",
-      "15m",
-      "30m",
-      "1h",
-      "2h",
-      "1d"
-    ],
-    "time_options": [
-      "5m",
-      "15m",
-      "1h",
-      "6h",
-      "12h",
-      "24h",
-      "2d",
-      "7d",
-      "30d"
-    ]
-  },
-  "timezone": "",
-  "title": "Dashboard with rows",
-  "uid": "1DdOzBNmk",
-  "version": 5
-}

+ 28 - 50
docker/blocks/mssql/dashboard.json → devenv/dev-dashboards/datasource_tests_mssql_fakedata.json

@@ -1,40 +1,4 @@
 {
 {
-  "__inputs": [
-    {
-      "name": "DS_MSSQL",
-      "label": "MSSQL",
-      "description": "",
-      "type": "datasource",
-      "pluginId": "mssql",
-      "pluginName": "MSSQL"
-    }
-  ],
-  "__requires": [
-    {
-      "type": "grafana",
-      "id": "grafana",
-      "name": "Grafana",
-      "version": "5.0.0"
-    },
-    {
-      "type": "panel",
-      "id": "graph",
-      "name": "Graph",
-      "version": "5.0.0"
-    },
-    {
-      "type": "datasource",
-      "id": "mssql",
-      "name": "MSSQL",
-      "version": "1.0.0"
-    },
-    {
-      "type": "panel",
-      "id": "table",
-      "name": "Table",
-      "version": "5.0.0"
-    }
-  ],
   "annotations": {
   "annotations": {
     "list": [
     "list": [
       {
       {
@@ -52,8 +16,7 @@
   "editable": true,
   "editable": true,
   "gnetId": null,
   "gnetId": null,
   "graphTooltip": 0,
   "graphTooltip": 0,
-  "id": null,
-  "iteration": 1520976748896,
+  "iteration": 1532618661457,
   "links": [],
   "links": [],
   "panels": [
   "panels": [
     {
     {
@@ -63,7 +26,7 @@
       "bars": false,
       "bars": false,
       "dashLength": 10,
       "dashLength": 10,
       "dashes": false,
       "dashes": false,
-      "datasource": "${DS_MSSQL}",
+      "datasource": "gdev-mssql",
       "fill": 2,
       "fill": 2,
       "gridPos": {
       "gridPos": {
         "h": 9,
         "h": 9,
@@ -149,14 +112,18 @@
           "min": null,
           "min": null,
           "show": true
           "show": true
         }
         }
-      ]
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
     },
     },
     {
     {
       "aliasColors": {},
       "aliasColors": {},
       "bars": false,
       "bars": false,
       "dashLength": 10,
       "dashLength": 10,
       "dashes": false,
       "dashes": false,
-      "datasource": "${DS_MSSQL}",
+      "datasource": "gdev-mssql",
       "fill": 2,
       "fill": 2,
       "gridPos": {
       "gridPos": {
         "h": 18,
         "h": 18,
@@ -234,14 +201,18 @@
           "min": null,
           "min": null,
           "show": true
           "show": true
         }
         }
-      ]
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
     },
     },
     {
     {
       "aliasColors": {},
       "aliasColors": {},
       "bars": false,
       "bars": false,
       "dashLength": 10,
       "dashLength": 10,
       "dashes": false,
       "dashes": false,
-      "datasource": "${DS_MSSQL}",
+      "datasource": "gdev-mssql",
       "fill": 2,
       "fill": 2,
       "gridPos": {
       "gridPos": {
         "h": 9,
         "h": 9,
@@ -313,11 +284,15 @@
           "min": null,
           "min": null,
           "show": true
           "show": true
         }
         }
-      ]
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
     },
     },
     {
     {
       "columns": [],
       "columns": [],
-      "datasource": "${DS_MSSQL}",
+      "datasource": "gdev-mssql",
       "fontSize": "100%",
       "fontSize": "100%",
       "gridPos": {
       "gridPos": {
         "h": 10,
         "h": 10,
@@ -371,13 +346,13 @@
   ],
   ],
   "schemaVersion": 16,
   "schemaVersion": 16,
   "style": "dark",
   "style": "dark",
-  "tags": [],
+  "tags": ["gdev", "mssql", "fake-data-gen"],
   "templating": {
   "templating": {
     "list": [
     "list": [
       {
       {
         "allValue": null,
         "allValue": null,
         "current": {},
         "current": {},
-        "datasource": "${DS_MSSQL}",
+        "datasource": "gdev-mssql",
         "hide": 0,
         "hide": 0,
         "includeAll": false,
         "includeAll": false,
         "label": "Datacenter",
         "label": "Datacenter",
@@ -387,6 +362,7 @@
         "query": "SELECT DISTINCT datacenter FROM grafana_metric",
         "query": "SELECT DISTINCT datacenter FROM grafana_metric",
         "refresh": 1,
         "refresh": 1,
         "regex": "",
         "regex": "",
+        "skipUrlSync": false,
         "sort": 1,
         "sort": 1,
         "tagValuesQuery": "",
         "tagValuesQuery": "",
         "tags": [],
         "tags": [],
@@ -397,7 +373,7 @@
       {
       {
         "allValue": null,
         "allValue": null,
         "current": {},
         "current": {},
-        "datasource": "${DS_MSSQL}",
+        "datasource": "gdev-mssql",
         "hide": 0,
         "hide": 0,
         "includeAll": true,
         "includeAll": true,
         "label": "Hostname",
         "label": "Hostname",
@@ -407,6 +383,7 @@
         "query": "SELECT DISTINCT hostname FROM grafana_metric WHERE datacenter='$datacenter'",
         "query": "SELECT DISTINCT hostname FROM grafana_metric WHERE datacenter='$datacenter'",
         "refresh": 1,
         "refresh": 1,
         "regex": "",
         "regex": "",
+        "skipUrlSync": false,
         "sort": 1,
         "sort": 1,
         "tagValuesQuery": "",
         "tagValuesQuery": "",
         "tags": [],
         "tags": [],
@@ -499,6 +476,7 @@
         ],
         ],
         "query": "1s,10s,30s,1m,5m,10m,30m,1h,6h,12h,1d,7d,14d,30d",
         "query": "1s,10s,30s,1m,5m,10m,30m,1h,6h,12h,1d,7d,14d,30d",
         "refresh": 2,
         "refresh": 2,
+        "skipUrlSync": false,
         "type": "interval"
         "type": "interval"
       }
       }
     ]
     ]
@@ -533,7 +511,7 @@
     ]
     ]
   },
   },
   "timezone": "",
   "timezone": "",
-  "title": "Grafana Fake Data Gen - MSSQL",
+  "title": "Datasource tests - MSSQL",
   "uid": "86Js1xRmk",
   "uid": "86Js1xRmk",
-  "version": 11
+  "version": 1
 }
 }

文件差异内容过多而无法显示
+ 606 - 116
devenv/dev-dashboards/datasource_tests_mssql_unittest.json


+ 20 - 47
docker/blocks/mysql/dashboard.json → devenv/dev-dashboards/datasource_tests_mysql_fakedata.json

@@ -1,40 +1,4 @@
 {
 {
-  "__inputs": [
-    {
-      "name": "DS_MYSQL",
-      "label": "MySQL",
-      "description": "",
-      "type": "datasource",
-      "pluginId": "mysql",
-      "pluginName": "MySQL"
-    }
-  ],
-  "__requires": [
-    {
-      "type": "grafana",
-      "id": "grafana",
-      "name": "Grafana",
-      "version": "5.0.0"
-    },
-    {
-      "type": "panel",
-      "id": "graph",
-      "name": "Graph",
-      "version": "5.0.0"
-    },
-    {
-      "type": "datasource",
-      "id": "mysql",
-      "name": "MySQL",
-      "version": "5.0.0"
-    },
-    {
-      "type": "panel",
-      "id": "table",
-      "name": "Table",
-      "version": "5.0.0"
-    }
-  ],
   "annotations": {
   "annotations": {
     "list": [
     "list": [
       {
       {
@@ -52,8 +16,7 @@
   "editable": true,
   "editable": true,
   "gnetId": null,
   "gnetId": null,
   "graphTooltip": 0,
   "graphTooltip": 0,
-  "id": null,
-  "iteration": 1523372133566,
+  "iteration": 1532620738041,
   "links": [],
   "links": [],
   "panels": [
   "panels": [
     {
     {
@@ -63,7 +26,7 @@
       "bars": false,
       "bars": false,
       "dashLength": 10,
       "dashLength": 10,
       "dashes": false,
       "dashes": false,
-      "datasource": "${DS_MYSQL}",
+      "datasource": "gdev-mysql",
       "fill": 2,
       "fill": 2,
       "gridPos": {
       "gridPos": {
         "h": 9,
         "h": 9,
@@ -161,7 +124,7 @@
       "bars": false,
       "bars": false,
       "dashLength": 10,
       "dashLength": 10,
       "dashes": false,
       "dashes": false,
-      "datasource": "${DS_MYSQL}",
+      "datasource": "gdev-mysql",
       "fill": 2,
       "fill": 2,
       "gridPos": {
       "gridPos": {
         "h": 18,
         "h": 18,
@@ -251,7 +214,7 @@
       "bars": false,
       "bars": false,
       "dashLength": 10,
       "dashLength": 10,
       "dashes": false,
       "dashes": false,
-      "datasource": "${DS_MYSQL}",
+      "datasource": "gdev-mysql",
       "fill": 2,
       "fill": 2,
       "gridPos": {
       "gridPos": {
         "h": 9,
         "h": 9,
@@ -332,7 +295,7 @@
     },
     },
     {
     {
       "columns": [],
       "columns": [],
-      "datasource": "${DS_MYSQL}",
+      "datasource": "gdev-mysql",
       "fontSize": "100%",
       "fontSize": "100%",
       "gridPos": {
       "gridPos": {
         "h": 9,
         "h": 9,
@@ -390,6 +353,7 @@
   "schemaVersion": 16,
   "schemaVersion": 16,
   "style": "dark",
   "style": "dark",
   "tags": [
   "tags": [
+    "gdev",
     "fake-data-gen",
     "fake-data-gen",
     "mysql"
     "mysql"
   ],
   ],
@@ -397,8 +361,11 @@
     "list": [
     "list": [
       {
       {
         "allValue": null,
         "allValue": null,
-        "current": {},
-        "datasource": "${DS_MYSQL}",
+        "current": {
+          "text": "America",
+          "value": "America"
+        },
+        "datasource": "gdev-mysql",
         "hide": 0,
         "hide": 0,
         "includeAll": false,
         "includeAll": false,
         "label": "Datacenter",
         "label": "Datacenter",
@@ -408,6 +375,7 @@
         "query": "SELECT DISTINCT datacenter FROM grafana_metric",
         "query": "SELECT DISTINCT datacenter FROM grafana_metric",
         "refresh": 1,
         "refresh": 1,
         "regex": "",
         "regex": "",
+        "skipUrlSync": false,
         "sort": 1,
         "sort": 1,
         "tagValuesQuery": "",
         "tagValuesQuery": "",
         "tags": [],
         "tags": [],
@@ -417,8 +385,11 @@
       },
       },
       {
       {
         "allValue": null,
         "allValue": null,
-        "current": {},
-        "datasource": "${DS_MYSQL}",
+        "current": {
+          "text": "All",
+          "value": "$__all"
+        },
+        "datasource": "gdev-mysql",
         "hide": 0,
         "hide": 0,
         "includeAll": true,
         "includeAll": true,
         "label": "Hostname",
         "label": "Hostname",
@@ -428,6 +399,7 @@
         "query": "SELECT DISTINCT hostname FROM grafana_metric WHERE datacenter='$datacenter'",
         "query": "SELECT DISTINCT hostname FROM grafana_metric WHERE datacenter='$datacenter'",
         "refresh": 1,
         "refresh": 1,
         "regex": "",
         "regex": "",
+        "skipUrlSync": false,
         "sort": 1,
         "sort": 1,
         "tagValuesQuery": "",
         "tagValuesQuery": "",
         "tags": [],
         "tags": [],
@@ -520,6 +492,7 @@
         ],
         ],
         "query": "1s,10s,30s,1m,5m,10m,30m,1h,6h,12h,1d,7d,14d,30d",
         "query": "1s,10s,30s,1m,5m,10m,30m,1h,6h,12h,1d,7d,14d,30d",
         "refresh": 2,
         "refresh": 2,
+        "skipUrlSync": false,
         "type": "interval"
         "type": "interval"
       }
       }
     ]
     ]
@@ -554,7 +527,7 @@
     ]
     ]
   },
   },
   "timezone": "",
   "timezone": "",
-  "title": "Grafana Fake Data Gen - MySQL",
+  "title": "Datasource tests - MySQL",
   "uid": "DGsCac3kz",
   "uid": "DGsCac3kz",
   "version": 8
   "version": 8
 }
 }

文件差异内容过多而无法显示
+ 288 - 178
devenv/dev-dashboards/datasource_tests_mysql_unittest.json


+ 36 - 51
docker/blocks/postgres/dashboard.json → devenv/dev-dashboards/datasource_tests_postgres_fakedata.json

@@ -1,40 +1,4 @@
 {
 {
-  "__inputs": [
-    {
-      "name": "DS_POSTGRESQL",
-      "label": "PostgreSQL",
-      "description": "",
-      "type": "datasource",
-      "pluginId": "postgres",
-      "pluginName": "PostgreSQL"
-    }
-  ],
-  "__requires": [
-    {
-      "type": "grafana",
-      "id": "grafana",
-      "name": "Grafana",
-      "version": "5.0.0"
-    },
-    {
-      "type": "panel",
-      "id": "graph",
-      "name": "Graph",
-      "version": ""
-    },
-    {
-      "type": "datasource",
-      "id": "postgres",
-      "name": "PostgreSQL",
-      "version": "1.0.0"
-    },
-    {
-      "type": "panel",
-      "id": "table",
-      "name": "Table",
-      "version": ""
-    }
-  ],
   "annotations": {
   "annotations": {
     "list": [
     "list": [
       {
       {
@@ -52,8 +16,7 @@
   "editable": true,
   "editable": true,
   "gnetId": null,
   "gnetId": null,
   "graphTooltip": 0,
   "graphTooltip": 0,
-  "id": null,
-  "iteration": 1518601837383,
+  "iteration": 1532620601931,
   "links": [],
   "links": [],
   "panels": [
   "panels": [
     {
     {
@@ -63,7 +26,7 @@
       "bars": false,
       "bars": false,
       "dashLength": 10,
       "dashLength": 10,
       "dashes": false,
       "dashes": false,
-      "datasource": "${DS_POSTGRESQL}",
+      "datasource": "gdev-postgres",
       "fill": 2,
       "fill": 2,
       "gridPos": {
       "gridPos": {
         "h": 9,
         "h": 9,
@@ -150,14 +113,18 @@
           "min": null,
           "min": null,
           "show": true
           "show": true
         }
         }
-      ]
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
     },
     },
     {
     {
       "aliasColors": {},
       "aliasColors": {},
       "bars": false,
       "bars": false,
       "dashLength": 10,
       "dashLength": 10,
       "dashes": false,
       "dashes": false,
-      "datasource": "${DS_POSTGRESQL}",
+      "datasource": "gdev-postgres",
       "fill": 2,
       "fill": 2,
       "gridPos": {
       "gridPos": {
         "h": 18,
         "h": 18,
@@ -236,14 +203,18 @@
           "min": null,
           "min": null,
           "show": true
           "show": true
         }
         }
-      ]
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
     },
     },
     {
     {
       "aliasColors": {},
       "aliasColors": {},
       "bars": false,
       "bars": false,
       "dashLength": 10,
       "dashLength": 10,
       "dashes": false,
       "dashes": false,
-      "datasource": "${DS_POSTGRESQL}",
+      "datasource": "gdev-postgres",
       "fill": 2,
       "fill": 2,
       "gridPos": {
       "gridPos": {
         "h": 9,
         "h": 9,
@@ -316,11 +287,15 @@
           "min": null,
           "min": null,
           "show": true
           "show": true
         }
         }
-      ]
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
     },
     },
     {
     {
       "columns": [],
       "columns": [],
-      "datasource": "${DS_POSTGRESQL}",
+      "datasource": "gdev-postgres",
       "fontSize": "100%",
       "fontSize": "100%",
       "gridPos": {
       "gridPos": {
         "h": 9,
         "h": 9,
@@ -377,6 +352,7 @@
   "schemaVersion": 16,
   "schemaVersion": 16,
   "style": "dark",
   "style": "dark",
   "tags": [
   "tags": [
+    "gdev",
     "fake-data-gen",
     "fake-data-gen",
     "postgres"
     "postgres"
   ],
   ],
@@ -384,8 +360,11 @@
     "list": [
     "list": [
       {
       {
         "allValue": null,
         "allValue": null,
-        "current": {},
-        "datasource": "${DS_POSTGRESQL}",
+        "current": {
+          "text": "America",
+          "value": "America"
+        },
+        "datasource": "gdev-postgres",
         "hide": 0,
         "hide": 0,
         "includeAll": false,
         "includeAll": false,
         "label": "Datacenter",
         "label": "Datacenter",
@@ -395,6 +374,7 @@
         "query": "SELECT DISTINCT datacenter FROM grafana_metric",
         "query": "SELECT DISTINCT datacenter FROM grafana_metric",
         "refresh": 1,
         "refresh": 1,
         "regex": "",
         "regex": "",
+        "skipUrlSync": false,
         "sort": 1,
         "sort": 1,
         "tagValuesQuery": "",
         "tagValuesQuery": "",
         "tags": [],
         "tags": [],
@@ -404,8 +384,11 @@
       },
       },
       {
       {
         "allValue": null,
         "allValue": null,
-        "current": {},
-        "datasource": "${DS_POSTGRESQL}",
+        "current": {
+          "text": "All",
+          "value": "$__all"
+        },
+        "datasource": "gdev-postgres",
         "hide": 0,
         "hide": 0,
         "includeAll": true,
         "includeAll": true,
         "label": "Hostname",
         "label": "Hostname",
@@ -415,6 +398,7 @@
         "query": "SELECT DISTINCT hostname FROM grafana_metric WHERE datacenter='$datacenter'",
         "query": "SELECT DISTINCT hostname FROM grafana_metric WHERE datacenter='$datacenter'",
         "refresh": 1,
         "refresh": 1,
         "regex": "",
         "regex": "",
+        "skipUrlSync": false,
         "sort": 1,
         "sort": 1,
         "tagValuesQuery": "",
         "tagValuesQuery": "",
         "tags": [],
         "tags": [],
@@ -507,6 +491,7 @@
         ],
         ],
         "query": "1s,10s,30s,1m,5m,10m,30m,1h,6h,12h,1d,7d,14d,30d",
         "query": "1s,10s,30s,1m,5m,10m,30m,1h,6h,12h,1d,7d,14d,30d",
         "refresh": 2,
         "refresh": 2,
+        "skipUrlSync": false,
         "type": "interval"
         "type": "interval"
       }
       }
     ]
     ]
@@ -541,7 +526,7 @@
     ]
     ]
   },
   },
   "timezone": "",
   "timezone": "",
-  "title": "Grafana Fake Data Gen - PostgreSQL",
+  "title": "Datasource tests - Postgres",
   "uid": "JYola5qzz",
   "uid": "JYola5qzz",
-  "version": 1
+  "version": 4
 }
 }

文件差异内容过多而无法显示
+ 416 - 162
devenv/dev-dashboards/datasource_tests_postgres_unittest.json


+ 1558 - 0
devenv/dev-dashboards/panel_tests_graph.json

@@ -0,0 +1,1558 @@
+{
+  "annotations": {
+    "list": [
+      {
+        "builtIn": 1,
+        "datasource": "-- Grafana --",
+        "enable": true,
+        "hide": true,
+        "iconColor": "rgba(0, 211, 255, 1)",
+        "name": "Annotations & Alerts",
+        "type": "dashboard"
+      }
+    ]
+  },
+  "editable": true,
+  "gnetId": null,
+  "graphTooltip": 0,
+  "links": [],
+  "panels": [
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "editable": true,
+      "error": false,
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 0,
+        "y": 0
+      },
+      "id": 1,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "connected",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "refId": "A",
+          "scenario": "random_walk",
+          "scenarioId": "no_data_points",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "No Data Points Warning",
+      "tooltip": {
+        "msResolution": false,
+        "shared": true,
+        "sort": 0,
+        "value_type": "cumulative"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "editable": true,
+      "error": false,
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 8,
+        "y": 0
+      },
+      "id": 2,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "connected",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "refId": "A",
+          "scenario": "random_walk",
+          "scenarioId": "datapoints_outside_range",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Datapoints Outside Range Warning",
+      "tooltip": {
+        "msResolution": false,
+        "shared": true,
+        "sort": 0,
+        "value_type": "cumulative"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "editable": true,
+      "error": false,
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 16,
+        "y": 0
+      },
+      "id": 3,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "connected",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "refId": "A",
+          "scenario": "random_walk",
+          "scenarioId": "random_walk",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Random walk series",
+      "tooltip": {
+        "msResolution": false,
+        "shared": true,
+        "sort": 0,
+        "value_type": "cumulative"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "editable": true,
+      "error": false,
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 16,
+        "x": 0,
+        "y": 7
+      },
+      "id": 4,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "connected",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "refId": "A",
+          "scenario": "random_walk",
+          "scenarioId": "random_walk",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": "2s",
+      "timeShift": null,
+      "title": "Millisecond res x-axis and tooltip",
+      "tooltip": {
+        "msResolution": false,
+        "shared": true,
+        "sort": 0,
+        "value_type": "cumulative"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "content": "Just verify that the tooltip time has millisecond resolution ",
+      "editable": true,
+      "error": false,
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 16,
+        "y": 7
+      },
+      "id": 6,
+      "links": [],
+      "mode": "markdown",
+      "title": "",
+      "type": "text"
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "editable": true,
+      "error": false,
+      "fill": 1,
+      "gridPos": {
+        "h": 9,
+        "w": 16,
+        "x": 0,
+        "y": 14
+      },
+      "id": 5,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "connected",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [
+        {
+          "alias": "B-series",
+          "yaxis": 2
+        }
+      ],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "B",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "2000,3000,4000,1000,3000,10000",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "2 yaxis and axis labels",
+      "tooltip": {
+        "msResolution": false,
+        "shared": true,
+        "sort": 0,
+        "value_type": "cumulative"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "percent",
+          "label": "Perecent",
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": "Pressure",
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "content": "Verify that axis labels look ok",
+      "editable": true,
+      "error": false,
+      "gridPos": {
+        "h": 9,
+        "w": 8,
+        "x": 16,
+        "y": 14
+      },
+      "id": 7,
+      "links": [],
+      "mode": "markdown",
+      "title": "",
+      "type": "text"
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "editable": true,
+      "error": false,
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 0,
+        "y": 23
+      },
+      "id": 8,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "connected",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "refId": "B",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,null,null,null,null,null,null,100,10,10,20,30,40,10",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "null value connected",
+      "tooltip": {
+        "msResolution": false,
+        "shared": true,
+        "sort": 0,
+        "value_type": "cumulative"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "editable": true,
+      "error": false,
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 8,
+        "y": 23
+      },
+      "id": 10,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null as zero",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "refId": "B",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,null,null,null,null,null,null,100,10,10,20,30,40,10",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "null value null as zero",
+      "tooltip": {
+        "msResolution": false,
+        "shared": true,
+        "sort": 0,
+        "value_type": "cumulative"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "content": "Should be a long line connecting the null region in the `connected`  mode, and in zero it should just be a line with zero value at the null points. ",
+      "editable": true,
+      "error": false,
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 16,
+        "y": 23
+      },
+      "id": 13,
+      "links": [],
+      "mode": "markdown",
+      "title": "",
+      "type": "text"
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "editable": true,
+      "error": false,
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 16,
+        "x": 0,
+        "y": 30
+      },
+      "id": 9,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [
+        {
+          "alias": "B-series",
+          "zindex": -3
+        }
+      ],
+      "spaceLength": 10,
+      "stack": true,
+      "steppedLine": false,
+      "targets": [
+        {
+          "hide": false,
+          "refId": "B",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,null,null,null,null,null,null,100,10,10,20,30,40,10",
+          "target": ""
+        },
+        {
+          "alias": "",
+          "hide": false,
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,10,20,30,40,40,40,100,10,20,20",
+          "target": ""
+        },
+        {
+          "alias": "",
+          "hide": false,
+          "refId": "C",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,10,20,30,40,40,40,100,10,20,20",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Stacking value ontop of nulls",
+      "tooltip": {
+        "msResolution": false,
+        "shared": true,
+        "sort": 0,
+        "value_type": "cumulative"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "content": "Stacking values on top of nulls, should treat the null values as zero. ",
+      "editable": true,
+      "error": false,
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 16,
+        "y": 30
+      },
+      "id": 14,
+      "links": [],
+      "mode": "markdown",
+      "title": "",
+      "type": "text"
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "editable": true,
+      "error": false,
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 16,
+        "x": 0,
+        "y": 37
+      },
+      "id": 12,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [
+        {
+          "alias": "B-series",
+          "zindex": -3
+        }
+      ],
+      "spaceLength": 10,
+      "stack": true,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "hide": false,
+          "refId": "B",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,40,null,null,null,null,null,null,100,10,10,20,30,40,10",
+          "target": ""
+        },
+        {
+          "alias": "",
+          "hide": false,
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,40,null,null,null,null,null,null,100,10,10,20,30,40,10",
+          "target": ""
+        },
+        {
+          "alias": "",
+          "hide": false,
+          "refId": "C",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,40,null,null,null,null,null,null,100,10,10,20,30,40,10",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Stacking all series null segment",
+      "tooltip": {
+        "msResolution": false,
+        "shared": true,
+        "sort": 0,
+        "value_type": "cumulative"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "content": "Stacking when all values are null should leave a gap in the graph",
+      "editable": true,
+      "error": false,
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 16,
+        "y": 37
+      },
+      "id": 15,
+      "links": [],
+      "mode": "markdown",
+      "title": "",
+      "type": "text"
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "decimals": 3,
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 24,
+        "x": 0,
+        "y": 44
+      },
+      "id": 20,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 1,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Legend Table Single Series Should Take Minimum Height",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "decimals": 3,
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 12,
+        "x": 0,
+        "y": 51
+      },
+      "id": 16,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 1,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "B",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "C",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "D",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Legend Table No Scroll Visible",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "decimals": 3,
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 12,
+        "x": 12,
+        "y": 51
+      },
+      "id": 17,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 1,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "B",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "C",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "D",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "E",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "F",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "G",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "H",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "I",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "J",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Legend Table Should Scroll",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "decimals": 3,
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 12,
+        "x": 0,
+        "y": 58
+      },
+      "id": 18,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 1,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "B",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "C",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "D",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Legend Table No Scroll Visible",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "decimals": 3,
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 12,
+        "x": 12,
+        "y": 58
+      },
+      "id": 19,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 1,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "B",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "C",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "D",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "E",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "F",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "G",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "H",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "I",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "J",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "K",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "L",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Legend Table No Scroll Visible",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    }
+  ],
+  "refresh": false,
+  "revision": 8,
+  "schemaVersion": 16,
+  "style": "dark",
+  "tags": [
+    "gdev",
+    "panel-tests"
+  ],
+  "templating": {
+    "list": []
+  },
+  "time": {
+    "from": "now-1h",
+    "to": "now"
+  },
+  "timepicker": {
+    "refresh_intervals": [
+      "5s",
+      "10s",
+      "30s",
+      "1m",
+      "5m",
+      "15m",
+      "30m",
+      "1h",
+      "2h",
+      "1d"
+    ],
+    "time_options": [
+      "5m",
+      "15m",
+      "1h",
+      "6h",
+      "12h",
+      "24h",
+      "2d",
+      "7d",
+      "30d"
+    ]
+  },
+  "timezone": "browser",
+  "title": "Panel Tests - Graph",
+  "uid": "5SdHCadmz",
+  "version": 3
+}

+ 574 - 0
devenv/dev-dashboards/panel_tests_singlestat.json

@@ -0,0 +1,574 @@
+{
+  "annotations": {
+    "list": [
+      {
+        "builtIn": 1,
+        "datasource": "-- Grafana --",
+        "enable": true,
+        "hide": true,
+        "iconColor": "rgba(0, 211, 255, 1)",
+        "name": "Annotations & Alerts",
+        "type": "dashboard"
+      }
+    ]
+  },
+  "editable": true,
+  "gnetId": null,
+  "graphTooltip": 0,
+  "links": [],
+  "panels": [
+    {
+      "cacheTimeout": null,
+      "colorBackground": false,
+      "colorValue": true,
+      "colors": [
+        "#299c46",
+        "rgba(237, 129, 40, 0.89)",
+        "#d44a3a"
+      ],
+      "datasource": "gdev-testdata",
+      "decimals": null,
+      "description": "",
+      "format": "ms",
+      "gauge": {
+        "maxValue": 100,
+        "minValue": 0,
+        "show": false,
+        "thresholdLabels": false,
+        "thresholdMarkers": true
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 0,
+        "y": 0
+      },
+      "id": 2,
+      "interval": null,
+      "links": [],
+      "mappingType": 1,
+      "mappingTypes": [
+        {
+          "name": "value to text",
+          "value": 1
+        },
+        {
+          "name": "range to text",
+          "value": 2
+        }
+      ],
+      "maxDataPoints": 100,
+      "nullPointMode": "connected",
+      "nullText": null,
+      "postfix": "postfix",
+      "postfixFontSize": "50%",
+      "prefix": "prefix",
+      "prefixFontSize": "50%",
+      "rangeMaps": [
+        {
+          "from": "null",
+          "text": "N/A",
+          "to": "null"
+        }
+      ],
+      "sparkline": {
+        "fillColor": "rgba(31, 118, 189, 0.18)",
+        "full": false,
+        "lineColor": "rgb(31, 120, 193)",
+        "show": true
+      },
+      "tableColumn": "",
+      "targets": [
+        {
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,2,3,4,5"
+        }
+      ],
+      "thresholds": "5,10",
+      "title": "prefix 3 ms (green) postfixt + sparkline",
+      "type": "singlestat",
+      "valueFontSize": "80%",
+      "valueMaps": [
+        {
+          "op": "=",
+          "text": "N/A",
+          "value": "null"
+        }
+      ],
+      "valueName": "avg"
+    },
+    {
+      "cacheTimeout": null,
+      "colorBackground": false,
+      "colorPrefix": false,
+      "colorValue": true,
+      "colors": [
+        "#d44a3a",
+        "rgba(237, 129, 40, 0.89)",
+        "#299c46"
+      ],
+      "datasource": "gdev-testdata",
+      "decimals": null,
+      "description": "",
+      "format": "ms",
+      "gauge": {
+        "maxValue": 100,
+        "minValue": 0,
+        "show": false,
+        "thresholdLabels": false,
+        "thresholdMarkers": true
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 8,
+        "y": 0
+      },
+      "id": 3,
+      "interval": null,
+      "links": [],
+      "mappingType": 1,
+      "mappingTypes": [
+        {
+          "name": "value to text",
+          "value": 1
+        },
+        {
+          "name": "range to text",
+          "value": 2
+        }
+      ],
+      "maxDataPoints": 100,
+      "nullPointMode": "connected",
+      "nullText": null,
+      "postfix": "",
+      "postfixFontSize": "50%",
+      "prefix": "",
+      "prefixFontSize": "50%",
+      "rangeMaps": [
+        {
+          "from": "null",
+          "text": "N/A",
+          "to": "null"
+        }
+      ],
+      "sparkline": {
+        "fillColor": "rgba(31, 118, 189, 0.18)",
+        "full": true,
+        "lineColor": "rgb(31, 120, 193)",
+        "show": true
+      },
+      "tableColumn": "",
+      "targets": [
+        {
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,2,3,4,5"
+        }
+      ],
+      "thresholds": "5,10",
+      "title": "3 ms (red)  + full height sparkline",
+      "type": "singlestat",
+      "valueFontSize": "200%",
+      "valueMaps": [
+        {
+          "op": "=",
+          "text": "N/A",
+          "value": "null"
+        }
+      ],
+      "valueName": "avg"
+    },
+    {
+      "cacheTimeout": null,
+      "colorBackground": true,
+      "colorPrefix": false,
+      "colorValue": false,
+      "colors": [
+        "#d44a3a",
+        "rgba(237, 129, 40, 0.89)",
+        "#299c46"
+      ],
+      "datasource": "gdev-testdata",
+      "decimals": null,
+      "description": "",
+      "format": "ms",
+      "gauge": {
+        "maxValue": 100,
+        "minValue": 0,
+        "show": false,
+        "thresholdLabels": false,
+        "thresholdMarkers": true
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 16,
+        "y": 0
+      },
+      "id": 4,
+      "interval": null,
+      "links": [],
+      "mappingType": 1,
+      "mappingTypes": [
+        {
+          "name": "value to text",
+          "value": 1
+        },
+        {
+          "name": "range to text",
+          "value": 2
+        }
+      ],
+      "maxDataPoints": 100,
+      "nullPointMode": "connected",
+      "nullText": null,
+      "postfix": "",
+      "postfixFontSize": "50%",
+      "prefix": "",
+      "prefixFontSize": "50%",
+      "rangeMaps": [
+        {
+          "from": "null",
+          "text": "N/A",
+          "to": "null"
+        }
+      ],
+      "sparkline": {
+        "fillColor": "rgba(31, 118, 189, 0.18)",
+        "full": true,
+        "lineColor": "rgb(31, 120, 193)",
+        "show": false
+      },
+      "tableColumn": "",
+      "targets": [
+        {
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,2,3,4,5"
+        }
+      ],
+      "thresholds": "5,10",
+      "title": "3 ms + red background",
+      "type": "singlestat",
+      "valueFontSize": "200%",
+      "valueMaps": [
+        {
+          "op": "=",
+          "text": "N/A",
+          "value": "null"
+        }
+      ],
+      "valueName": "avg"
+    },
+    {
+      "cacheTimeout": null,
+      "colorBackground": false,
+      "colorPrefix": false,
+      "colorValue": true,
+      "colors": [
+        "#299c46",
+        "rgba(237, 129, 40, 0.89)",
+        "#d44a3a"
+      ],
+      "datasource": "gdev-testdata",
+      "decimals": null,
+      "description": "",
+      "format": "ms",
+      "gauge": {
+        "maxValue": 150,
+        "minValue": 0,
+        "show": true,
+        "thresholdLabels": true,
+        "thresholdMarkers": true
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 0,
+        "y": 7
+      },
+      "id": 5,
+      "interval": null,
+      "links": [],
+      "mappingType": 1,
+      "mappingTypes": [
+        {
+          "name": "value to text",
+          "value": 1
+        },
+        {
+          "name": "range to text",
+          "value": 2
+        }
+      ],
+      "maxDataPoints": 100,
+      "nullPointMode": "connected",
+      "nullText": null,
+      "postfix": "",
+      "postfixFontSize": "50%",
+      "prefix": "",
+      "prefixFontSize": "50%",
+      "rangeMaps": [
+        {
+          "from": "null",
+          "text": "N/A",
+          "to": "null"
+        }
+      ],
+      "sparkline": {
+        "fillColor": "rgba(31, 118, 189, 0.18)",
+        "full": true,
+        "lineColor": "rgb(31, 120, 193)",
+        "show": false
+      },
+      "tableColumn": "",
+      "targets": [
+        {
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "10,20,80"
+        }
+      ],
+      "thresholds": "81,90",
+      "title": "80 ms green gauge, thresholds 81, 90",
+      "type": "singlestat",
+      "valueFontSize": "80%",
+      "valueMaps": [
+        {
+          "op": "=",
+          "text": "N/A",
+          "value": "null"
+        }
+      ],
+      "valueName": "current"
+    },
+    {
+      "cacheTimeout": null,
+      "colorBackground": false,
+      "colorPrefix": false,
+      "colorValue": true,
+      "colors": [
+        "#299c46",
+        "rgba(237, 129, 40, 0.89)",
+        "#d44a3a"
+      ],
+      "datasource": "gdev-testdata",
+      "decimals": null,
+      "description": "",
+      "format": "ms",
+      "gauge": {
+        "maxValue": 150,
+        "minValue": 0,
+        "show": true,
+        "thresholdLabels": false,
+        "thresholdMarkers": true
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 8,
+        "y": 7
+      },
+      "id": 6,
+      "interval": null,
+      "links": [],
+      "mappingType": 1,
+      "mappingTypes": [
+        {
+          "name": "value to text",
+          "value": 1
+        },
+        {
+          "name": "range to text",
+          "value": 2
+        }
+      ],
+      "maxDataPoints": 100,
+      "nullPointMode": "connected",
+      "nullText": null,
+      "postfix": "",
+      "postfixFontSize": "50%",
+      "prefix": "",
+      "prefixFontSize": "50%",
+      "rangeMaps": [
+        {
+          "from": "null",
+          "text": "N/A",
+          "to": "null"
+        }
+      ],
+      "sparkline": {
+        "fillColor": "rgba(31, 118, 189, 0.18)",
+        "full": true,
+        "lineColor": "rgb(31, 120, 193)",
+        "show": false
+      },
+      "tableColumn": "",
+      "targets": [
+        {
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "10,20,80"
+        }
+      ],
+      "thresholds": "81,90",
+      "title": "80 ms green gauge, thresholds 81, 90, no labels",
+      "type": "singlestat",
+      "valueFontSize": "80%",
+      "valueMaps": [
+        {
+          "op": "=",
+          "text": "N/A",
+          "value": "null"
+        }
+      ],
+      "valueName": "current"
+    },
+    {
+      "cacheTimeout": null,
+      "colorBackground": false,
+      "colorPrefix": false,
+      "colorValue": true,
+      "colors": [
+        "#299c46",
+        "rgba(237, 129, 40, 0.89)",
+        "#d44a3a"
+      ],
+      "datasource": "gdev-testdata",
+      "decimals": null,
+      "description": "",
+      "format": "ms",
+      "gauge": {
+        "maxValue": 150,
+        "minValue": 0,
+        "show": true,
+        "thresholdLabels": false,
+        "thresholdMarkers": false
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 16,
+        "y": 7
+      },
+      "id": 7,
+      "interval": null,
+      "links": [],
+      "mappingType": 1,
+      "mappingTypes": [
+        {
+          "name": "value to text",
+          "value": 1
+        },
+        {
+          "name": "range to text",
+          "value": 2
+        }
+      ],
+      "maxDataPoints": 100,
+      "nullPointMode": "connected",
+      "nullText": null,
+      "postfix": "",
+      "postfixFontSize": "50%",
+      "prefix": "",
+      "prefixFontSize": "50%",
+      "rangeMaps": [
+        {
+          "from": "null",
+          "text": "N/A",
+          "to": "null"
+        }
+      ],
+      "sparkline": {
+        "fillColor": "rgba(31, 118, 189, 0.18)",
+        "full": true,
+        "lineColor": "rgb(31, 120, 193)",
+        "show": false
+      },
+      "tableColumn": "",
+      "targets": [
+        {
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "10,20,80"
+        }
+      ],
+      "thresholds": "81,90",
+      "title": "80 ms green gauge, thresholds 81, 90, no markers or labels",
+      "type": "singlestat",
+      "valueFontSize": "80%",
+      "valueMaps": [
+        {
+          "op": "=",
+          "text": "N/A",
+          "value": "null"
+        }
+      ],
+      "valueName": "current"
+    }
+  ],
+  "refresh": false,
+  "revision": 8,
+  "schemaVersion": 16,
+  "style": "dark",
+  "tags": [
+    "gdev",
+    "panel-tests"
+  ],
+  "templating": {
+    "list": []
+  },
+  "time": {
+    "from": "now-1h",
+    "to": "now"
+  },
+  "timepicker": {
+    "refresh_intervals": [
+      "5s",
+      "10s",
+      "30s",
+      "1m",
+      "5m",
+      "15m",
+      "30m",
+      "1h",
+      "2h",
+      "1d"
+    ],
+    "time_options": [
+      "5m",
+      "15m",
+      "1h",
+      "6h",
+      "12h",
+      "24h",
+      "2d",
+      "7d",
+      "30d"
+    ]
+  },
+  "timezone": "browser",
+  "title": "Panel Tests - Singlestat",
+  "uid": "singlestat",
+  "version": 14
+}

+ 453 - 0
devenv/dev-dashboards/panel_tests_table.json

@@ -0,0 +1,453 @@
+{
+  "annotations": {
+    "list": [
+      {
+        "builtIn": 1,
+        "datasource": "-- Grafana --",
+        "enable": true,
+        "hide": true,
+        "iconColor": "rgba(0, 211, 255, 1)",
+        "name": "Annotations & Alerts",
+        "type": "dashboard"
+      }
+    ]
+  },
+  "editable": true,
+  "gnetId": null,
+  "graphTooltip": 0,
+  "links": [],
+  "panels": [
+    {
+      "columns": [],
+      "datasource": "gdev-testdata",
+      "fontSize": "100%",
+      "gridPos": {
+        "h": 11,
+        "w": 12,
+        "x": 0,
+        "y": 0
+      },
+      "id": 3,
+      "links": [],
+      "pageSize": 10,
+      "scroll": true,
+      "showHeader": true,
+      "sort": {
+        "col": 0,
+        "desc": true
+      },
+      "styles": [
+        {
+          "alias": "Time",
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "pattern": "Time",
+          "type": "date"
+        },
+        {
+          "alias": "",
+          "colorMode": "cell",
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "decimals": 2,
+          "mappingType": 1,
+          "pattern": "ColorCell",
+          "thresholds": [
+            "5",
+            "10"
+          ],
+          "type": "number",
+          "unit": "currencyUSD"
+        },
+        {
+          "alias": "",
+          "colorMode": "value",
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "decimals": 2,
+          "mappingType": 1,
+          "pattern": "ColorValue",
+          "thresholds": [
+            "5",
+            "10"
+          ],
+          "type": "number",
+          "unit": "Bps"
+        },
+        {
+          "alias": "",
+          "colorMode": null,
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "decimals": 2,
+          "pattern": "/.*/",
+          "thresholds": [],
+          "type": "number",
+          "unit": "short"
+        }
+      ],
+      "targets": [
+        {
+          "alias": "server1",
+          "expr": "",
+          "format": "table",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0,20,10"
+        },
+        {
+          "alias": "server2",
+          "refId": "B",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0"
+        }
+      ],
+      "title": "Time series to rows (2 pages)",
+      "transform": "timeseries_to_rows",
+      "type": "table"
+    },
+    {
+      "columns": [
+        {
+          "text": "Avg",
+          "value": "avg"
+        },
+        {
+          "text": "Max",
+          "value": "max"
+        },
+        {
+          "text": "Current",
+          "value": "current"
+        }
+      ],
+      "datasource": "gdev-testdata",
+      "fontSize": "100%",
+      "gridPos": {
+        "h": 11,
+        "w": 12,
+        "x": 12,
+        "y": 0
+      },
+      "id": 4,
+      "links": [],
+      "pageSize": 10,
+      "scroll": true,
+      "showHeader": true,
+      "sort": {
+        "col": 0,
+        "desc": true
+      },
+      "styles": [
+        {
+          "alias": "Time",
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "pattern": "Time",
+          "type": "date"
+        },
+        {
+          "alias": "",
+          "colorMode": "cell",
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "decimals": 2,
+          "mappingType": 1,
+          "pattern": "ColorCell",
+          "thresholds": [
+            "5",
+            "10"
+          ],
+          "type": "number",
+          "unit": "currencyUSD"
+        },
+        {
+          "alias": "",
+          "colorMode": "value",
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "decimals": 2,
+          "mappingType": 1,
+          "pattern": "ColorValue",
+          "thresholds": [
+            "5",
+            "10"
+          ],
+          "type": "number",
+          "unit": "Bps"
+        },
+        {
+          "alias": "",
+          "colorMode": null,
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "decimals": 2,
+          "pattern": "/.*/",
+          "thresholds": [],
+          "type": "number",
+          "unit": "short"
+        }
+      ],
+      "targets": [
+        {
+          "alias": "server1",
+          "expr": "",
+          "format": "table",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0,20,10"
+        },
+        {
+          "alias": "server2",
+          "refId": "B",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0"
+        }
+      ],
+      "title": "Time series aggregations",
+      "transform": "timeseries_aggregations",
+      "type": "table"
+    },
+    {
+      "columns": [],
+      "datasource": "gdev-testdata",
+      "fontSize": "100%",
+      "gridPos": {
+        "h": 7,
+        "w": 24,
+        "x": 0,
+        "y": 11
+      },
+      "id": 5,
+      "links": [],
+      "pageSize": null,
+      "scroll": true,
+      "showHeader": true,
+      "sort": {
+        "col": 0,
+        "desc": true
+      },
+      "styles": [
+        {
+          "alias": "Time",
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "pattern": "Time",
+          "type": "date"
+        },
+        {
+          "alias": "",
+          "colorMode": "row",
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "decimals": 2,
+          "mappingType": 1,
+          "pattern": "/Color/",
+          "thresholds": [
+            "5",
+            "10"
+          ],
+          "type": "number",
+          "unit": "currencyUSD"
+        },
+        {
+          "alias": "",
+          "colorMode": null,
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "decimals": 2,
+          "pattern": "/.*/",
+          "thresholds": [],
+          "type": "number",
+          "unit": "short"
+        }
+      ],
+      "targets": [
+        {
+          "alias": "ColorValue",
+          "expr": "",
+          "format": "table",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0,20,10"
+        }
+      ],
+      "title": "color row by threshold",
+      "transform": "timeseries_to_columns",
+      "type": "table"
+    },
+    {
+      "columns": [],
+      "datasource": "gdev-testdata",
+      "fontSize": "100%",
+      "gridPos": {
+        "h": 8,
+        "w": 24,
+        "x": 0,
+        "y": 18
+      },
+      "id": 2,
+      "links": [],
+      "pageSize": null,
+      "scroll": true,
+      "showHeader": true,
+      "sort": {
+        "col": 0,
+        "desc": true
+      },
+      "styles": [
+        {
+          "alias": "Time",
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "pattern": "Time",
+          "type": "date"
+        },
+        {
+          "alias": "",
+          "colorMode": "cell",
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "decimals": 2,
+          "mappingType": 1,
+          "pattern": "ColorCell",
+          "thresholds": [
+            "5",
+            "10"
+          ],
+          "type": "number",
+          "unit": "currencyUSD"
+        },
+        {
+          "alias": "",
+          "colorMode": "value",
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "decimals": 2,
+          "mappingType": 1,
+          "pattern": "ColorValue",
+          "thresholds": [
+            "5",
+            "10"
+          ],
+          "type": "number",
+          "unit": "Bps"
+        },
+        {
+          "alias": "",
+          "colorMode": null,
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "decimals": 2,
+          "pattern": "/.*/",
+          "thresholds": [],
+          "type": "number",
+          "unit": "short"
+        }
+      ],
+      "targets": [
+        {
+          "alias": "ColorValue",
+          "expr": "",
+          "format": "table",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0,20,10"
+        },
+        {
+          "alias": "ColorCell",
+          "refId": "B",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "5,1,2,3,4,5,10,20"
+        }
+      ],
+      "title": "Column style thresholds & units",
+      "transform": "timeseries_to_columns",
+      "type": "table"
+    }
+  ],
+  "refresh": false,
+  "revision": 8,
+  "schemaVersion": 16,
+  "style": "dark",
+  "tags": [
+    "gdev",
+    "panel-tests"
+  ],
+  "templating": {
+    "list": []
+  },
+  "time": {
+    "from": "now-1h",
+    "to": "now"
+  },
+  "timepicker": {
+    "refresh_intervals": [
+      "5s",
+      "10s",
+      "30s",
+      "1m",
+      "5m",
+      "15m",
+      "30m",
+      "1h",
+      "2h",
+      "1d"
+    ],
+    "time_options": [
+      "5m",
+      "15m",
+      "1h",
+      "6h",
+      "12h",
+      "24h",
+      "2d",
+      "7d",
+      "30d"
+    ]
+  },
+  "timezone": "browser",
+  "title": "Panel Tests - Table",
+  "uid": "pttable",
+  "version": 1
+}

+ 3 - 3
public/app/plugins/app/testdata/dashboards/alerts.json → devenv/dev-dashboards/testdata_alerts.json

@@ -1,6 +1,6 @@
 {
 {
   "revision": 2,
   "revision": 2,
-  "title": "TestData - Alerts",
+  "title": "Alerting with TestData",
   "tags": [
   "tags": [
     "grafana-test"
     "grafana-test"
   ],
   ],
@@ -48,7 +48,7 @@
           },
           },
           "aliasColors": {},
           "aliasColors": {},
           "bars": false,
           "bars": false,
-          "datasource": "Grafana TestData",
+          "datasource": "gdev-testdata",
           "editable": true,
           "editable": true,
           "error": false,
           "error": false,
           "fill": 1,
           "fill": 1,
@@ -161,7 +161,7 @@
           },
           },
           "aliasColors": {},
           "aliasColors": {},
           "bars": false,
           "bars": false,
-          "datasource": "Grafana TestData",
+          "datasource": "gdev-testdata",
           "editable": true,
           "editable": true,
           "error": false,
           "error": false,
           "fill": 1,
           "fill": 1,

+ 15 - 9
devenv/setup.sh

@@ -1,4 +1,4 @@
-#/bin/bash
+#!/bin/bash
 
 
 bulkDashboard() {
 bulkDashboard() {
 
 
@@ -7,11 +7,11 @@ bulkDashboard() {
 		COUNTER=0
 		COUNTER=0
 		MAX=400
 		MAX=400
 		while [  $COUNTER -lt $MAX ]; do
 		while [  $COUNTER -lt $MAX ]; do
-				jsonnet -o "dashboards/bulk-testing/dashboard${COUNTER}.json" -e "local bulkDash = import 'dashboards/bulk-testing/bulkdash.jsonnet'; bulkDash + {  uid: 'uid-${COUNTER}',  title: 'title-${COUNTER}' }"
+				jsonnet -o "bulk-dashboards/dashboard${COUNTER}.json" -e "local bulkDash = import 'bulk-dashboards/bulkdash.jsonnet'; bulkDash + {  uid: 'uid-${COUNTER}',  title: 'title-${COUNTER}' }"
 				let COUNTER=COUNTER+1
 				let COUNTER=COUNTER+1
 		done
 		done
 
 
-		ln -s -f -r ./dashboards/bulk-testing/bulk-dashboards.yaml ../conf/provisioning/dashboards/custom.yaml
+		ln -s -f -r ./bulk-dashboards/bulk-dashboards.yaml ../conf/provisioning/dashboards/custom.yaml
 }
 }
 
 
 requiresJsonnet() {
 requiresJsonnet() {
@@ -22,31 +22,37 @@ requiresJsonnet() {
 		fi
 		fi
 }
 }
 
 
-defaultDashboards() {
+devDashboards() {
+		echo -e "\xE2\x9C\x94 Setting up all dev dashboards using provisioning"
 		ln -s -f ../../../devenv/dashboards.yaml ../conf/provisioning/dashboards/dev.yaml
 		ln -s -f ../../../devenv/dashboards.yaml ../conf/provisioning/dashboards/dev.yaml
 }
 }
 
 
-defaultDatasources() {
-		echo "setting up all default datasources using provisioning"
+devDatasources() {
+		echo -e "\xE2\x9C\x94 Setting up all dev datasources using provisioning"
 
 
 		ln -s -f ../../../devenv/datasources.yaml ../conf/provisioning/datasources/dev.yaml
 		ln -s -f ../../../devenv/datasources.yaml ../conf/provisioning/datasources/dev.yaml
 }
 }
 
 
 usage() {
 usage() {
-	echo -e "install.sh\n\tThis script setups dev provision for datasources and dashboards"
+	echo -e "\n"
 	echo "Usage:"
 	echo "Usage:"
 	echo "  bulk-dashboards                     - create and provisioning 400 dashboards"
 	echo "  bulk-dashboards                     - create and provisioning 400 dashboards"
 	echo "  no args                             - provisiong core datasources and dev dashboards"
 	echo "  no args                             - provisiong core datasources and dev dashboards"
 }
 }
 
 
 main() {
 main() {
+	echo -e "------------------------------------------------------------------"
+	echo -e "This script setups provisioning for dev datasources and dashboards"
+	echo -e "------------------------------------------------------------------"
+	echo -e "\n"
+
 	local cmd=$1
 	local cmd=$1
 
 
 	if [[ $cmd == "bulk-dashboards" ]]; then
 	if [[ $cmd == "bulk-dashboards" ]]; then
 		bulkDashboard
 		bulkDashboard
 	else
 	else
-		defaultDashboards
-		defaultDatasources
+		devDashboards
+		devDatasources
 	fi
 	fi
 
 
   if [[ -z "$cmd" ]]; then
   if [[ -z "$cmd" ]]; then

+ 2 - 1
docker/blocks/nginx_proxy/Dockerfile

@@ -1,3 +1,4 @@
 FROM nginx:alpine
 FROM nginx:alpine
 
 
-COPY nginx.conf /etc/nginx/nginx.conf
+COPY nginx.conf /etc/nginx/nginx.conf
+COPY htpasswd /etc/nginx/htpasswd

+ 3 - 0
docker/blocks/nginx_proxy/htpasswd

@@ -0,0 +1,3 @@
+user1:$apr1$1odeeQb.$kwV8D/VAAGUDU7pnHuKoV0
+user2:$apr1$A2kf25r.$6S0kp3C7vIuixS5CL0XA9.
+admin:$apr1$IWn4DoRR$E2ol7fS/dkI18eU4bXnBO1

+ 20 - 1
docker/blocks/nginx_proxy/nginx.conf

@@ -13,7 +13,26 @@ http {
     listen 10080;
     listen 10080;
 
 
     location /grafana/ {
     location /grafana/ {
+      ################################################################
+      # Enable these settings to test with basic auth and an auth proxy header
+      # the htpasswd file contains an admin user with password admin and
+      # user1: grafana and user2: grafana
+      ################################################################
+
+      # auth_basic "Restricted Content";
+      # auth_basic_user_file /etc/nginx/htpasswd;
+
+      ################################################################
+      # To use the auth proxy header, set the following in custom.ini:
+      # [auth.proxy]
+      # enabled = true
+      # header_name = X-WEBAUTH-USER
+      # header_property = username
+      ################################################################
+
+      # proxy_set_header X-WEBAUTH-USER $remote_user;
+
       proxy_pass http://localhost:3000/;
       proxy_pass http://localhost:3000/;
     }
     }
   }
   }
-}
+}

+ 85 - 0
docker/blocks/openldap/ldap_dev.toml

@@ -0,0 +1,85 @@
+# To troubleshoot and get more log info enable ldap debug logging in grafana.ini
+# [log]
+# filters = ldap:debug
+
+[[servers]]
+# Ldap server host (specify multiple hosts space separated)
+host = "127.0.0.1"
+# Default port is 389 or 636 if use_ssl = true
+port = 389
+# Set to true if ldap server supports TLS
+use_ssl = false
+# Set to true if connect ldap server with STARTTLS pattern (create connection in insecure, then upgrade to secure connection with TLS)
+start_tls = false
+# set to true if you want to skip ssl cert validation
+ssl_skip_verify = false
+# set to the path to your root CA certificate or leave unset to use system defaults
+# root_ca_cert = "/path/to/certificate.crt"
+
+# Search user bind dn
+bind_dn = "cn=admin,dc=grafana,dc=org"
+# Search user bind password
+# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;"""
+bind_password = 'grafana'
+
+# User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"
+search_filter = "(cn=%s)"
+
+# An array of base dns to search through
+search_base_dns = ["dc=grafana,dc=org"]
+
+# In POSIX LDAP schemas, without memberOf attribute a secondary query must be made for groups.
+# This is done by enabling group_search_filter below. You must also set member_of= "cn"
+# in [servers.attributes] below.
+
+# Users with nested/recursive group membership and an LDAP server that supports LDAP_MATCHING_RULE_IN_CHAIN
+# can set group_search_filter, group_search_filter_user_attribute, group_search_base_dns and member_of
+# below in such a way that the user's recursive group membership is considered.
+#
+# Nested Groups + Active Directory (AD) Example:
+#
+#   AD groups store the Distinguished Names (DNs) of members, so your filter must
+#   recursively search your groups for the authenticating user's DN. For example:
+#
+#     group_search_filter = "(member:1.2.840.113556.1.4.1941:=%s)"
+#     group_search_filter_user_attribute = "distinguishedName"
+#     group_search_base_dns = ["ou=groups,dc=grafana,dc=org"]
+#
+#     [servers.attributes]
+#     ...
+#     member_of = "distinguishedName"
+
+## Group search filter, to retrieve the groups of which the user is a member (only set if memberOf attribute is not available)
+# group_search_filter = "(&(objectClass=posixGroup)(memberUid=%s))"
+## Group search filter user attribute defines what user attribute gets substituted for %s in group_search_filter.
+## Defaults to the value of username in [server.attributes]
+## Valid options are any of your values in [servers.attributes]
+## If you are using nested groups you probably want to set this and member_of in
+## [servers.attributes] to "distinguishedName"
+# group_search_filter_user_attribute = "distinguishedName"
+## An array of the base DNs to search through for groups. Typically uses ou=groups
+# group_search_base_dns = ["ou=groups,dc=grafana,dc=org"]
+
+# Specify names of the ldap attributes your ldap uses
+[servers.attributes]
+name = "givenName"
+surname = "sn"
+username = "cn"
+member_of = "memberOf"
+email =  "email"
+
+# Map ldap groups to grafana org roles
+[[servers.group_mappings]]
+group_dn = "cn=admins,ou=groups,dc=grafana,dc=org"
+org_role = "Admin"
+# The Grafana organization database id, optional, if left out the default org (id 1) will be used
+# org_id = 1
+
+[[servers.group_mappings]]
+group_dn = "cn=editors,ou=groups,dc=grafana,dc=org"
+org_role = "Editor"
+
+[[servers.group_mappings]]
+# If you want to match all (or no ldap groups) then you can use wildcard
+group_dn = "*"
+org_role = "Viewer"

+ 2 - 5
docker/blocks/openldap/notes.md

@@ -14,12 +14,12 @@ After adding ldif files to `prepopulate`:
 
 
 ## Enabling LDAP in Grafana
 ## Enabling LDAP in Grafana
 
 
-The default `ldap.toml` file in `conf` has host set to `127.0.0.1` and port to set to 389 so all you need to do is enable it in the .ini file to get Grafana to use this block:
+Copy the ldap_dev.toml file in this folder into your `conf` folder (it is gitignored already). To enable it in the .ini file to get Grafana to use this block:
 
 
 ```ini
 ```ini
 [auth.ldap]
 [auth.ldap]
 enabled = true
 enabled = true
-config_file = conf/ldap.toml
+config_file = conf/ldap_dev.toml
 ; allow_sign_up = true
 ; allow_sign_up = true
 ```
 ```
 
 
@@ -43,6 +43,3 @@ editors
 
 
 no groups
 no groups
   ldap-viewer
   ldap-viewer
-
-
-

+ 2 - 1
docs/sources/alerting/notifications.md

@@ -130,7 +130,7 @@ There are a couple of configuration options which need to be set up in Grafana U
 
 
 Once these two properties are set, you can send the alerts to Kafka for further processing or throttling.
 Once these two properties are set, you can send the alerts to Kafka for further processing or throttling.
 
 
-### All supported notifier
+### All supported notifiers
 
 
 Name | Type |Support images
 Name | Type |Support images
 -----|------------ | ------
 -----|------------ | ------
@@ -148,6 +148,7 @@ Pushover | `pushover` | no
 Telegram | `telegram` | no
 Telegram | `telegram` | no
 Line | `line` | no
 Line | `line` | no
 Prometheus Alertmanager | `prometheus-alertmanager` | no
 Prometheus Alertmanager | `prometheus-alertmanager` | no
+Microsoft Teams | `teams` | yes
 
 
 
 
 
 

+ 2 - 0
docs/sources/features/datasources/cloudwatch.md

@@ -115,6 +115,8 @@ and `dimension keys/values`.
 In place of `region` you can specify `default` to use the default region configured in the datasource for the query,
 In place of `region` you can specify `default` to use the default region configured in the datasource for the query,
 e.g. `metrics(AWS/DynamoDB, default)` or `dimension_values(default, ..., ..., ...)`.
 e.g. `metrics(AWS/DynamoDB, default)` or `dimension_values(default, ..., ..., ...)`.
 
 
+Read more about the available dimensions in the [CloudWatch  Metrics and Dimensions Reference](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CW_Support_For_AWS.html).
+
 Name | Description
 Name | Description
 ------- | --------
 ------- | --------
 *regions()* | Returns a list of regions AWS provides their service.
 *regions()* | Returns a list of regions AWS provides their service.

+ 3 - 3
docs/sources/features/datasources/elasticsearch.md

@@ -58,8 +58,8 @@ a time pattern for the index name or a wildcard.
 
 
 ### Elasticsearch version
 ### Elasticsearch version
 
 
-Be sure to specify your Elasticsearch version in the version selection dropdown. This is very important as there are differences how queries are composed. Currently only 2.x and 5.x
-are supported.
+Be sure to specify your Elasticsearch version in the version selection dropdown. This is very important as there are differences how queries are composed.
+Currently the versions available is 2.x, 5.x and 5.6+ where 5.6+ means a version of 5.6 or higher, 6.3.2 for example.
 
 
 ### Min time interval
 ### Min time interval
 A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example `1m` if your data is written every minute.
 A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example `1m` if your data is written every minute.
@@ -115,7 +115,7 @@ The Elasticsearch data source supports two types of queries you can use in the *
 
 
 Query | Description
 Query | Description
 ------------ | -------------
 ------------ | -------------
-*{"find": "fields", "type": "keyword"} | Returns a list of field names with the index type `keyword`.
+*{"find": "fields", "type": "keyword"}* | Returns a list of field names with the index type `keyword`.
 *{"find": "terms", "field": "@hostname", "size": 1000}* |  Returns a list of values for a field using term aggregation. Query will user current dashboard time range as time range for query.
 *{"find": "terms", "field": "@hostname", "size": 1000}* |  Returns a list of values for a field using term aggregation. Query will user current dashboard time range as time range for query.
 *{"find": "terms", "field": "@hostname", "query": '<lucene query>'}* | Returns a list of values for a field using term aggregation & and a specified lucene query filter. Query will use current dashboard time range as time range for query.
 *{"find": "terms", "field": "@hostname", "query": '<lucene query>'}* | Returns a list of values for a field using term aggregation & and a specified lucene query filter. Query will use current dashboard time range as time range for query.
 
 

+ 8 - 2
docs/sources/features/datasources/mssql.md

@@ -81,10 +81,15 @@ Macro example | Description
 *$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *'2017-04-21T05:01:17Z'*
 *$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *'2017-04-21T05:01:17Z'*
 *$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'*
 *$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'*
 *$__timeGroup(dateColumn,'5m'[, fillvalue])* | Will be replaced by an expression usable in GROUP BY clause. Providing a *fillValue* of *NULL* or *floating value* will automatically fill empty series in timerange with that value. <br/>For example, *CAST(ROUND(DATEDIFF(second, '1970-01-01', time_column)/300.0, 0) as bigint)\*300*.
 *$__timeGroup(dateColumn,'5m'[, fillvalue])* | Will be replaced by an expression usable in GROUP BY clause. Providing a *fillValue* of *NULL* or *floating value* will automatically fill empty series in timerange with that value. <br/>For example, *CAST(ROUND(DATEDIFF(second, '1970-01-01', time_column)/300.0, 0) as bigint)\*300*.
-*$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so all null values will be converted to the fill value (all null values would be set to zero using this example).
+*$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so missing points in that series will be added by grafana and 0 will be used as value.
+*$__timeGroup(dateColumn,'5m', NULL)* | Same as above but NULL will be used as value for missing points.
+*$__timeGroup(dateColumn,'5m', previous)* | Same as above but the previous value in that series will be used as fill value if no value has been seen yet NULL will be used (only available in Grafana 5.3+).
+*$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+).
 *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183*
 *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183*
 *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
 *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
 *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183*
 *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183*
+*$__unixEpochGroup(dateColumn,'5m', [fillmode])* | Same as $__timeGroup but for times stored as unix timestamp (only available in Grafana 5.3+).
+*$__unixEpochGroupAlias(dateColumn,'5m', [fillmode])* | Same as above but also adds a column alias (only available in Grafana 5.3+).
 
 
 We plan to add many more macros. If you have suggestions for what macros you would like to see, please [open an issue](https://github.com/grafana/grafana) in our GitHub repo.
 We plan to add many more macros. If you have suggestions for what macros you would like to see, please [open an issue](https://github.com/grafana/grafana) in our GitHub repo.
 
 
@@ -148,7 +153,8 @@ The resulting table panel:
 
 
 ## Time series queries
 ## Time series queries
 
 
-If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must must have a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch in seconds. You may return a column named `metric` that is used as metric name for the value column. Any column except `time` and `metric` is treated as a value column. If you omit the `metric` column, tha name of the value column will be the metric name. You may select multiple value columns, each will have its name as metric.
+If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must must have a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch in seconds. You may return a column named `metric` that is used as metric name for the value column. Any column except `time` and `metric` is treated as a value column. If you omit the `metric` column, the name of the value column will be the metric name. You may select multiple value columns, each will have its name as metric.
+If you return multiple value columns and a column named `metric` then this column is used as prefix for the series name (only available in Grafana 5.3+).
 
 
 **Example database table:**
 **Example database table:**
 
 

+ 7 - 1
docs/sources/features/datasources/mysql.md

@@ -64,10 +64,15 @@ Macro example | Description
 *$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *'2017-04-21T05:01:17Z'*
 *$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *'2017-04-21T05:01:17Z'*
 *$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'*
 *$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'*
 *$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *cast(cast(UNIX_TIMESTAMP(dateColumn)/(300) as signed)*300 as signed),*
 *$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *cast(cast(UNIX_TIMESTAMP(dateColumn)/(300) as signed)*300 as signed),*
-*$__timeGroup(dateColumn,'5m',0)* | Same as above but with a fill parameter so all null values will be converted to the fill value (all null values would be set to zero using this example).
+*$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so missing points in that series will be added by grafana and 0 will be used as value.
+*$__timeGroup(dateColumn,'5m', NULL)* | Same as above but NULL will be used as value for missing points.
+*$__timeGroup(dateColumn,'5m', previous)* | Same as above but the previous value in that series will be used as fill value if no value has been seen yet NULL will be used (only available in Grafana 5.3+).
+*$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+).
 *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183*
 *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183*
 *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
 *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
 *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183*
 *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183*
+*$__unixEpochGroup(dateColumn,'5m', [fillmode])* | Same as $__timeGroup but for times stored as unix timestamp (only available in Grafana 5.3+).
+*$__unixEpochGroupAlias(dateColumn,'5m', [fillmode])* | Same as above but also adds a column alias (only available in Grafana 5.3+).
 
 
 We plan to add many more macros. If you have suggestions for what macros you would like to see, please [open an issue](https://github.com/grafana/grafana) in our GitHub repo.
 We plan to add many more macros. If you have suggestions for what macros you would like to see, please [open an issue](https://github.com/grafana/grafana) in our GitHub repo.
 
 
@@ -104,6 +109,7 @@ The resulting table panel:
 If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must return a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch.
 If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must return a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch.
 Any column except `time` and `metric` is treated as a value column.
 Any column except `time` and `metric` is treated as a value column.
 You may return a column named `metric` that is used as metric name for the value column.
 You may return a column named `metric` that is used as metric name for the value column.
+If you return multiple value columns and a column named `metric` then this column is used as prefix for the series name (only available in Grafana 5.3+).
 
 
 **Example with `metric` column:**
 **Example with `metric` column:**
 
 

+ 10 - 2
docs/sources/features/datasources/postgres.md

@@ -31,6 +31,7 @@ Name | Description
 *User* | Database user's login/username
 *User* | Database user's login/username
 *Password* | Database user's password
 *Password* | Database user's password
 *SSL Mode* | This option determines whether or with what priority a secure SSL TCP/IP connection will be negotiated with the server.
 *SSL Mode* | This option determines whether or with what priority a secure SSL TCP/IP connection will be negotiated with the server.
+*TimescaleDB* | With this option enabled Grafana will use TimescaleDB features, e.g. use ```time_bucket``` for grouping by time (only available in Grafana 5.3+).
 
 
 ### Database User Permissions (Important!)
 ### Database User Permissions (Important!)
 
 
@@ -60,11 +61,16 @@ Macro example | Description
 *$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *dateColumn BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:06:17Z'*
 *$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *dateColumn BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:06:17Z'*
 *$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *'2017-04-21T05:01:17Z'*
 *$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *'2017-04-21T05:01:17Z'*
 *$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'*
 *$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'*
-*$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *(extract(epoch from dateColumn)/300)::bigint*300 AS time*
-*$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so all null values will be converted to the fill value (all null values would be set to zero using this example).
+*$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *(extract(epoch from dateColumn)/300)::bigint*300*
+*$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so missing points in that series will be added by grafana and 0 will be used as value.
+*$__timeGroup(dateColumn,'5m', NULL)* | Same as above but NULL will be used as value for missing points.
+*$__timeGroup(dateColumn,'5m', previous)* | Same as above but the previous value in that series will be used as fill value if no value has been seen yet NULL will be used (only available in Grafana 5.3+).
+*$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+).
 *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn >= 1494410783 AND dateColumn <= 1494497183*
 *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn >= 1494410783 AND dateColumn <= 1494497183*
 *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
 *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
 *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183*
 *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183*
+*$__unixEpochGroup(dateColumn,'5m', [fillmode])* | Same as $__timeGroup but for times stored as unix timestamp (only available in Grafana 5.3+).
+*$__unixEpochGroupAlias(dateColumn,'5m', [fillmode])* | Same as above but also adds a column alias (only available in Grafana 5.3+).
 
 
 We plan to add many more macros. If you have suggestions for what macros you would like to see, please [open an issue](https://github.com/grafana/grafana) in our GitHub repo.
 We plan to add many more macros. If you have suggestions for what macros you would like to see, please [open an issue](https://github.com/grafana/grafana) in our GitHub repo.
 
 
@@ -102,6 +108,7 @@ The resulting table panel:
 If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must return a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch.
 If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must return a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch.
 Any column except `time` and `metric` is treated as a value column.
 Any column except `time` and `metric` is treated as a value column.
 You may return a column named `metric` that is used as metric name for the value column.
 You may return a column named `metric` that is used as metric name for the value column.
+If you return multiple value columns and a column named `metric` then this column is used as prefix for the series name (only available in Grafana 5.3+).
 
 
 **Example with `metric` column:**
 **Example with `metric` column:**
 
 
@@ -285,4 +292,5 @@ datasources:
       password: "Password!"
       password: "Password!"
     jsonData:
     jsonData:
       sslmode: "disable" # disable/require/verify-ca/verify-full
       sslmode: "disable" # disable/require/verify-ca/verify-full
+      timescaledb: false
 ```
 ```

+ 26 - 0
docs/sources/features/datasources/prometheus.md

@@ -75,6 +75,32 @@ Name | Description
 
 
 For details of *metric names*, *label names* and *label values* are please refer to the [Prometheus documentation](http://prometheus.io/docs/concepts/data_model/#metric-names-and-labels).
 For details of *metric names*, *label names* and *label values* are please refer to the [Prometheus documentation](http://prometheus.io/docs/concepts/data_model/#metric-names-and-labels).
 
 
+
+#### Using interval and range variables
+
+> Support for `$__range`, `$__range_s` and `$__range_ms` only available from Grafana v5.3
+
+It's possible to use some global built-in variables in query variables; `$__interval`, `$__interval_ms`, `$__range`, `$__range_s` and `$__range_ms`, see [Global built-in variables](/reference/templating/#global-built-in-variables) for more information. These can be convenient to use in conjunction with the `query_result` function when you need to filter variable queries since
+`label_values` function doesn't support queries.
+
+Make sure to set the variable's `refresh` trigger to be `On Time Range Change` to get the correct instances when changing the time range on the dashboard.
+
+**Example usage:**
+
+Populate a variable with the the busiest 5 request instances based on average QPS over the time range shown in the dashboard:
+
+```
+Query: query_result(topk(5, sum(rate(http_requests_total[$__range])) by (instance)))
+Regex: /"([^"]+)"/
+```
+
+Populate a variable with the instances having a certain state over the time range shown in the dashboard, using the more precise `$__range_s`:
+
+```
+Query: query_result(max_over_time(<metric>[${__range_s}s]) != <state>)
+Regex:
+```
+
 ### Using variables in queries
 ### Using variables in queries
 
 
 There are two syntaxes:
 There are two syntaxes:

+ 2 - 2
docs/sources/guides/basic_concepts.md

@@ -54,7 +54,7 @@ We utilize a unit abstraction so that Grafana looks great on all screens both sm
 
 
  > Note: With MaxDataPoint functionality, Grafana can show you the perfect amount of datapoints no matter your resolution or time-range.
  > Note: With MaxDataPoint functionality, Grafana can show you the perfect amount of datapoints no matter your resolution or time-range.
 
 
-Utilize the [Repeating Row functionality](/reference/templating/#utilizing-template-variables-with-repeating-panels-and-repeating-rows) to dynamically create or remove entire Rows (that can be filled with Panels), based on the Template variables selected.
+Utilize the [Repeating Rows functionality](/reference/templating/#repeating-rows) to dynamically create or remove entire Rows (that can be filled with Panels), based on the Template variables selected.
 
 
 Rows can be collapsed by clicking on the Row Title. If you save a Dashboard with a Row collapsed, it will save in that state and will not preload those graphs until the row is expanded.
 Rows can be collapsed by clicking on the Row Title. If you save a Dashboard with a Row collapsed, it will save in that state and will not preload those graphs until the row is expanded.
 
 
@@ -72,7 +72,7 @@ Panels like the [Graph](/reference/graph/) panel allow you to graph as many metr
 
 
 Panels can be made more dynamic by utilizing [Dashboard Templating](/reference/templating/) variable strings within the panel configuration (including queries to your Data Source configured via the Query Editor).
 Panels can be made more dynamic by utilizing [Dashboard Templating](/reference/templating/) variable strings within the panel configuration (including queries to your Data Source configured via the Query Editor).
 
 
-Utilize the [Repeating Panel](/reference/templating/#utilizing-template-variables-with-repeating-panels-and-repeating-rows) functionality to dynamically create or remove Panels based on the [Templating Variables](/reference/templating/#utilizing-template-variables-with-repeating-panels-and-repeating-rows) selected.
+Utilize the [Repeating Panel](/reference/templating/#repeating-panels) functionality to dynamically create or remove Panels based on the [Templating Variables](/reference/templating/#repeating-panels) selected.
 
 
 The time range on Panels is normally what is set in the [Dashboard time picker](/reference/timerange/) but this can be overridden by utilizes [Panel specific time overrides](/reference/timerange/#panel-time-overrides-timeshift).
 The time range on Panels is normally what is set in the [Dashboard time picker](/reference/timerange/) but this can be overridden by utilizes [Panel specific time overrides](/reference/timerange/#panel-time-overrides-timeshift).
 
 

+ 0 - 1
docs/sources/http_api/alerting.md

@@ -59,7 +59,6 @@ Content-Type: application/json
     "panelId": 1,
     "panelId": 1,
     "name": "fire place sensor",
     "name": "fire place sensor",
     "state": "alerting",
     "state": "alerting",
-    "message": "Someone is trying to break in through the fire place",
     "newStateDate": "2018-05-14T05:55:20+02:00",
     "newStateDate": "2018-05-14T05:55:20+02:00",
     "evalDate": "0001-01-01T00:00:00Z",
     "evalDate": "0001-01-01T00:00:00Z",
     "evalData": null,
     "evalData": null,

+ 1 - 1
docs/sources/http_api/dashboard.md

@@ -85,7 +85,7 @@ Status Codes:
 - **403** – Access denied
 - **403** – Access denied
 - **412** – Precondition failed
 - **412** – Precondition failed
 
 
-The **412** status code is used for explaing that you cannot create the dashboard and why.
+The **412** status code is used for explaining that you cannot create the dashboard and why.
 There can be different reasons for this:
 There can be different reasons for this:
 
 
 - The dashboard has been changed by someone else, `status=version-mismatch`
 - The dashboard has been changed by someone else, `status=version-mismatch`

+ 1 - 1
docs/sources/http_api/folder.md

@@ -223,7 +223,7 @@ Status Codes:
 - **404** – Folder not found
 - **404** – Folder not found
 - **412** – Precondition failed
 - **412** – Precondition failed
 
 
-The **412** status code is used for explaing that you cannot update the folder and why.
+The **412** status code is used for explaining that you cannot update the folder and why.
 There can be different reasons for this:
 There can be different reasons for this:
 
 
 - The folder has been changed by someone else, `status=version-mismatch`
 - The folder has been changed by someone else, `status=version-mismatch`

+ 286 - 0
docs/sources/http_api/playlist.md

@@ -0,0 +1,286 @@
++++
+title = "Playlist HTTP API "
+description = "Playlist Admin HTTP API"
+keywords = ["grafana", "http", "documentation", "api", "playlist"]
+aliases = ["/http_api/playlist/"]
+type = "docs"
+[menu.docs]
+name = "Playlist"
+parent = "http_api"
++++
+
+# Playlist API
+
+## Search Playlist
+
+`GET /api/playlists`
+
+Get all existing playlist for the current organization using pagination
+
+**Example Request**:
+
+```bash
+GET /api/playlists HTTP/1.1
+Accept: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+```
+
+  Querystring Parameters:
+
+  These parameters are used as querystring parameters.
+  
+  - **query** - Limit response to playlist having a name like this value.
+  - **limit** - Limit response to *X* number of playlist.
+
+**Example Response**:
+
+```json
+HTTP/1.1 200
+Content-Type: application/json
+[
+  {
+    "id": 1,
+    "name": "my playlist",
+    "interval": "5m"
+  }
+]
+```
+
+## Get one playlist
+
+`GET /api/playlists/:id`
+
+**Example Request**:
+
+```bash
+GET /api/playlists/1 HTTP/1.1
+Accept: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+```
+
+**Example Response**:
+
+```json
+HTTP/1.1 200
+Content-Type: application/json
+{
+  "id" : 1,
+  "name": "my playlist",
+  "interval": "5m",
+  "orgId": "my org",
+  "items": [
+    {
+      "id": 1,
+      "playlistId": 1,
+      "type": "dashboard_by_id",
+      "value": "3",
+      "order": 1,
+      "title":"my third dasboard"
+    },
+    {
+      "id": 2,
+      "playlistId": 1,
+      "type": "dashboard_by_tag",
+      "value": "myTag",
+      "order": 2,
+      "title":"my other dasboard"
+    }
+  ]
+}
+```
+
+## Get Playlist items
+
+`GET /api/playlists/:id/items`
+
+**Example Request**:
+
+```bash
+GET /api/playlists/1/items HTTP/1.1
+Accept: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+```
+
+**Example Response**:
+
+```json
+HTTP/1.1 200
+Content-Type: application/json
+[
+  {
+    "id": 1,
+    "playlistId": 1,
+    "type": "dashboard_by_id",
+    "value": "3",
+    "order": 1,
+    "title":"my third dasboard"
+  },
+  {
+    "id": 2,
+    "playlistId": 1,
+    "type": "dashboard_by_tag",
+    "value": "myTag",
+    "order": 2,
+    "title":"my other dasboard"
+  }
+]
+```
+
+## Get Playlist dashboards
+
+`GET /api/playlists/:id/dashboards`
+
+**Example Request**:
+
+```bash
+GET /api/playlists/1/dashboards HTTP/1.1
+Accept: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+```
+
+**Example Response**:
+
+```json
+HTTP/1.1 200
+Content-Type: application/json
+[
+  {
+    "id": 3,
+    "title": "my third dasboard",
+    "order": 1,
+  },
+  {
+    "id": 5,
+    "title":"my other dasboard"
+    "order": 2,
+    
+  }
+]
+```
+
+## Create a playlist
+
+`POST /api/playlists/`
+
+**Example Request**:
+
+```bash
+PUT /api/playlists/1 HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+  {
+    "name": "my playlist",
+    "interval": "5m",
+    "items": [
+      {
+        "type": "dashboard_by_id",
+        "value": "3",
+        "order": 1,
+        "title":"my third dasboard"
+      },
+      {
+        "type": "dashboard_by_tag",
+        "value": "myTag",
+        "order": 2,
+        "title":"my other dasboard"
+      }
+    ]
+  }
+```
+
+**Example Response**:
+
+```json
+HTTP/1.1 200
+Content-Type: application/json
+  {
+    "id": 1,
+    "name": "my playlist",
+    "interval": "5m"
+  }
+```
+
+## Update a playlist
+
+`PUT /api/playlists/:id`
+
+**Example Request**:
+
+```bash
+PUT /api/playlists/1 HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+  {
+    "name": "my playlist",
+    "interval": "5m",
+    "items": [
+      {
+        "playlistId": 1,
+        "type": "dashboard_by_id",
+        "value": "3",
+        "order": 1,
+        "title":"my third dasboard"
+      },
+      {
+        "playlistId": 1,
+        "type": "dashboard_by_tag",
+        "value": "myTag",
+        "order": 2,
+        "title":"my other dasboard"
+      }
+    ]
+  }
+```
+
+**Example Response**:
+
+```json
+HTTP/1.1 200
+Content-Type: application/json
+{
+  "id" : 1,
+  "name": "my playlist",
+  "interval": "5m",
+  "orgId": "my org",
+  "items": [
+    {
+      "id": 1,
+      "playlistId": 1,
+      "type": "dashboard_by_id",
+      "value": "3",
+      "order": 1,
+      "title":"my third dasboard"
+    },
+    {
+      "id": 2,
+      "playlistId": 1,
+      "type": "dashboard_by_tag",
+      "value": "myTag",
+      "order": 2,
+      "title":"my other dasboard"
+    }
+  ]
+}
+```
+
+## Delete a playlist
+
+`DELETE /api/playlists/:id`
+
+**Example Request**:
+
+```bash
+DELETE /api/playlists/1 HTTP/1.1
+Accept: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+```
+
+**Example Response**:
+
+```json
+HTTP/1.1 200
+Content-Type: application/json
+{}
+```

+ 33 - 0
docs/sources/http_api/user.md

@@ -363,6 +363,39 @@ Content-Type: application/json
 ]
 ]
 ```
 ```
 
 
+## Teams that the actual User is member of
+
+`GET /api/user/teams`
+
+Return a list of all teams that the current user is member of.
+
+**Example Request**:
+
+```http
+GET /api/user/teams HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+```
+
+**Example Response**:
+
+```http
+HTTP/1.1 200
+Content-Type: application/json
+
+[
+  {
+    "id": 1,
+    "orgId": 1,
+    "name": "MyTestTeam",
+    "email": "",
+    "avatarUrl": "\/avatar\/3f49c15916554246daa714b9bd0ee398",
+    "memberCount": 1
+  }
+]
+```
+
 ## Star a dashboard
 ## Star a dashboard
 
 
 `POST /api/user/stars/dashboard/:dashboardId`
 `POST /api/user/stars/dashboard/:dashboardId`

+ 118 - 8
docs/sources/installation/configuration.md

@@ -15,6 +15,8 @@ weight = 1
 The Grafana back-end has a number of configuration options that can be
 The Grafana back-end has a number of configuration options that can be
 specified in a `.ini` configuration file or specified using environment variables.
 specified in a `.ini` configuration file or specified using environment variables.
 
 
+> **Note.** Grafana needs to be restarted for any configuration changes to take effect.
+
 ## Comments In .ini Files
 ## Comments In .ini Files
 
 
 Semicolons (the `;` char) are the standard way to comment out lines in a `.ini` file.
 Semicolons (the `;` char) are the standard way to comment out lines in a `.ini` file.
@@ -82,7 +84,7 @@ command line in the init.d script or the systemd service file.
 
 
 ### temp_data_lifetime
 ### temp_data_lifetime
 
 
-How long temporary images in `data` directory should be kept. Defaults to: `24h`. Supported modifiers: `h` (hours), 
+How long temporary images in `data` directory should be kept. Defaults to: `24h`. Supported modifiers: `h` (hours),
 `m` (minutes), for example: `168h`, `30m`, `10h30m`. Use `0` to never clean up temporary files.
 `m` (minutes), for example: `168h`, `30m`, `10h30m`. Use `0` to never clean up temporary files.
 
 
 ### logs
 ### logs
@@ -179,7 +181,7 @@ embedded database (included in the main Grafana binary).
 
 
 ### url
 ### url
 
 
-Use either URL or or the other fields below to configure the database
+Use either URL or the other fields below to configure the database
 Example: `mysql://user:secret@host:port/database`
 Example: `mysql://user:secret@host:port/database`
 
 
 ### type
 ### type
@@ -193,9 +195,9 @@ will be stored.
 
 
 ### host
 ### host
 
 
-Only applicable to MySQL or Postgres. Includes IP or hostname and port.
+Only applicable to MySQL or Postgres. Includes IP or hostname and port or in case of unix sockets the path to it.
 For example, for MySQL running on the same host as Grafana: `host =
 For example, for MySQL running on the same host as Grafana: `host =
-127.0.0.1:3306`
+127.0.0.1:3306` or with unix sockets: `host = /var/run/mysqld/mysqld.sock`
 
 
 ### name
 ### name
 
 
@@ -296,6 +298,12 @@ Set to `true` to automatically add new users to the main organization
 (id 1). When set to `false`, new users will automatically cause a new
 (id 1). When set to `false`, new users will automatically cause a new
 organization to be created for that new user.
 organization to be created for that new user.
 
 
+### auto_assign_org_id
+
+Set this value to automatically add new users to the provided org.
+This requires `auto_assign_org` to be set to `true`. Please make sure
+that this organization does already exists.
+
 ### auto_assign_org_role
 ### auto_assign_org_role
 
 
 The role new users will be assigned for the main organization (if the
 The role new users will be assigned for the main organization (if the
@@ -422,6 +430,108 @@ allowed_organizations = github google
 
 
 <hr>
 <hr>
 
 
+## [auth.gitlab]
+
+> Only available in Grafana v5.3+.
+
+You need to [create a GitLab OAuth
+application](https://docs.gitlab.com/ce/integration/oauth_provider.html).
+Choose a descriptive *Name*, and use the following *Redirect URI*:
+
+```
+https://grafana.example.com/login/gitlab
+```
+
+where `https://grafana.example.com` is the URL you use to connect to Grafana.
+Adjust it as needed if you don't use HTTPS or if you use a different port; for
+instance, if you access Grafana at `http://203.0.113.31:3000`, you should use
+
+```
+http://203.0.113.31:3000/login/gitlab
+```
+
+Finally, select *api* as the *Scope* and submit the form. Note that if you're
+not going to use GitLab groups for authorization (i.e. not setting
+`allowed_groups`, see below), you can select *read_user* instead of *api* as
+the *Scope*, thus giving a more restricted access to your GitLab API.
+
+You'll get an *Application Id* and a *Secret* in return; we'll call them
+`GITLAB_APPLICATION_ID` and `GITLAB_SECRET` respectively for the rest of this
+section.
+
+Add the following to your Grafana configuration file to enable GitLab
+authentication:
+
+```ini
+[auth.gitlab]
+enabled = false
+allow_sign_up = false
+client_id = GITLAB_APPLICATION_ID
+client_secret = GITLAB_SECRET
+scopes = api
+auth_url = https://gitlab.com/oauth/authorize
+token_url = https://gitlab.com/oauth/token
+api_url = https://gitlab.com/api/v4
+allowed_groups =
+```
+
+Restart the Grafana backend for your changes to take effect.
+
+If you use your own instance of GitLab instead of `gitlab.com`, adjust
+`auth_url`, `token_url` and `api_url` accordingly by replacing the `gitlab.com`
+hostname with your own.
+
+With `allow_sign_up` set to `false`, only existing users will be able to login
+using their GitLab account, but with `allow_sign_up` set to `true`, *any* user
+who can authenticate on GitLab will be able to login on your Grafana instance;
+if you use the public `gitlab.com`, it means anyone in the world would be able
+to login on your Grafana instance.
+
+You can can however limit access to only members of a given group or list of
+groups by setting the `allowed_groups` option.
+
+### allowed_groups
+
+To limit access to authenticated users that are members of one or more [GitLab
+groups](https://docs.gitlab.com/ce/user/group/index.html), set `allowed_groups`
+to a comma- or space-separated list of groups. For instance, if you want to
+only give access to members of the `example` group, set
+
+
+```ini
+allowed_groups = example
+```
+
+If you want to also give access to members of the subgroup `bar`, which is in
+the group `foo`, set
+
+```ini
+allowed_groups = example, foo/bar
+```
+
+Note that in GitLab, the group or subgroup name doesn't always match its
+display name, especially if the display name contains spaces or special
+characters. Make sure you always use the group or subgroup name as it appears
+in the URL of the group or subgroup.
+
+Here's a complete example with `alloed_sign_up` enabled, and access limited to
+the `example` and `foo/bar` groups:
+
+```ini
+[auth.gitlab]
+enabled = false
+allow_sign_up = true
+client_id = GITLAB_APPLICATION_ID
+client_secret = GITLAB_SECRET
+scopes = api
+auth_url = https://gitlab.com/oauth/authorize
+token_url = https://gitlab.com/oauth/token
+api_url = https://gitlab.com/api/v4
+allowed_groups = example, foo/bar
+```
+
+<hr>
+
 ## [auth.google]
 ## [auth.google]
 
 
 First, you need to create a Google OAuth Client:
 First, you need to create a Google OAuth Client:
@@ -689,9 +799,9 @@ session provider you have configured.
 
 
 - **file:** session file path, e.g. `data/sessions`
 - **file:** session file path, e.g. `data/sessions`
 - **mysql:** go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1:3306)/database_name`
 - **mysql:** go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1:3306)/database_name`
-- **postgres:** ex:  user=a password=b host=localhost port=5432 dbname=c sslmode=verify-full
-- **memcache:** ex:  127.0.0.1:11211
-- **redis:** ex: `addr=127.0.0.1:6379,pool_size=100,prefix=grafana`
+- **postgres:** ex:  `user=a password=b host=localhost port=5432 dbname=c sslmode=verify-full`
+- **memcache:** ex:  `127.0.0.1:11211`
+- **redis:** ex: `addr=127.0.0.1:6379,pool_size=100,prefix=grafana`. For unix socket, use for example: `network=unix,addr=/var/run/redis/redis.sock,pool_size=100,db=grafana`
 
 
 Postgres valid `sslmode` are `disable`, `require`, `verify-ca`, and `verify-full` (default).
 Postgres valid `sslmode` are `disable`, `require`, `verify-ca`, and `verify-full` (default).
 
 
@@ -857,7 +967,7 @@ Secret key. e.g. AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
 Url to where Grafana will send PUT request with images
 Url to where Grafana will send PUT request with images
 
 
 ### public_url
 ### public_url
-Optional parameter. Url to send to users in notifications, directly appended with the resulting uploaded file name.
+Optional parameter. Url to send to users in notifications. If the string contains the sequence ${file}, it will be replaced with the uploaded filename. Otherwise, the file name will be appended to the path part of the url, leaving any query string unchanged.
 
 
 ### username
 ### username
 basic auth username
 basic auth username

+ 8 - 3
docs/sources/installation/docker.md

@@ -38,6 +38,8 @@ The back-end web server has a number of configuration options. Go to the
 [Configuration]({{< relref "configuration.md" >}}) page for details on all
 [Configuration]({{< relref "configuration.md" >}}) page for details on all
 those options.
 those options.
 
 
+> For any changes to `conf/grafana.ini` (or corresponding environment variables) to take effect you need to restart Grafana by restarting the Docker container.
+
 ## Running a Specific Version of Grafana
 ## Running a Specific Version of Grafana
 
 
 ```bash
 ```bash
@@ -49,10 +51,13 @@ $ docker run \
   grafana/grafana:5.1.0
   grafana/grafana:5.1.0
 ```
 ```
 
 
-## Running of the master branch
+## Running the master branch
+
+For every successful build of the master branch we update the `grafana/grafana:master` tag and create a new tag `grafana/grafana-dev:master-<commit hash>` with the hash of the git commit that was built. This means you can always get the latest version of Grafana.
+
+When running Grafana master in production we **strongly** recommend that you use the `grafana/grafana-dev:master-<commit hash>` tag as that will guarantee that you use a specific version of Grafana instead of whatever was the most recent commit at the time.
 
 
-For every successful commit we publish a Grafana container to [`grafana/grafana`](https://hub.docker.com/r/grafana/grafana/tags/) and [`grafana/grafana-dev`](https://hub.docker.com/r/grafana/grafana-dev/tags/). In `grafana/grafana` container we will always overwrite the `master` tag with the latest version. In `grafana/grafana-dev` we will include
-the git commit in the tag. If you run Grafana master in production we **strongly** recommend that you use the later since different machines might run different version of grafana if they pull the master tag at different times.
+For a list of available tags, check out [grafana/grafana](https://hub.docker.com/r/grafana/grafana/tags/) and [grafana/grafana-dev](https://hub.docker.com/r/grafana/grafana-dev/tags/). 
 
 
 ## Installing Plugins for Grafana
 ## Installing Plugins for Grafana
 
 

+ 13 - 2
docs/sources/installation/ldap.md

@@ -23,8 +23,9 @@ specific configuration file (default: `/etc/grafana/ldap.toml`).
 ### Example config
 ### Example config
 
 
 ```toml
 ```toml
-# Set to true to log user information returned from LDAP
-verbose_logging = false
+# To troubleshoot and get more log info enable ldap debug logging in grafana.ini
+# [log]
+# filters = ldap:debug
 
 
 [[servers]]
 [[servers]]
 # Ldap server host (specify multiple hosts space separated)
 # Ldap server host (specify multiple hosts space separated)
@@ -39,6 +40,9 @@ start_tls = false
 ssl_skip_verify = false
 ssl_skip_verify = false
 # set to the path to your root CA certificate or leave unset to use system defaults
 # set to the path to your root CA certificate or leave unset to use system defaults
 # root_ca_cert = "/path/to/certificate.crt"
 # root_ca_cert = "/path/to/certificate.crt"
+# Authentication against LDAP servers requiring client certificates
+# client_cert = "/path/to/client.crt"
+# client_key = "/path/to/client.key"
 
 
 # Search user bind dn
 # Search user bind dn
 bind_dn = "cn=admin,dc=grafana,dc=org"
 bind_dn = "cn=admin,dc=grafana,dc=org"
@@ -47,6 +51,7 @@ bind_dn = "cn=admin,dc=grafana,dc=org"
 bind_password = 'grafana'
 bind_password = 'grafana'
 
 
 # User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"
 # User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"
+# Allow login from email or username, example "(|(sAMAccountName=%s)(userPrincipalName=%s))"
 search_filter = "(cn=%s)"
 search_filter = "(cn=%s)"
 
 
 # An array of base dns to search through
 # An array of base dns to search through
@@ -73,6 +78,8 @@ email =  "email"
 [[servers.group_mappings]]
 [[servers.group_mappings]]
 group_dn = "cn=admins,dc=grafana,dc=org"
 group_dn = "cn=admins,dc=grafana,dc=org"
 org_role = "Admin"
 org_role = "Admin"
+# To make user an instance admin  (Grafana Admin) uncomment line below
+# grafana_admin = true
 # The Grafana organization database id, optional, if left out the default org (id 1) will be used.  Setting this allows for multiple group_dn's to be assigned to the same org_role provided the org_id differs
 # The Grafana organization database id, optional, if left out the default org (id 1) will be used.  Setting this allows for multiple group_dn's to be assigned to the same org_role provided the org_id differs
 # org_id = 1
 # org_id = 1
 
 
@@ -132,6 +139,10 @@ Users page, this change will be reset the next time the user logs in. If you
 change the LDAP groups of a user, the change will take effect the next
 change the LDAP groups of a user, the change will take effect the next
 time the user logs in.
 time the user logs in.
 
 
+### Grafana Admin
+with a servers.group_mappings section you can set grafana_admin = true or false to sync Grafana Admin permission. A Grafana server admin has admin access over all orgs &
+users.
+
 ### Priority
 ### Priority
 The first group mapping that an LDAP user is matched to will be used for the sync. If you have LDAP users that fit multiple mappings, the topmost mapping in the TOML config will be used.
 The first group mapping that an LDAP user is matched to will be used for the sync. If you have LDAP users that fit multiple mappings, the topmost mapping in the TOML config will be used.
 
 

+ 8 - 11
docs/sources/project/building_from_source.md

@@ -57,7 +57,7 @@ For this you need nodejs (v.6+).
 ```bash
 ```bash
 npm install -g yarn
 npm install -g yarn
 yarn install --pure-lockfile
 yarn install --pure-lockfile
-npm run watch
+yarn watch
 ```
 ```
 
 
 ## Running Grafana Locally
 ## Running Grafana Locally
@@ -83,21 +83,18 @@ go get github.com/Unknwon/bra
 bra run
 bra run
 ```
 ```
 
 
-You'll also need to run `npm run watch` to watch for changes to the front-end (typescript, html, sass)
+You'll also need to run `yarn watch` to watch for changes to the front-end (typescript, html, sass)
 
 
 ### Running tests
 ### Running tests
 
 
-- You can run backend Golang tests using "go test ./pkg/...".
-- Execute all frontend tests with "npm run test"
+- You can run backend Golang tests using `go test ./pkg/...`.
+- Execute all frontend tests with `yarn test`
 
 
-Writing & watching frontend tests (we have two test runners)
+Writing & watching frontend tests
+
+- Start watcher: `yarn jest`
+- Jest will run all test files that end with the name ".test.ts"
 
 
-- jest for all new tests that do not require browser context (React+more)
-   - Start watcher: `npm run jest`
-   - Jest will run all test files that end with the name ".jest.ts"
-- karma + mocha is used for testing angularjs components. We do want to migrate these test to jest over time (if possible).
-  - Start watcher: `npm run karma`
-  - Karma+Mocha runs all files that end with the name "_specs.ts".
 
 
 ## Creating optimized release packages
 ## Creating optimized release packages
 
 

+ 29 - 9
docs/sources/reference/templating.md

@@ -11,7 +11,7 @@ weight = 1
 # Variables
 # Variables
 
 
 Variables allows for more interactive and dynamic dashboards. Instead of hard-coding things like server, application
 Variables allows for more interactive and dynamic dashboards. Instead of hard-coding things like server, application
-and sensor name in you metric queries you can use variables in their place. Variables are shown as dropdown select boxes at the top of
+and sensor name in your metric queries you can use variables in their place. Variables are shown as dropdown select boxes at the top of
 the dashboard. These dropdowns make it easy to change the data being displayed in your dashboard.
 the dashboard. These dropdowns make it easy to change the data being displayed in your dashboard.
 
 
 {{< docs-imagebox img="/img/docs/v50/variables_dashboard.png" >}}
 {{< docs-imagebox img="/img/docs/v50/variables_dashboard.png" >}}
@@ -273,29 +273,49 @@ The `$__timeFilter` is used in the MySQL data source.
 
 
 This variable is only available in the Singlestat panel and can be used in the prefix or suffix fields on the Options tab. The variable will be replaced with the series name or alias.
 This variable is only available in the Singlestat panel and can be used in the prefix or suffix fields on the Options tab. The variable will be replaced with the series name or alias.
 
 
+### The $__range Variable
+
+> Only available in Grafana v5.3+
+
+Currently only supported for Prometheus data sources. This variable represents the range for the current dashboard. It is calculated by `to - from`. It has a millisecond and a second representation called `$__range_ms` and `$__range_s`.
+
 ## Repeating Panels
 ## Repeating Panels
 
 
 Template variables can be very useful to dynamically change your queries across a whole dashboard. If you want
 Template variables can be very useful to dynamically change your queries across a whole dashboard. If you want
 Grafana to dynamically create new panels or rows based on what values you have selected you can use the *Repeat* feature.
 Grafana to dynamically create new panels or rows based on what values you have selected you can use the *Repeat* feature.
 
 
-If you have a variable with `Multi-value` or `Include all value` options enabled you can choose one panel or one row and have Grafana repeat that row
-for every selected value. You find this option under the General tab in panel edit mode. Select the variable to repeat by, and a `min span`.
-The `min span` controls how small Grafana will make the panels (if you have many values selected). Grafana will automatically adjust the width of
-each repeated panel so that the whole row is filled. Currently, you cannot mix other panels on a row with a repeated panel.
+If you have a variable with `Multi-value` or `Include all value` options enabled you can choose one panel and have Grafana repeat that panel
+for every selected value. You find the *Repeat* feature under the *General tab* in panel edit mode.
+
+The `direction` controls how the panels will be arranged.
+
+By choosing `horizontal` the panels will be arranged side-by-side. Grafana will automatically adjust the width
+of each repeated panel so that the whole row is filled. Currently, you cannot mix other panels on a row with a repeated
+panel. Each panel will never be smaller that the provided `Min width` if you have many selected values.
+
+By choosing `vertical` the panels will be arranged from top to bottom in a column. The `Min width` doesn't have any effect in this case. The width of the repeated panels will be the same as of the first panel (the original template) being repeated.
 
 
 Only make changes to the first panel (the original template). To have the changes take effect on all panels you need to trigger a dynamic dashboard re-build.
 Only make changes to the first panel (the original template). To have the changes take effect on all panels you need to trigger a dynamic dashboard re-build.
 You can do this by either changing the variable value (that is the basis for the repeat) or reload the dashboard.
 You can do this by either changing the variable value (that is the basis for the repeat) or reload the dashboard.
 
 
 ## Repeating Rows
 ## Repeating Rows
 
 
-This option requires you to open the row options view. Hover over the row left side to trigger the row menu, in this menu click `Row Options`. This
-opens the row options view. Here you find a *Repeat* dropdown where you can select the variable to repeat by.
+As seen above with the *Panels* you can also repeat *Rows* if you have variables set with  `Multi-value` or
+`Include all value` selection option.
+
+To enable this feature you need to first add a new *Row* using the *Add Panel* menu. Then by hovering the row title and
+clicking on the cog button, you will access the `Row Options` configuration panel. You can then select the variable
+you want to repeat the row for.
+
+It may be a good idea to use a variable in the row title as well.
+
+Example: [Repeated Rows Dashboard](http://play.grafana.org/dashboard/db/repeated-rows)
 
 
-### URL state
+## URL state
 
 
 Variable values are always synced to the URL using the syntax `var-<varname>=value`.
 Variable values are always synced to the URL using the syntax `var-<varname>=value`.
 
 
-### Examples
+## Examples
 
 
 - [Graphite Templated Dashboard](http://play.grafana.org/dashboard/db/graphite-templated-nested)
 - [Graphite Templated Dashboard](http://play.grafana.org/dashboard/db/graphite-templated-nested)
 - [Elasticsearch Templated Dashboard](http://play.grafana.org/dashboard/db/elasticsearch-templated)
 - [Elasticsearch Templated Dashboard](http://play.grafana.org/dashboard/db/elasticsearch-templated)

+ 1 - 1
jest.config.js

@@ -13,7 +13,7 @@ module.exports = {
   "roots": [
   "roots": [
     "<rootDir>/public"
     "<rootDir>/public"
   ],
   ],
-  "testRegex": "(\\.|/)(jest)\\.(jsx?|tsx?)$",
+  "testRegex": "(\\.|/)(test)\\.(jsx?|tsx?)$",
   "moduleFileExtensions": [
   "moduleFileExtensions": [
     "ts",
     "ts",
     "tsx",
     "tsx",

+ 0 - 40
karma.conf.js

@@ -1,40 +0,0 @@
-var webpack = require('webpack');
-var path = require('path');
-var webpackTestConfig = require('./scripts/webpack/webpack.test.js');
-
-module.exports = function(config) {
-
-  'use strict';
-
-  config.set({
-    frameworks: ['mocha', 'expect', 'sinon'],
-
-    // list of files / patterns to load in the browser
-    files: [
-      { pattern: 'public/test/index.ts', watched: false }
-    ],
-
-    preprocessors: {
-      'public/test/index.ts': ['webpack', 'sourcemap'],
-    },
-
-    webpack: webpackTestConfig,
-    webpackMiddleware: {
-      stats: 'minimal',
-    },
-
-    // list of files to exclude
-    exclude: [],
-    reporters: ['dots'],
-    port: 9876,
-    colors: true,
-    logLevel: config.LOG_INFO,
-    autoWatch: true,
-    browsers: ['PhantomJS'],
-    captureTimeout: 20000,
-    singleRun: true,
-    // autoWatchBatchDelay: 1000,
-    // browserNoActivityTimeout: 60000,
-  });
-
-};

+ 9 - 21
package.json

@@ -32,9 +32,8 @@
     "es6-shim": "^0.35.3",
     "es6-shim": "^0.35.3",
     "expect.js": "~0.2.0",
     "expect.js": "~0.2.0",
     "expose-loader": "^0.7.3",
     "expose-loader": "^0.7.3",
-    "extract-text-webpack-plugin": "^4.0.0-beta.0",
     "file-loader": "^1.1.11",
     "file-loader": "^1.1.11",
-    "fork-ts-checker-webpack-plugin": "^0.4.1",
+    "fork-ts-checker-webpack-plugin": "^0.4.2",
     "gaze": "^1.1.2",
     "gaze": "^1.1.2",
     "glob": "~7.0.0",
     "glob": "~7.0.0",
     "grunt": "1.0.1",
     "grunt": "1.0.1",
@@ -45,10 +44,7 @@
     "grunt-contrib-concat": "^1.0.1",
     "grunt-contrib-concat": "^1.0.1",
     "grunt-contrib-copy": "~1.0.0",
     "grunt-contrib-copy": "~1.0.0",
     "grunt-contrib-cssmin": "~1.0.2",
     "grunt-contrib-cssmin": "~1.0.2",
-    "grunt-contrib-jshint": "~1.1.0",
     "grunt-exec": "^1.0.1",
     "grunt-exec": "^1.0.1",
-    "grunt-jscs": "3.0.1",
-    "grunt-karma": "~2.0.0",
     "grunt-notify": "^0.4.5",
     "grunt-notify": "^0.4.5",
     "grunt-postcss": "^0.8.0",
     "grunt-postcss": "^0.8.0",
     "grunt-sass": "^2.0.0",
     "grunt-sass": "^2.0.0",
@@ -60,23 +56,16 @@
     "html-webpack-plugin": "^3.2.0",
     "html-webpack-plugin": "^3.2.0",
     "husky": "^0.14.3",
     "husky": "^0.14.3",
     "jest": "^22.0.4",
     "jest": "^22.0.4",
-    "jshint-stylish": "~2.2.1",
-    "karma": "1.7.0",
-    "karma-chrome-launcher": "~2.2.0",
-    "karma-expect": "~1.1.3",
-    "karma-mocha": "~1.3.0",
-    "karma-phantomjs-launcher": "1.0.4",
-    "karma-sinon": "^1.0.5",
-    "karma-sourcemap-loader": "^0.3.7",
-    "karma-webpack": "^3.0.0",
     "lint-staged": "^6.0.0",
     "lint-staged": "^6.0.0",
     "load-grunt-tasks": "3.5.2",
     "load-grunt-tasks": "3.5.2",
+    "mini-css-extract-plugin": "^0.4.0",
     "mobx-react-devtools": "^4.2.15",
     "mobx-react-devtools": "^4.2.15",
     "mocha": "^4.0.1",
     "mocha": "^4.0.1",
     "ng-annotate-loader": "^0.6.1",
     "ng-annotate-loader": "^0.6.1",
-    "ng-annotate-webpack-plugin": "^0.2.1-pre",
+    "ng-annotate-webpack-plugin": "^0.3.0",
     "ngtemplate-loader": "^2.0.1",
     "ngtemplate-loader": "^2.0.1",
     "npm": "^5.4.2",
     "npm": "^5.4.2",
+    "optimize-css-assets-webpack-plugin": "^4.0.2",
     "phantomjs-prebuilt": "^2.1.15",
     "phantomjs-prebuilt": "^2.1.15",
     "postcss-browser-reporter": "^0.5.0",
     "postcss-browser-reporter": "^0.5.0",
     "postcss-loader": "^2.0.6",
     "postcss-loader": "^2.0.6",
@@ -90,15 +79,16 @@
     "style-loader": "^0.21.0",
     "style-loader": "^0.21.0",
     "systemjs": "0.20.19",
     "systemjs": "0.20.19",
     "systemjs-plugin-css": "^0.1.36",
     "systemjs-plugin-css": "^0.1.36",
-    "ts-loader": "^4.3.0",
     "ts-jest": "^22.4.6",
     "ts-jest": "^22.4.6",
+    "ts-loader": "^4.3.0",
+    "tslib": "^1.9.3",
     "tslint": "^5.8.0",
     "tslint": "^5.8.0",
     "tslint-loader": "^3.5.3",
     "tslint-loader": "^3.5.3",
     "typescript": "^2.6.2",
     "typescript": "^2.6.2",
+    "uglifyjs-webpack-plugin": "^1.2.7",
     "webpack": "^4.8.0",
     "webpack": "^4.8.0",
     "webpack-bundle-analyzer": "^2.9.0",
     "webpack-bundle-analyzer": "^2.9.0",
     "webpack-cleanup-plugin": "^0.5.1",
     "webpack-cleanup-plugin": "^0.5.1",
-    "fork-ts-checker-webpack-plugin": "^0.4.2",
     "webpack-cli": "^2.1.4",
     "webpack-cli": "^2.1.4",
     "webpack-dev-server": "^3.1.0",
     "webpack-dev-server": "^3.1.0",
     "webpack-merge": "^4.1.0",
     "webpack-merge": "^4.1.0",
@@ -112,7 +102,6 @@
     "test": "grunt test",
     "test": "grunt test",
     "test:coverage": "grunt test --coverage=true",
     "test:coverage": "grunt test --coverage=true",
     "lint": "tslint -c tslint.json --project tsconfig.json --type-check",
     "lint": "tslint -c tslint.json --project tsconfig.json --type-check",
-    "karma": "grunt karma:dev",
     "jest": "jest --notify --watch",
     "jest": "jest --notify --watch",
     "api-tests": "jest --notify --watch --config=tests/api/jest.js",
     "api-tests": "jest --notify --watch --config=tests/api/jest.js",
     "precommit": "lint-staged && grunt precommit"
     "precommit": "lint-staged && grunt precommit"
@@ -155,16 +144,15 @@
     "immutable": "^3.8.2",
     "immutable": "^3.8.2",
     "jquery": "^3.2.1",
     "jquery": "^3.2.1",
     "lodash": "^4.17.10",
     "lodash": "^4.17.10",
-    "mini-css-extract-plugin": "^0.4.0",
     "mobx": "^3.4.1",
     "mobx": "^3.4.1",
     "mobx-react": "^4.3.5",
     "mobx-react": "^4.3.5",
     "mobx-state-tree": "^1.3.1",
     "mobx-state-tree": "^1.3.1",
     "moment": "^2.22.2",
     "moment": "^2.22.2",
     "mousetrap": "^1.6.0",
     "mousetrap": "^1.6.0",
     "mousetrap-global-bind": "^1.1.0",
     "mousetrap-global-bind": "^1.1.0",
-    "optimize-css-assets-webpack-plugin": "^4.0.2",
     "prismjs": "^1.6.0",
     "prismjs": "^1.6.0",
     "prop-types": "^15.6.0",
     "prop-types": "^15.6.0",
+    "rc-cascader": "^0.14.0",
     "react": "^16.2.0",
     "react": "^16.2.0",
     "react-dom": "^16.2.0",
     "react-dom": "^16.2.0",
     "react-grid-layout": "0.16.6",
     "react-grid-layout": "0.16.6",
@@ -182,7 +170,7 @@
     "tether": "^1.4.0",
     "tether": "^1.4.0",
     "tether-drop": "https://github.com/torkelo/drop/tarball/master",
     "tether-drop": "https://github.com/torkelo/drop/tarball/master",
     "tinycolor2": "^1.4.1",
     "tinycolor2": "^1.4.1",
-    "uglifyjs-webpack-plugin": "^1.2.7"
+    "tslint-react": "^3.6.0"
   },
   },
   "resolutions": {
   "resolutions": {
     "caniuse-db": "1.0.30000772"
     "caniuse-db": "1.0.30000772"

+ 52 - 0
packaging/docker/Dockerfile

@@ -0,0 +1,52 @@
+FROM debian:stretch-slim
+
+ARG GRAFANA_TGZ="grafana-latest.linux-x64.tar.gz"
+
+RUN apt-get update && apt-get install -qq -y tar && \
+    apt-get autoremove -y && \
+    rm -rf /var/lib/apt/lists/*
+
+COPY ${GRAFANA_TGZ} /tmp/grafana.tar.gz
+
+RUN mkdir /tmp/grafana && tar xfvz /tmp/grafana.tar.gz --strip-components=1 -C /tmp/grafana
+
+FROM debian:stretch-slim
+
+ARG GF_UID="472"
+ARG GF_GID="472"
+
+ENV PATH=/usr/share/grafana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
+    GF_PATHS_CONFIG="/etc/grafana/grafana.ini" \
+    GF_PATHS_DATA="/var/lib/grafana" \
+    GF_PATHS_HOME="/usr/share/grafana" \
+    GF_PATHS_LOGS="/var/log/grafana" \
+    GF_PATHS_PLUGINS="/var/lib/grafana/plugins" \
+    GF_PATHS_PROVISIONING="/etc/grafana/provisioning"
+
+WORKDIR $GF_PATHS_HOME
+
+RUN apt-get update && apt-get install -qq -y libfontconfig ca-certificates && \
+    apt-get autoremove -y && \
+    rm -rf /var/lib/apt/lists/*
+
+COPY --from=0 /tmp/grafana "$GF_PATHS_HOME"
+
+RUN mkdir -p "$GF_PATHS_HOME/.aws" && \
+    groupadd -r -g $GF_GID grafana && \
+    useradd -r -u $GF_UID -g grafana grafana && \
+    mkdir -p "$GF_PATHS_PROVISIONING/datasources" \
+             "$GF_PATHS_PROVISIONING/dashboards" \
+             "$GF_PATHS_LOGS" \
+             "$GF_PATHS_PLUGINS" \
+             "$GF_PATHS_DATA" && \
+    cp "$GF_PATHS_HOME/conf/sample.ini" "$GF_PATHS_CONFIG" && \
+    cp "$GF_PATHS_HOME/conf/ldap.toml" /etc/grafana/ldap.toml && \
+    chown -R grafana:grafana "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" && \
+    chmod 777 "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS"
+
+EXPOSE 3000
+
+COPY ./run.sh /run.sh
+
+USER grafana
+ENTRYPOINT [ "/run.sh" ]

+ 43 - 0
packaging/docker/README.md

@@ -0,0 +1,43 @@
+# Grafana Docker image
+
+## Running your Grafana container
+
+Start your container binding the external port `3000`.
+
+```bash
+docker run -d --name=grafana -p 3000:3000 grafana/grafana
+```
+
+Try it out, default admin user is admin/admin.
+
+## How to use the container
+
+Further documentation can be found at http://docs.grafana.org/installation/docker/
+
+## Changelog
+
+### v5.1.5, v5.2.0-beta2
+* Fix: config keys ending with _FILE are not respected [#170](https://github.com/grafana/grafana-docker/issues/170)
+
+### v5.2.0-beta1
+* Support for Docker Secrets
+
+### v5.1.0
+* Major restructuring of the container
+* Usage of `chown` removed
+* File permissions incompatibility with previous versions
+  * user id changed from 104 to 472
+  * group id changed from 107 to 472
+* Runs as the grafana user by default (instead of root)
+* All default volumes removed
+
+### v4.2.0
+* Plugins are now installed into ${GF_PATHS_PLUGINS}
+* Building the container now requires a full url to the deb package instead of just version
+* Fixes bug caused by installing multiple plugins
+
+### v4.0.0-beta2
+* Plugins dir (`/var/lib/grafana/plugins`) is no longer a separate volume
+
+### v3.1.1
+* Make it possible to install specific plugin version https://github.com/grafana/grafana-docker/issues/59#issuecomment-260584026

+ 13 - 0
packaging/docker/build-deploy.sh

@@ -0,0 +1,13 @@
+#!/bin/sh
+set -e
+
+_grafana_version=$1
+./build.sh "$_grafana_version"
+docker login -u "$DOCKER_USER" -p "$DOCKER_PASS"
+
+./push_to_docker_hub.sh "$_grafana_version"
+
+if echo "$_grafana_version" | grep -q "^master-"; then
+  apk add --no-cache curl
+  ./deploy_to_k8s.sh "grafana/grafana-dev:$_grafana_version"
+fi

+ 25 - 0
packaging/docker/build.sh

@@ -0,0 +1,25 @@
+#!/bin/sh
+
+_grafana_tag=$1
+
+# If the tag starts with v, treat this as a official release
+if echo "$_grafana_tag" | grep -q "^v"; then
+	_grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2)
+	_docker_repo=${2:-grafana/grafana}
+else
+	_grafana_version=$_grafana_tag
+	_docker_repo=${2:-grafana/grafana-dev}
+fi
+
+echo "Building ${_docker_repo}:${_grafana_version}"
+
+docker build \
+	--tag "${_docker_repo}:${_grafana_version}" \
+	--no-cache=true .
+
+# Tag as 'latest' for official release; otherwise tag as grafana/grafana:master
+if echo "$_grafana_tag" | grep -q "^v"; then
+	docker tag "${_docker_repo}:${_grafana_version}" "${_docker_repo}:latest"
+else
+	docker tag "${_docker_repo}:${_grafana_version}" "grafana/grafana:master"
+fi

+ 16 - 0
packaging/docker/custom/Dockerfile

@@ -0,0 +1,16 @@
+ARG GRAFANA_VERSION="latest"
+
+FROM grafana/grafana:${GRAFANA_VERSION}
+
+USER grafana
+
+ARG GF_INSTALL_PLUGINS=""
+
+RUN if [ ! -z "${GF_INSTALL_PLUGINS}" ]; then \
+    OLDIFS=$IFS; \
+        IFS=','; \
+    for plugin in ${GF_INSTALL_PLUGINS}; do \
+        IFS=$OLDIFS; \
+        grafana-cli --pluginsDir "$GF_PATHS_PLUGINS" plugins install ${plugin}; \
+    done; \
+fi

+ 6 - 0
packaging/docker/deploy_to_k8s.sh

@@ -0,0 +1,6 @@
+#!/bin/sh
+
+curl -s --header "Content-Type: application/json" \
+     --data "{\"build_parameters\": {\"CIRCLE_JOB\": \"deploy\", \"IMAGE_NAMES\": \"$1\"}}" \
+     --request POST \
+     https://circleci.com/api/v1.1/project/github/raintank/deployment_tools/tree/master?circle-token=$CIRCLE_TOKEN

+ 24 - 0
packaging/docker/push_to_docker_hub.sh

@@ -0,0 +1,24 @@
+#!/bin/sh
+set -e
+
+_grafana_tag=$1
+
+# If the tag starts with v, treat this as a official release
+if echo "$_grafana_tag" | grep -q "^v"; then
+	_grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2)
+	_docker_repo=${2:-grafana/grafana}
+else
+	_grafana_version=$_grafana_tag
+	_docker_repo=${2:-grafana/grafana-dev}
+fi
+
+echo "pushing ${_docker_repo}:${_grafana_version}"
+docker push "${_docker_repo}:${_grafana_version}"
+
+if echo "$_grafana_tag" | grep -q "^v" && echo "$_grafana_tag" | grep -vq "beta"; then
+	echo "pushing ${_docker_repo}:latest"
+	docker push "${_docker_repo}:latest"
+elif echo "$_grafana_tag" | grep -q "master"; then
+	echo "pushing grafana/grafana:master"
+	docker push grafana/grafana:master
+fi

+ 88 - 0
packaging/docker/run.sh

@@ -0,0 +1,88 @@
+#!/bin/bash -e
+
+PERMISSIONS_OK=0
+
+if [ ! -r "$GF_PATHS_CONFIG" ]; then
+    echo "GF_PATHS_CONFIG='$GF_PATHS_CONFIG' is not readable."
+    PERMISSIONS_OK=1
+fi
+
+if [ ! -w "$GF_PATHS_DATA" ]; then
+    echo "GF_PATHS_DATA='$GF_PATHS_DATA' is not writable."
+    PERMISSIONS_OK=1
+fi
+
+if [ ! -r "$GF_PATHS_HOME" ]; then
+    echo "GF_PATHS_HOME='$GF_PATHS_HOME' is not readable."
+    PERMISSIONS_OK=1
+fi
+
+if [ $PERMISSIONS_OK -eq 1 ]; then
+    echo "You may have issues with file permissions, more information here: http://docs.grafana.org/installation/docker/#migration-from-a-previous-version-of-the-docker-container-to-5-1-or-later"
+fi
+
+if [ ! -d "$GF_PATHS_PLUGINS" ]; then
+    mkdir "$GF_PATHS_PLUGINS"
+fi
+
+if [ ! -z ${GF_AWS_PROFILES+x} ]; then
+    > "$GF_PATHS_HOME/.aws/credentials"
+
+    for profile in ${GF_AWS_PROFILES}; do
+        access_key_varname="GF_AWS_${profile}_ACCESS_KEY_ID"
+        secret_key_varname="GF_AWS_${profile}_SECRET_ACCESS_KEY"
+        region_varname="GF_AWS_${profile}_REGION"
+
+        if [ ! -z "${!access_key_varname}" -a ! -z "${!secret_key_varname}" ]; then
+            echo "[${profile}]" >> "$GF_PATHS_HOME/.aws/credentials"
+            echo "aws_access_key_id = ${!access_key_varname}" >> "$GF_PATHS_HOME/.aws/credentials"
+            echo "aws_secret_access_key = ${!secret_key_varname}" >> "$GF_PATHS_HOME/.aws/credentials"
+            if [ ! -z "${!region_varname}" ]; then
+                echo "region = ${!region_varname}" >> "$GF_PATHS_HOME/.aws/credentials"
+            fi
+        fi
+    done
+
+    chmod 600 "$GF_PATHS_HOME/.aws/credentials"
+fi
+
+# Convert all environment variables with names ending in __FILE into the content of
+# the file that they point at and use the name without the trailing __FILE.
+# This can be used to carry in Docker secrets.
+for VAR_NAME in $(env | grep '^GF_[^=]\+__FILE=.\+' | sed -r "s/([^=]*)__FILE=.*/\1/g"); do
+    VAR_NAME_FILE="$VAR_NAME"__FILE
+    if [ "${!VAR_NAME}" ]; then
+        echo >&2 "ERROR: Both $VAR_NAME and $VAR_NAME_FILE are set (but are exclusive)"
+        exit 1
+    fi
+    echo "Getting secret $VAR_NAME from ${!VAR_NAME_FILE}"
+    export "$VAR_NAME"="$(< "${!VAR_NAME_FILE}")"
+    unset "$VAR_NAME_FILE"
+done
+
+export HOME="$GF_PATHS_HOME"
+
+if [ ! -z "${GF_INSTALL_PLUGINS}" ]; then
+  OLDIFS=$IFS
+  IFS=','
+  for plugin in ${GF_INSTALL_PLUGINS}; do
+    IFS=$OLDIFS
+    if [[ $plugin =~ .*\;.* ]]; then
+        pluginUrl=$(echo "$plugin" | cut -d';' -f 1)
+        pluginWithoutUrl=$(echo "$plugin" | cut -d';' -f 2)
+        grafana-cli --pluginUrl "${pluginUrl}" --pluginsDir "${GF_PATHS_PLUGINS}" plugins install ${pluginWithoutUrl}
+    else
+        grafana-cli --pluginsDir "${GF_PATHS_PLUGINS}" plugins install ${plugin}
+    fi
+  done
+fi
+
+exec grafana-server                                         \
+  --homepath="$GF_PATHS_HOME"                               \
+  --config="$GF_PATHS_CONFIG"                               \
+  "$@"                                                      \
+  cfg:default.log.mode="console"                            \
+  cfg:default.paths.data="$GF_PATHS_DATA"                   \
+  cfg:default.paths.logs="$GF_PATHS_LOGS"                   \
+  cfg:default.paths.plugins="$GF_PATHS_PLUGINS"             \
+  cfg:default.paths.provisioning="$GF_PATHS_PROVISIONING"

+ 2 - 2
pkg/api/api.go

@@ -73,8 +73,7 @@ func (hs *HTTPServer) registerRoutes() {
 	r.Get("/dashboards/", reqSignedIn, Index)
 	r.Get("/dashboards/", reqSignedIn, Index)
 	r.Get("/dashboards/*", reqSignedIn, Index)
 	r.Get("/dashboards/*", reqSignedIn, Index)
 
 
-	r.Get("/explore/", reqEditorRole, Index)
-	r.Get("/explore/*", reqEditorRole, Index)
+	r.Get("/explore", reqEditorRole, Index)
 
 
 	r.Get("/playlists/", reqSignedIn, Index)
 	r.Get("/playlists/", reqSignedIn, Index)
 	r.Get("/playlists/*", reqSignedIn, Index)
 	r.Get("/playlists/*", reqSignedIn, Index)
@@ -121,6 +120,7 @@ func (hs *HTTPServer) registerRoutes() {
 			userRoute.Put("/", bind(m.UpdateUserCommand{}), Wrap(UpdateSignedInUser))
 			userRoute.Put("/", bind(m.UpdateUserCommand{}), Wrap(UpdateSignedInUser))
 			userRoute.Post("/using/:id", Wrap(UserSetUsingOrg))
 			userRoute.Post("/using/:id", Wrap(UserSetUsingOrg))
 			userRoute.Get("/orgs", Wrap(GetSignedInUserOrgList))
 			userRoute.Get("/orgs", Wrap(GetSignedInUserOrgList))
+			userRoute.Get("/teams", Wrap(GetSignedInUserTeamList))
 
 
 			userRoute.Post("/stars/dashboard/:id", Wrap(StarDashboard))
 			userRoute.Post("/stars/dashboard/:id", Wrap(StarDashboard))
 			userRoute.Delete("/stars/dashboard/:id", Wrap(UnstarDashboard))
 			userRoute.Delete("/stars/dashboard/:id", Wrap(UnstarDashboard))

+ 16 - 2
pkg/api/datasources.go

@@ -158,12 +158,26 @@ func UpdateDataSource(c *m.ReqContext, cmd m.UpdateDataSourceCommand) Response {
 		}
 		}
 		return Error(500, "Failed to update datasource", err)
 		return Error(500, "Failed to update datasource", err)
 	}
 	}
-	ds := convertModelToDtos(cmd.Result)
+
+	query := m.GetDataSourceByIdQuery{
+		Id:    cmd.Id,
+		OrgId: c.OrgId,
+	}
+
+	if err := bus.Dispatch(&query); err != nil {
+		if err == m.ErrDataSourceNotFound {
+			return Error(404, "Data source not found", nil)
+		}
+		return Error(500, "Failed to query datasources", err)
+	}
+
+	dtos := convertModelToDtos(query.Result)
+
 	return JSON(200, util.DynMap{
 	return JSON(200, util.DynMap{
 		"message":    "Datasource updated",
 		"message":    "Datasource updated",
 		"id":         cmd.Id,
 		"id":         cmd.Id,
 		"name":       cmd.Name,
 		"name":       cmd.Name,
-		"datasource": ds,
+		"datasource": dtos,
 	})
 	})
 }
 }
 
 

+ 7 - 1
pkg/api/login.go

@@ -78,7 +78,13 @@ func tryLoginUsingRememberCookie(c *m.ReqContext) bool {
 	user := userQuery.Result
 	user := userQuery.Result
 
 
 	// validate remember me cookie
 	// validate remember me cookie
-	if val, _ := c.GetSuperSecureCookie(user.Rands+user.Password, setting.CookieRememberName); val != user.Login {
+	signingKey := user.Rands + user.Password
+	if len(signingKey) < 10 {
+		c.Logger.Error("Invalid user signingKey")
+		return false
+	}
+
+	if val, _ := c.GetSuperSecureCookie(signingKey, setting.CookieRememberName); val != user.Login {
 		return false
 		return false
 	}
 	}
 
 

+ 2 - 2
pkg/api/metrics.go

@@ -52,7 +52,7 @@ func QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) Response {
 		if res.Error != nil {
 		if res.Error != nil {
 			res.ErrorString = res.Error.Error()
 			res.ErrorString = res.Error.Error()
 			resp.Message = res.ErrorString
 			resp.Message = res.ErrorString
-			statusCode = 500
+			statusCode = 400
 		}
 		}
 	}
 	}
 
 
@@ -99,7 +99,7 @@ func GetTestDataRandomWalk(c *m.ReqContext) Response {
 	timeRange := tsdb.NewTimeRange(from, to)
 	timeRange := tsdb.NewTimeRange(from, to)
 	request := &tsdb.TsdbQuery{TimeRange: timeRange}
 	request := &tsdb.TsdbQuery{TimeRange: timeRange}
 
 
-	dsInfo := &m.DataSource{Type: "grafana-testdata-datasource"}
+	dsInfo := &m.DataSource{Type: "testdata"}
 	request.Queries = append(request.Queries, &tsdb.Query{
 	request.Queries = append(request.Queries, &tsdb.Query{
 		RefId:      "A",
 		RefId:      "A",
 		IntervalMs: intervalMs,
 		IntervalMs: intervalMs,

+ 1 - 0
pkg/api/playlist.go

@@ -160,6 +160,7 @@ func CreatePlaylist(c *m.ReqContext, cmd m.CreatePlaylistCommand) Response {
 
 
 func UpdatePlaylist(c *m.ReqContext, cmd m.UpdatePlaylistCommand) Response {
 func UpdatePlaylist(c *m.ReqContext, cmd m.UpdatePlaylistCommand) Response {
 	cmd.OrgId = c.OrgId
 	cmd.OrgId = c.OrgId
+	cmd.Id = c.ParamsInt64(":id")
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
 		return Error(500, "Failed to save playlist", err)
 		return Error(500, "Failed to save playlist", err)

+ 9 - 2
pkg/api/pluginproxy/ds_proxy.go

@@ -203,6 +203,7 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) {
 		req.Header.Del("X-Forwarded-Host")
 		req.Header.Del("X-Forwarded-Host")
 		req.Header.Del("X-Forwarded-Port")
 		req.Header.Del("X-Forwarded-Port")
 		req.Header.Del("X-Forwarded-Proto")
 		req.Header.Del("X-Forwarded-Proto")
+		req.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s", setting.BuildVersion))
 
 
 		// set X-Forwarded-For header
 		// set X-Forwarded-For header
 		if req.RemoteAddr != "" {
 		if req.RemoteAddr != "" {
@@ -319,9 +320,15 @@ func (proxy *DataSourceProxy) applyRoute(req *http.Request) {
 		SecureJsonData: proxy.ds.SecureJsonData.Decrypt(),
 		SecureJsonData: proxy.ds.SecureJsonData.Decrypt(),
 	}
 	}
 
 
-	routeURL, err := url.Parse(proxy.route.Url)
+	interpolatedURL, err := interpolateString(proxy.route.Url, data)
 	if err != nil {
 	if err != nil {
-		logger.Error("Error parsing plugin route url")
+		logger.Error("Error interpolating proxy url", "error", err)
+		return
+	}
+
+	routeURL, err := url.Parse(interpolatedURL)
+	if err != nil {
+		logger.Error("Error parsing plugin route url", "error", err)
 		return
 		return
 	}
 	}
 
 

+ 28 - 8
pkg/api/pluginproxy/ds_proxy_test.go

@@ -49,6 +49,13 @@ func TestDSRouteRule(t *testing.T) {
 							{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
 							{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
 						},
 						},
 					},
 					},
+					{
+						Path: "api/common",
+						Url:  "{{.JsonData.dynamicUrl}}",
+						Headers: []plugins.AppPluginRouteHeader{
+							{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
+						},
+					},
 				},
 				},
 			}
 			}
 
 
@@ -57,7 +64,8 @@ func TestDSRouteRule(t *testing.T) {
 
 
 			ds := &m.DataSource{
 			ds := &m.DataSource{
 				JsonData: simplejson.NewFromAny(map[string]interface{}{
 				JsonData: simplejson.NewFromAny(map[string]interface{}{
-					"clientId": "asd",
+					"clientId":   "asd",
+					"dynamicUrl": "https://dynamic.grafana.com",
 				}),
 				}),
 				SecureJsonData: map[string][]byte{
 				SecureJsonData: map[string][]byte{
 					"key": key,
 					"key": key,
@@ -83,6 +91,17 @@ func TestDSRouteRule(t *testing.T) {
 				})
 				})
 			})
 			})
 
 
+			Convey("When matching route path and has dynamic url", func() {
+				proxy := NewDataSourceProxy(ds, plugin, ctx, "api/common/some/method")
+				proxy.route = plugin.Routes[3]
+				proxy.applyRoute(req)
+
+				Convey("should add headers and interpolate the url", func() {
+					So(req.URL.String(), ShouldEqual, "https://dynamic.grafana.com/some/method")
+					So(req.Header.Get("x-header"), ShouldEqual, "my secret 123")
+				})
+			})
+
 			Convey("Validating request", func() {
 			Convey("Validating request", func() {
 				Convey("plugin route with valid role", func() {
 				Convey("plugin route with valid role", func() {
 					proxy := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method")
 					proxy := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method")
@@ -212,20 +231,21 @@ func TestDSRouteRule(t *testing.T) {
 		})
 		})
 
 
 		Convey("When proxying graphite", func() {
 		Convey("When proxying graphite", func() {
+			setting.BuildVersion = "5.3.0"
 			plugin := &plugins.DataSourcePlugin{}
 			plugin := &plugins.DataSourcePlugin{}
 			ds := &m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE}
 			ds := &m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE}
 			ctx := &m.ReqContext{}
 			ctx := &m.ReqContext{}
 
 
 			proxy := NewDataSourceProxy(ds, plugin, ctx, "/render")
 			proxy := NewDataSourceProxy(ds, plugin, ctx, "/render")
+			req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
+			So(err, ShouldBeNil)
 
 
-			requestURL, _ := url.Parse("http://grafana.com/sub")
-			req := http.Request{URL: requestURL}
-
-			proxy.getDirector()(&req)
+			proxy.getDirector()(req)
 
 
 			Convey("Can translate request url and path", func() {
 			Convey("Can translate request url and path", func() {
 				So(req.URL.Host, ShouldEqual, "graphite:8080")
 				So(req.URL.Host, ShouldEqual, "graphite:8080")
 				So(req.URL.Path, ShouldEqual, "/render")
 				So(req.URL.Path, ShouldEqual, "/render")
+				So(req.Header.Get("User-Agent"), ShouldEqual, "Grafana/5.3.0")
 			})
 			})
 		})
 		})
 
 
@@ -243,10 +263,10 @@ func TestDSRouteRule(t *testing.T) {
 			ctx := &m.ReqContext{}
 			ctx := &m.ReqContext{}
 			proxy := NewDataSourceProxy(ds, plugin, ctx, "")
 			proxy := NewDataSourceProxy(ds, plugin, ctx, "")
 
 
-			requestURL, _ := url.Parse("http://grafana.com/sub")
-			req := http.Request{URL: requestURL}
+			req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
+			So(err, ShouldBeNil)
 
 
-			proxy.getDirector()(&req)
+			proxy.getDirector()(req)
 
 
 			Convey("Should add db to url", func() {
 			Convey("Should add db to url", func() {
 				So(req.URL.Path, ShouldEqual, "/db/site/")
 				So(req.URL.Path, ShouldEqual, "/db/site/")

+ 15 - 0
pkg/api/user.go

@@ -111,6 +111,21 @@ func GetSignedInUserOrgList(c *m.ReqContext) Response {
 	return getUserOrgList(c.UserId)
 	return getUserOrgList(c.UserId)
 }
 }
 
 
+// GET /api/user/teams
+func GetSignedInUserTeamList(c *m.ReqContext) Response {
+	query := m.GetTeamsByUserQuery{OrgId: c.OrgId, UserId: c.UserId}
+
+	if err := bus.Dispatch(&query); err != nil {
+		return Error(500, "Failed to get user teams", err)
+	}
+
+	for _, team := range query.Result {
+		team.AvatarUrl = dtos.GetGravatarUrlWithDefault(team.Email, team.Name)
+	}
+
+	return JSON(200, query.Result)
+}
+
 // GET /api/user/:id/orgs
 // GET /api/user/:id/orgs
 func GetUserOrgList(c *m.ReqContext) Response {
 func GetUserOrgList(c *m.ReqContext) Response {
 	return getUserOrgList(c.ParamsInt64(":id"))
 	return getUserOrgList(c.ParamsInt64(":id"))

+ 12 - 3
pkg/components/imguploader/webdavuploader.go

@@ -9,6 +9,7 @@ import (
 	"net/http"
 	"net/http"
 	"net/url"
 	"net/url"
 	"path"
 	"path"
+	"strings"
 	"time"
 	"time"
 
 
 	"github.com/grafana/grafana/pkg/util"
 	"github.com/grafana/grafana/pkg/util"
@@ -35,6 +36,16 @@ var netClient = &http.Client{
 	Transport: netTransport,
 	Transport: netTransport,
 }
 }
 
 
+func (u *WebdavUploader) PublicURL(filename string) string {
+	if strings.Contains(u.public_url, "${file}") {
+		return strings.Replace(u.public_url, "${file}", filename, -1)
+	} else {
+		publicURL, _ := url.Parse(u.public_url)
+		publicURL.Path = path.Join(publicURL.Path, filename)
+		return publicURL.String()
+	}
+}
+
 func (u *WebdavUploader) Upload(ctx context.Context, pa string) (string, error) {
 func (u *WebdavUploader) Upload(ctx context.Context, pa string) (string, error) {
 	url, _ := url.Parse(u.url)
 	url, _ := url.Parse(u.url)
 	filename := util.GetRandomString(20) + ".png"
 	filename := util.GetRandomString(20) + ".png"
@@ -65,9 +76,7 @@ func (u *WebdavUploader) Upload(ctx context.Context, pa string) (string, error)
 	}
 	}
 
 
 	if u.public_url != "" {
 	if u.public_url != "" {
-		publicURL, _ := url.Parse(u.public_url)
-		publicURL.Path = path.Join(publicURL.Path, filename)
-		return publicURL.String(), nil
+		return u.PublicURL(filename), nil
 	}
 	}
 
 
 	return url.String(), nil
 	return url.String(), nil

+ 13 - 0
pkg/components/imguploader/webdavuploader_test.go

@@ -2,6 +2,7 @@ package imguploader
 
 
 import (
 import (
 	"context"
 	"context"
+	"net/url"
 	"testing"
 	"testing"
 
 
 	. "github.com/smartystreets/goconvey/convey"
 	. "github.com/smartystreets/goconvey/convey"
@@ -26,3 +27,15 @@ func TestUploadToWebdav(t *testing.T) {
 		So(path, ShouldStartWith, "http://publicurl:8888/webdav/")
 		So(path, ShouldStartWith, "http://publicurl:8888/webdav/")
 	})
 	})
 }
 }
+
+func TestPublicURL(t *testing.T) {
+	Convey("Given a public URL with parameters, and no template", t, func() {
+		webdavUploader, _ := NewWebdavImageUploader("http://localhost:8888/webdav/", "test", "test", "http://cloudycloud.me/s/DOIFDOMV/download?files=")
+		parsed, _ := url.Parse(webdavUploader.PublicURL("fileyfile.png"))
+		So(parsed.Path, ShouldEndWith, "fileyfile.png")
+	})
+	Convey("Given a public URL with parameters, and a template", t, func() {
+		webdavUploader, _ := NewWebdavImageUploader("http://localhost:8888/webdav/", "test", "test", "http://cloudycloud.me/s/DOIFDOMV/download?files=${file}")
+		So(webdavUploader.PublicURL("fileyfile.png"), ShouldEndWith, "fileyfile.png")
+	})
+}

+ 7 - 0
pkg/login/ext_user.go

@@ -72,6 +72,13 @@ func UpsertUser(cmd *m.UpsertUserCommand) error {
 		return err
 		return err
 	}
 	}
 
 
+	// Sync isGrafanaAdmin permission
+	if extUser.IsGrafanaAdmin != nil && *extUser.IsGrafanaAdmin != cmd.Result.IsAdmin {
+		if err := bus.Dispatch(&m.UpdateUserPermissionsCommand{UserId: cmd.Result.Id, IsGrafanaAdmin: *extUser.IsGrafanaAdmin}); err != nil {
+			return err
+		}
+	}
+
 	err = bus.Dispatch(&m.SyncTeamsCommand{
 	err = bus.Dispatch(&m.SyncTeamsCommand{
 		User:         cmd.Result,
 		User:         cmd.Result,
 		ExternalUser: extUser,
 		ExternalUser: extUser,

+ 14 - 3
pkg/login/ldap.go

@@ -59,6 +59,13 @@ func (a *ldapAuther) Dial() error {
 			}
 			}
 		}
 		}
 	}
 	}
+	var clientCert tls.Certificate
+	if a.server.ClientCert != "" && a.server.ClientKey != "" {
+		clientCert, err = tls.LoadX509KeyPair(a.server.ClientCert, a.server.ClientKey)
+		if err != nil {
+			return err
+		}
+	}
 	for _, host := range strings.Split(a.server.Host, " ") {
 	for _, host := range strings.Split(a.server.Host, " ") {
 		address := fmt.Sprintf("%s:%d", host, a.server.Port)
 		address := fmt.Sprintf("%s:%d", host, a.server.Port)
 		if a.server.UseSSL {
 		if a.server.UseSSL {
@@ -67,6 +74,9 @@ func (a *ldapAuther) Dial() error {
 				ServerName:         host,
 				ServerName:         host,
 				RootCAs:            certPool,
 				RootCAs:            certPool,
 			}
 			}
+			if len(clientCert.Certificate) > 0 {
+				tlsCfg.Certificates = append(tlsCfg.Certificates, clientCert)
+			}
 			if a.server.StartTLS {
 			if a.server.StartTLS {
 				a.conn, err = ldap.Dial("tcp", address)
 				a.conn, err = ldap.Dial("tcp", address)
 				if err == nil {
 				if err == nil {
@@ -175,6 +185,7 @@ func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo
 
 
 		if ldapUser.isMemberOf(group.GroupDN) {
 		if ldapUser.isMemberOf(group.GroupDN) {
 			extUser.OrgRoles[group.OrgId] = group.OrgRole
 			extUser.OrgRoles[group.OrgId] = group.OrgRole
+			extUser.IsGrafanaAdmin = group.IsGrafanaAdmin
 		}
 		}
 	}
 	}
 
 
@@ -190,18 +201,18 @@ func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo
 	}
 	}
 
 
 	// add/update user in grafana
 	// add/update user in grafana
-	userQuery := &m.UpsertUserCommand{
+	upsertUserCmd := &m.UpsertUserCommand{
 		ReqContext:    ctx,
 		ReqContext:    ctx,
 		ExternalUser:  extUser,
 		ExternalUser:  extUser,
 		SignupAllowed: setting.LdapAllowSignup,
 		SignupAllowed: setting.LdapAllowSignup,
 	}
 	}
 
 
-	err := bus.Dispatch(userQuery)
+	err := bus.Dispatch(upsertUserCmd)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	return userQuery.Result, nil
+	return upsertUserCmd.Result, nil
 }
 }
 
 
 func (a *ldapAuther) serverBind() error {
 func (a *ldapAuther) serverBind() error {

+ 6 - 3
pkg/login/ldap_settings.go

@@ -21,6 +21,8 @@ type LdapServerConf struct {
 	StartTLS      bool             `toml:"start_tls"`
 	StartTLS      bool             `toml:"start_tls"`
 	SkipVerifySSL bool             `toml:"ssl_skip_verify"`
 	SkipVerifySSL bool             `toml:"ssl_skip_verify"`
 	RootCACert    string           `toml:"root_ca_cert"`
 	RootCACert    string           `toml:"root_ca_cert"`
+	ClientCert    string           `toml:"client_cert"`
+	ClientKey     string           `toml:"client_key"`
 	BindDN        string           `toml:"bind_dn"`
 	BindDN        string           `toml:"bind_dn"`
 	BindPassword  string           `toml:"bind_password"`
 	BindPassword  string           `toml:"bind_password"`
 	Attr          LdapAttributeMap `toml:"attributes"`
 	Attr          LdapAttributeMap `toml:"attributes"`
@@ -44,9 +46,10 @@ type LdapAttributeMap struct {
 }
 }
 
 
 type LdapGroupToOrgRole struct {
 type LdapGroupToOrgRole struct {
-	GroupDN string     `toml:"group_dn"`
-	OrgId   int64      `toml:"org_id"`
-	OrgRole m.RoleType `toml:"org_role"`
+	GroupDN        string     `toml:"group_dn"`
+	OrgId          int64      `toml:"org_id"`
+	IsGrafanaAdmin *bool      `toml:"grafana_admin"` // This is a pointer to know if it was set or not (for backwards compatability)
+	OrgRole        m.RoleType `toml:"org_role"`
 }
 }
 
 
 var LdapCfg LdapConfig
 var LdapCfg LdapConfig

+ 42 - 8
pkg/login/ldap_test.go

@@ -98,6 +98,10 @@ func TestLdapAuther(t *testing.T) {
 				So(result.Login, ShouldEqual, "torkelo")
 				So(result.Login, ShouldEqual, "torkelo")
 			})
 			})
 
 
+			Convey("Should set isGrafanaAdmin to false by default", func() {
+				So(result.IsAdmin, ShouldBeFalse)
+			})
+
 		})
 		})
 
 
 	})
 	})
@@ -223,8 +227,32 @@ func TestLdapAuther(t *testing.T) {
 				So(sc.addOrgUserCmd.Role, ShouldEqual, m.ROLE_ADMIN)
 				So(sc.addOrgUserCmd.Role, ShouldEqual, m.ROLE_ADMIN)
 				So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
 				So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
 			})
 			})
+
+			Convey("Should not update permissions unless specified", func() {
+				So(err, ShouldBeNil)
+				So(sc.updateUserPermissionsCmd, ShouldBeNil)
+			})
 		})
 		})
 
 
+		ldapAutherScenario("given ldap groups with grafana_admin=true", func(sc *scenarioContext) {
+			trueVal := true
+
+			ldapAuther := NewLdapAuthenticator(&LdapServerConf{
+				LdapGroups: []*LdapGroupToOrgRole{
+					{GroupDN: "cn=admins", OrgId: 1, OrgRole: "Admin", IsGrafanaAdmin: &trueVal},
+				},
+			})
+
+			sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
+			_, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
+				MemberOf: []string{"cn=admins"},
+			})
+
+			Convey("Should create user with admin set to true", func() {
+				So(err, ShouldBeNil)
+				So(sc.updateUserPermissionsCmd.IsGrafanaAdmin, ShouldBeTrue)
+			})
+		})
 	})
 	})
 
 
 	Convey("When calling SyncUser", t, func() {
 	Convey("When calling SyncUser", t, func() {
@@ -332,6 +360,11 @@ func ldapAutherScenario(desc string, fn scenarioFunc) {
 			return nil
 			return nil
 		})
 		})
 
 
+		bus.AddHandlerCtx("test", func(ctx context.Context, cmd *m.UpdateUserPermissionsCommand) error {
+			sc.updateUserPermissionsCmd = cmd
+			return nil
+		})
+
 		bus.AddHandler("test", func(cmd *m.GetUserByAuthInfoQuery) error {
 		bus.AddHandler("test", func(cmd *m.GetUserByAuthInfoQuery) error {
 			sc.getUserByAuthInfoQuery = cmd
 			sc.getUserByAuthInfoQuery = cmd
 			sc.getUserByAuthInfoQuery.Result = &m.User{Login: cmd.Login}
 			sc.getUserByAuthInfoQuery.Result = &m.User{Login: cmd.Login}
@@ -379,14 +412,15 @@ func ldapAutherScenario(desc string, fn scenarioFunc) {
 }
 }
 
 
 type scenarioContext struct {
 type scenarioContext struct {
-	getUserByAuthInfoQuery *m.GetUserByAuthInfoQuery
-	getUserOrgListQuery    *m.GetUserOrgListQuery
-	createUserCmd          *m.CreateUserCommand
-	addOrgUserCmd          *m.AddOrgUserCommand
-	updateOrgUserCmd       *m.UpdateOrgUserCommand
-	removeOrgUserCmd       *m.RemoveOrgUserCommand
-	updateUserCmd          *m.UpdateUserCommand
-	setUsingOrgCmd         *m.SetUsingOrgCommand
+	getUserByAuthInfoQuery   *m.GetUserByAuthInfoQuery
+	getUserOrgListQuery      *m.GetUserOrgListQuery
+	createUserCmd            *m.CreateUserCommand
+	addOrgUserCmd            *m.AddOrgUserCommand
+	updateOrgUserCmd         *m.UpdateOrgUserCommand
+	removeOrgUserCmd         *m.RemoveOrgUserCommand
+	updateUserCmd            *m.UpdateUserCommand
+	setUsingOrgCmd           *m.SetUsingOrgCommand
+	updateUserPermissionsCmd *m.UpdateUserPermissionsCommand
 }
 }
 
 
 func (sc *scenarioContext) userQueryReturns(user *m.User) {
 func (sc *scenarioContext) userQueryReturns(user *m.User) {

+ 8 - 0
pkg/metrics/metrics.go

@@ -44,6 +44,7 @@ var (
 	M_Alerting_Notification_Sent         *prometheus.CounterVec
 	M_Alerting_Notification_Sent         *prometheus.CounterVec
 	M_Aws_CloudWatch_GetMetricStatistics prometheus.Counter
 	M_Aws_CloudWatch_GetMetricStatistics prometheus.Counter
 	M_Aws_CloudWatch_ListMetrics         prometheus.Counter
 	M_Aws_CloudWatch_ListMetrics         prometheus.Counter
+	M_Aws_CloudWatch_GetMetricData       prometheus.Counter
 	M_DB_DataSource_QueryById            prometheus.Counter
 	M_DB_DataSource_QueryById            prometheus.Counter
 
 
 	// Timers
 	// Timers
@@ -218,6 +219,12 @@ func init() {
 		Namespace: exporterName,
 		Namespace: exporterName,
 	})
 	})
 
 
+	M_Aws_CloudWatch_GetMetricData = prometheus.NewCounter(prometheus.CounterOpts{
+		Name:      "aws_cloudwatch_get_metric_data_total",
+		Help:      "counter for getting metric data time series from aws",
+		Namespace: exporterName,
+	})
+
 	M_DB_DataSource_QueryById = prometheus.NewCounter(prometheus.CounterOpts{
 	M_DB_DataSource_QueryById = prometheus.NewCounter(prometheus.CounterOpts{
 		Name:      "db_datasource_query_by_id_total",
 		Name:      "db_datasource_query_by_id_total",
 		Help:      "counter for getting datasource by id",
 		Help:      "counter for getting datasource by id",
@@ -307,6 +314,7 @@ func initMetricVars() {
 		M_Alerting_Notification_Sent,
 		M_Alerting_Notification_Sent,
 		M_Aws_CloudWatch_GetMetricStatistics,
 		M_Aws_CloudWatch_GetMetricStatistics,
 		M_Aws_CloudWatch_ListMetrics,
 		M_Aws_CloudWatch_ListMetrics,
+		M_Aws_CloudWatch_GetMetricData,
 		M_DB_DataSource_QueryById,
 		M_DB_DataSource_QueryById,
 		M_Alerting_Active_Alerts,
 		M_Alerting_Active_Alerts,
 		M_StatTotal_Dashboards,
 		M_StatTotal_Dashboards,

+ 1 - 0
pkg/models/models.go

@@ -8,4 +8,5 @@ const (
 	TWITTER
 	TWITTER
 	GENERIC
 	GENERIC
 	GRAFANA_COM
 	GRAFANA_COM
+	GITLAB
 )
 )

+ 1 - 1
pkg/models/playlist.go

@@ -63,7 +63,7 @@ type PlaylistDashboards []*PlaylistDashboard
 
 
 type UpdatePlaylistCommand struct {
 type UpdatePlaylistCommand struct {
 	OrgId    int64             `json:"-"`
 	OrgId    int64             `json:"-"`
-	Id       int64             `json:"id" binding:"Required"`
+	Id       int64             `json:"id"`
 	Name     string            `json:"name" binding:"Required"`
 	Name     string            `json:"name" binding:"Required"`
 	Interval string            `json:"interval"`
 	Interval string            `json:"interval"`
 	Items    []PlaylistItemDTO `json:"items"`
 	Items    []PlaylistItemDTO `json:"items"`

+ 9 - 8
pkg/models/user_auth.go

@@ -13,14 +13,15 @@ type UserAuth struct {
 }
 }
 
 
 type ExternalUserInfo struct {
 type ExternalUserInfo struct {
-	AuthModule string
-	AuthId     string
-	UserId     int64
-	Email      string
-	Login      string
-	Name       string
-	Groups     []string
-	OrgRoles   map[int64]RoleType
+	AuthModule     string
+	AuthId         string
+	UserId         int64
+	Email          string
+	Login          string
+	Name           string
+	Groups         []string
+	OrgRoles       map[int64]RoleType
+	IsGrafanaAdmin *bool // This is a pointer to know if we should sync this or not (nil = ignore sync)
 }
 }
 
 
 // ---------------------
 // ---------------------

+ 3 - 0
pkg/plugins/datasource_plugin.go

@@ -17,11 +17,14 @@ import (
 	plugin "github.com/hashicorp/go-plugin"
 	plugin "github.com/hashicorp/go-plugin"
 )
 )
 
 
+// DataSourcePlugin contains all metadata about a datasource plugin
 type DataSourcePlugin struct {
 type DataSourcePlugin struct {
 	FrontendPluginBase
 	FrontendPluginBase
 	Annotations  bool              `json:"annotations"`
 	Annotations  bool              `json:"annotations"`
 	Metrics      bool              `json:"metrics"`
 	Metrics      bool              `json:"metrics"`
 	Alerting     bool              `json:"alerting"`
 	Alerting     bool              `json:"alerting"`
+	Explore      bool              `json:"explore"`
+	Logs         bool              `json:"logs"`
 	QueryOptions map[string]bool   `json:"queryOptions,omitempty"`
 	QueryOptions map[string]bool   `json:"queryOptions,omitempty"`
 	BuiltIn      bool              `json:"builtIn,omitempty"`
 	BuiltIn      bool              `json:"builtIn,omitempty"`
 	Mixed        bool              `json:"mixed,omitempty"`
 	Mixed        bool              `json:"mixed,omitempty"`

+ 1 - 2
pkg/services/alerting/notifier.go

@@ -3,7 +3,6 @@ package alerting
 import (
 import (
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
-	"time"
 
 
 	"golang.org/x/sync/errgroup"
 	"golang.org/x/sync/errgroup"
 
 
@@ -81,7 +80,7 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
 	renderOpts := rendering.Opts{
 	renderOpts := rendering.Opts{
 		Width:   1000,
 		Width:   1000,
 		Height:  500,
 		Height:  500,
-		Timeout: time.Second * 30,
+		Timeout: alertTimeout / 2,
 		OrgId:   context.Rule.OrgId,
 		OrgId:   context.Rule.OrgId,
 		OrgRole: m.ROLE_ADMIN,
 		OrgRole: m.ROLE_ADMIN,
 	}
 	}

+ 1 - 1
pkg/services/alerting/notifiers/slack.go

@@ -58,7 +58,7 @@ func init() {
           data-placement="right">
           data-placement="right">
         </input>
         </input>
         <info-popover mode="right-absolute">
         <info-popover mode="right-absolute">
-          Provide a bot token to use the Slack file.upload API (starts with "xoxb")
+          Provide a bot token to use the Slack file.upload API (starts with "xoxb"). Specify #channel-name or @username in Recipient for this to work 
         </info-popover>
         </info-popover>
       </div>
       </div>
     `,
     `,

+ 3 - 3
pkg/services/provisioning/datasources/config_reader.go

@@ -83,7 +83,7 @@ func (cr *configReader) parseDatasourceConfig(path string, file os.FileInfo) (*D
 }
 }
 
 
 func validateDefaultUniqueness(datasources []*DatasourcesAsConfig) error {
 func validateDefaultUniqueness(datasources []*DatasourcesAsConfig) error {
-	defaultCount := 0
+	defaultCount := map[int64]int{}
 	for i := range datasources {
 	for i := range datasources {
 		if datasources[i].Datasources == nil {
 		if datasources[i].Datasources == nil {
 			continue
 			continue
@@ -95,8 +95,8 @@ func validateDefaultUniqueness(datasources []*DatasourcesAsConfig) error {
 			}
 			}
 
 
 			if ds.IsDefault {
 			if ds.IsDefault {
-				defaultCount++
-				if defaultCount > 1 {
+				defaultCount[ds.OrgId] = defaultCount[ds.OrgId] + 1
+				if defaultCount[ds.OrgId] > 1 {
 					return ErrInvalidConfigToManyDefault
 					return ErrInvalidConfigToManyDefault
 				}
 				}
 			}
 			}

+ 14 - 0
pkg/services/provisioning/datasources/config_reader_test.go

@@ -19,6 +19,7 @@ var (
 	allProperties                   = "testdata/all-properties"
 	allProperties                   = "testdata/all-properties"
 	versionZero                     = "testdata/version-0"
 	versionZero                     = "testdata/version-0"
 	brokenYaml                      = "testdata/broken-yaml"
 	brokenYaml                      = "testdata/broken-yaml"
+	multipleOrgsWithDefault         = "testdata/multiple-org-default"
 
 
 	fakeRepo *fakeRepository
 	fakeRepo *fakeRepository
 )
 )
@@ -73,6 +74,19 @@ func TestDatasourceAsConfig(t *testing.T) {
 			})
 			})
 		})
 		})
 
 
+		Convey("Multiple datasources in different organizations with isDefault in each organization", func() {
+			dc := newDatasourceProvisioner(logger)
+			err := dc.applyChanges(multipleOrgsWithDefault)
+			Convey("should not raise error", func() {
+				So(err, ShouldBeNil)
+				So(len(fakeRepo.inserted), ShouldEqual, 4)
+				So(fakeRepo.inserted[0].IsDefault, ShouldBeTrue)
+				So(fakeRepo.inserted[0].OrgId, ShouldEqual, 1)
+				So(fakeRepo.inserted[2].IsDefault, ShouldBeTrue)
+				So(fakeRepo.inserted[2].OrgId, ShouldEqual, 2)
+			})
+		})
+
 		Convey("Two configured datasource and purge others ", func() {
 		Convey("Two configured datasource and purge others ", func() {
 			Convey("two other datasources in database", func() {
 			Convey("two other datasources in database", func() {
 				fakeRepo.loadAll = []*models.DataSource{
 				fakeRepo.loadAll = []*models.DataSource{

+ 1 - 1
pkg/services/provisioning/datasources/datasources.go

@@ -11,7 +11,7 @@ import (
 )
 )
 
 
 var (
 var (
-	ErrInvalidConfigToManyDefault = errors.New("datasource.yaml config is invalid. Only one datasource can be marked as default")
+	ErrInvalidConfigToManyDefault = errors.New("datasource.yaml config is invalid. Only one datasource per organization can be marked as default")
 )
 )
 
 
 func Provision(configDirectory string) error {
 func Provision(configDirectory string) error {

+ 25 - 0
pkg/services/provisioning/datasources/testdata/multiple-org-default/config.yaml

@@ -0,0 +1,25 @@
+apiVersion: 1
+
+datasources:
+  - orgId: 1
+    name: prometheus
+    type: prometheus
+    isDefault: True
+    access: proxy
+    url: http://prometheus.example.com:9090
+  - name: Graphite
+    type: graphite
+    access: proxy
+    url: http://localhost:8080
+  - orgId: 2
+    name: prometheus
+    type: prometheus
+    isDefault: True
+    access: proxy
+    url: http://prometheus.example.com:9090
+  - orgId: 2
+    name: Graphite
+    type: graphite
+    access: proxy
+    url: http://localhost:8080
+

+ 1 - 0
pkg/services/sqlstore/alert.go

@@ -73,6 +73,7 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error {
 		alert.name,
 		alert.name,
 		alert.state,
 		alert.state,
 		alert.new_state_date,
 		alert.new_state_date,
+		alert.eval_data,
 		alert.eval_date,
 		alert.eval_date,
 		alert.execution_error,
 		alert.execution_error,
 		dashboard.uid as dashboard_uid,
 		dashboard.uid as dashboard_uid,

+ 13 - 2
pkg/services/sqlstore/alert_test.go

@@ -13,7 +13,7 @@ func mockTimeNow() {
 	var timeSeed int64
 	var timeSeed int64
 	timeNow = func() time.Time {
 	timeNow = func() time.Time {
 		fakeNow := time.Unix(timeSeed, 0)
 		fakeNow := time.Unix(timeSeed, 0)
-		timeSeed += 1
+		timeSeed++
 		return fakeNow
 		return fakeNow
 	}
 	}
 }
 }
@@ -30,7 +30,7 @@ func TestAlertingDataAccess(t *testing.T) {
 		InitTestDB(t)
 		InitTestDB(t)
 
 
 		testDash := insertTestDashboard("dashboard with alerts", 1, 0, false, "alert")
 		testDash := insertTestDashboard("dashboard with alerts", 1, 0, false, "alert")
-
+		evalData, _ := simplejson.NewJson([]byte(`{"test": "test"}`))
 		items := []*m.Alert{
 		items := []*m.Alert{
 			{
 			{
 				PanelId:     1,
 				PanelId:     1,
@@ -40,6 +40,7 @@ func TestAlertingDataAccess(t *testing.T) {
 				Message:     "Alerting message",
 				Message:     "Alerting message",
 				Settings:    simplejson.New(),
 				Settings:    simplejson.New(),
 				Frequency:   1,
 				Frequency:   1,
+				EvalData:    evalData,
 			},
 			},
 		}
 		}
 
 
@@ -104,8 +105,18 @@ func TestAlertingDataAccess(t *testing.T) {
 
 
 			alert := alertQuery.Result[0]
 			alert := alertQuery.Result[0]
 			So(err2, ShouldBeNil)
 			So(err2, ShouldBeNil)
+			So(alert.Id, ShouldBeGreaterThan, 0)
+			So(alert.DashboardId, ShouldEqual, testDash.Id)
+			So(alert.PanelId, ShouldEqual, 1)
 			So(alert.Name, ShouldEqual, "Alerting title")
 			So(alert.Name, ShouldEqual, "Alerting title")
 			So(alert.State, ShouldEqual, "pending")
 			So(alert.State, ShouldEqual, "pending")
+			So(alert.NewStateDate, ShouldNotBeNil)
+			So(alert.EvalData, ShouldNotBeNil)
+			So(alert.EvalData.Get("test").MustString(), ShouldEqual, "test")
+			So(alert.EvalDate, ShouldNotBeNil)
+			So(alert.ExecutionError, ShouldEqual, "")
+			So(alert.DashboardUid, ShouldNotBeNil)
+			So(alert.DashboardSlug, ShouldEqual, "dashboard-with-alerts")
 		})
 		})
 
 
 		Convey("Viewer cannot read alerts", func() {
 		Convey("Viewer cannot read alerts", func() {

+ 2 - 1
pkg/services/sqlstore/dashboard_test.go

@@ -181,7 +181,7 @@ func TestDashboardDataAccess(t *testing.T) {
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
 				So(query.Result.FolderId, ShouldEqual, 0)
 				So(query.Result.FolderId, ShouldEqual, 0)
 				So(query.Result.CreatedBy, ShouldEqual, savedDash.CreatedBy)
 				So(query.Result.CreatedBy, ShouldEqual, savedDash.CreatedBy)
-				So(query.Result.Created, ShouldEqual, savedDash.Created.Truncate(time.Second))
+				So(query.Result.Created, ShouldHappenWithin, 3*time.Second, savedDash.Created)
 				So(query.Result.UpdatedBy, ShouldEqual, 100)
 				So(query.Result.UpdatedBy, ShouldEqual, 100)
 				So(query.Result.Updated.IsZero(), ShouldBeFalse)
 				So(query.Result.Updated.IsZero(), ShouldBeFalse)
 			})
 			})
@@ -387,6 +387,7 @@ func insertTestDashboardForPlugin(title string, orgId int64, folderId int64, isF
 
 
 func createUser(name string, role string, isAdmin bool) m.User {
 func createUser(name string, role string, isAdmin bool) m.User {
 	setting.AutoAssignOrg = true
 	setting.AutoAssignOrg = true
+	setting.AutoAssignOrgId = 1
 	setting.AutoAssignOrgRole = role
 	setting.AutoAssignOrgRole = role
 
 
 	currentUserCmd := m.CreateUserCommand{Login: name, Email: name + "@test.com", Name: "a " + name, IsAdmin: isAdmin}
 	currentUserCmd := m.CreateUserCommand{Login: name, Email: name + "@test.com", Name: "a " + name, IsAdmin: isAdmin}

+ 40 - 1
pkg/services/sqlstore/migrations/user_mig.go

@@ -1,6 +1,12 @@
 package migrations
 package migrations
 
 
-import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
+import (
+	"fmt"
+
+	"github.com/go-xorm/xorm"
+	. "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
+	"github.com/grafana/grafana/pkg/util"
+)
 
 
 func addUserMigrations(mg *Migrator) {
 func addUserMigrations(mg *Migrator) {
 	userV1 := Table{
 	userV1 := Table{
@@ -107,4 +113,37 @@ func addUserMigrations(mg *Migrator) {
 	mg.AddMigration("Add last_seen_at column to user", NewAddColumnMigration(userV2, &Column{
 	mg.AddMigration("Add last_seen_at column to user", NewAddColumnMigration(userV2, &Column{
 		Name: "last_seen_at", Type: DB_DateTime, Nullable: true,
 		Name: "last_seen_at", Type: DB_DateTime, Nullable: true,
 	}))
 	}))
+
+	// Adds salt & rands for old users who used ldap or oauth
+	mg.AddMigration("Add missing user data", &AddMissingUserSaltAndRandsMigration{})
+}
+
+type AddMissingUserSaltAndRandsMigration struct {
+	MigrationBase
+}
+
+func (m *AddMissingUserSaltAndRandsMigration) Sql(dialect Dialect) string {
+	return "code migration"
+}
+
+type TempUserDTO struct {
+	Id    int64
+	Login string
+}
+
+func (m *AddMissingUserSaltAndRandsMigration) Exec(sess *xorm.Session, mg *Migrator) error {
+	users := make([]*TempUserDTO, 0)
+
+	err := sess.Sql(fmt.Sprintf("SELECT id, login from %s WHERE rands = ''", mg.Dialect.Quote("user"))).Find(&users)
+	if err != nil {
+		return err
+	}
+
+	for _, user := range users {
+		_, err := sess.Exec("UPDATE "+mg.Dialect.Quote("user")+" SET salt = ?, rands = ? WHERE id = ?", util.GetRandomString(10), util.GetRandomString(10), user.Id)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
 }
 }

+ 11 - 5
pkg/services/sqlstore/migrator/migrator.go

@@ -12,7 +12,7 @@ import (
 
 
 type Migrator struct {
 type Migrator struct {
 	x          *xorm.Engine
 	x          *xorm.Engine
-	dialect    Dialect
+	Dialect    Dialect
 	migrations []Migration
 	migrations []Migration
 	Logger     log.Logger
 	Logger     log.Logger
 }
 }
@@ -31,7 +31,7 @@ func NewMigrator(engine *xorm.Engine) *Migrator {
 	mg.x = engine
 	mg.x = engine
 	mg.Logger = log.New("migrator")
 	mg.Logger = log.New("migrator")
 	mg.migrations = make([]Migration, 0)
 	mg.migrations = make([]Migration, 0)
-	mg.dialect = NewDialect(mg.x)
+	mg.Dialect = NewDialect(mg.x)
 	return mg
 	return mg
 }
 }
 
 
@@ -86,7 +86,7 @@ func (mg *Migrator) Start() error {
 			continue
 			continue
 		}
 		}
 
 
-		sql := m.Sql(mg.dialect)
+		sql := m.Sql(mg.Dialect)
 
 
 		record := MigrationLog{
 		record := MigrationLog{
 			MigrationId: m.Id(),
 			MigrationId: m.Id(),
@@ -122,7 +122,7 @@ func (mg *Migrator) exec(m Migration, sess *xorm.Session) error {
 
 
 	condition := m.GetCondition()
 	condition := m.GetCondition()
 	if condition != nil {
 	if condition != nil {
-		sql, args := condition.Sql(mg.dialect)
+		sql, args := condition.Sql(mg.Dialect)
 		results, err := sess.SQL(sql).Query(args...)
 		results, err := sess.SQL(sql).Query(args...)
 		if err != nil || len(results) == 0 {
 		if err != nil || len(results) == 0 {
 			mg.Logger.Debug("Skipping migration condition not fulfilled", "id", m.Id())
 			mg.Logger.Debug("Skipping migration condition not fulfilled", "id", m.Id())
@@ -130,7 +130,13 @@ func (mg *Migrator) exec(m Migration, sess *xorm.Session) error {
 		}
 		}
 	}
 	}
 
 
-	_, err := sess.Exec(m.Sql(mg.dialect))
+	var err error
+	if codeMigration, ok := m.(CodeMigration); ok {
+		err = codeMigration.Exec(sess, mg)
+	} else {
+		_, err = sess.Exec(m.Sql(mg.Dialect))
+	}
+
 	if err != nil {
 	if err != nil {
 		mg.Logger.Error("Executing migration failed", "id", m.Id(), "error", err)
 		mg.Logger.Error("Executing migration failed", "id", m.Id(), "error", err)
 		return err
 		return err

+ 7 - 0
pkg/services/sqlstore/migrator/types.go

@@ -3,6 +3,8 @@ package migrator
 import (
 import (
 	"fmt"
 	"fmt"
 	"strings"
 	"strings"
+
+	"github.com/go-xorm/xorm"
 )
 )
 
 
 const (
 const (
@@ -19,6 +21,11 @@ type Migration interface {
 	GetCondition() MigrationCondition
 	GetCondition() MigrationCondition
 }
 }
 
 
+type CodeMigration interface {
+	Migration
+	Exec(sess *xorm.Session, migrator *Migrator) error
+}
+
 type SQLType string
 type SQLType string
 
 
 type ColumnType string
 type ColumnType string

部分文件因为文件数量过多而无法显示