瀏覽代碼

Merge branch 'data-source-settings-to-react' of github.com:grafana/grafana into data-source-settings-to-react

Torkel Ödegaard 7 年之前
父節點
當前提交
8ff6bb07bc
共有 100 個文件被更改,包括 1429 次插入428 次删除
  1. 56 20
      .circleci/config.yml
  2. 2 0
      .gitignore
  3. 15 1
      CHANGELOG.md
  4. 5 0
      Gruntfile.js
  5. 8 2
      Makefile
  6. 33 8
      build.go
  7. 4 0
      conf/defaults.ini
  8. 5 0
      conf/sample.ini
  9. 123 6
      devenv/dev-dashboards/panel_tests_graph.json
  10. 108 2
      devenv/dev-dashboards/panel_tests_table.json
  11. 1 1
      docs/sources/administration/provisioning.md
  12. 1 1
      docs/sources/alerting/notifications.md
  13. 2 2
      docs/sources/auth/gitlab.md
  14. 2 1
      docs/sources/features/datasources/cloudwatch.md
  15. 52 0
      docs/sources/features/datasources/mysql.md
  16. 3 3
      docs/sources/guides/whats-new-in-v5-3.md
  17. 1 1
      docs/sources/http_api/alerting.md
  18. 2 1
      package.json
  19. 12 0
      packaging/docker/build-enterprise.sh
  20. 0 29
      packaging/release_process.md
  21. 4 0
      pkg/api/alerting.go
  22. 5 45
      pkg/api/dataproxy.go
  23. 1 0
      pkg/api/dtos/index.go
  24. 10 8
      pkg/api/http_server.go
  25. 12 0
      pkg/api/index.go
  26. 4 1
      pkg/api/metrics.go
  27. 8 8
      pkg/cmd/grafana-server/main.go
  28. 6 6
      pkg/cmd/grafana-server/server.go
  29. 1 0
      pkg/login/auth.go
  30. 1 1
      pkg/login/ldap.go
  31. 27 3
      pkg/metrics/metrics.go
  32. 14 0
      pkg/middleware/headers.go
  33. 8 6
      pkg/middleware/middleware.go
  34. 1 0
      pkg/middleware/middleware_test.go
  35. 1 1
      pkg/middleware/recovery.go
  36. 4 0
      pkg/middleware/recovery_test.go
  37. 2 1
      pkg/models/alert.go
  38. 2 1
      pkg/models/context.go
  39. 0 5
      pkg/models/datasource.go
  40. 1 0
      pkg/models/user.go
  41. 0 1
      pkg/plugins/plugins.go
  42. 34 3
      pkg/registry/registry.go
  43. 3 3
      pkg/services/alerting/commands.go
  44. 18 0
      pkg/services/alerting/conditions/reducer_test.go
  45. 18 1
      pkg/services/alerting/extractor.go
  46. 8 8
      pkg/services/alerting/extractor_test.go
  47. 3 0
      pkg/services/alerting/notifiers/dingding.go
  48. 1 1
      pkg/services/alerting/notifiers/telegram.go
  49. 8 8
      pkg/services/alerting/notifiers/telegram_test.go
  50. 2 5
      pkg/services/alerting/reader.go
  51. 2 1
      pkg/services/alerting/test_rule.go
  52. 17 0
      pkg/services/cache/cache.go
  53. 2 1
      pkg/services/dashboards/dashboard_service.go
  54. 53 0
      pkg/services/datasources/cache.go
  55. 18 4
      pkg/services/sqlstore/dashboard.go
  56. 8 3
      pkg/services/sqlstore/sqlstore.go
  57. 30 3
      pkg/services/sqlstore/user.go
  58. 22 16
      pkg/setting/setting.go
  59. 13 2
      pkg/tsdb/cloudwatch/metric_find_query.go
  60. 32 1
      pkg/tsdb/cloudwatch/metric_find_query_test.go
  61. 2 4
      pkg/tsdb/graphite/graphite.go
  62. 1 1
      pkg/tsdb/mysql/macros.go
  63. 3 3
      pkg/tsdb/mysql/macros_test.go
  64. 1 2
      pkg/tsdb/stackdriver/stackdriver.go
  65. 28 0
      public/app/core/actions/appNotification.ts
  66. 2 1
      public/app/core/actions/index.ts
  67. 28 0
      public/app/core/actions/user.ts
  68. 2 0
      public/app/core/angular_wrappers.ts
  69. 12 5
      public/app/core/components/Animations/SlideDown.tsx
  70. 38 0
      public/app/core/components/AppNotifications/AppNotificationItem.tsx
  71. 60 0
      public/app/core/components/AppNotifications/AppNotificationList.tsx
  72. 1 0
      public/app/core/components/EmptyListCTA/EmptyListCTA.test.tsx
  73. 2 1
      public/app/core/components/EmptyListCTA/EmptyListCTA.tsx
  74. 1 0
      public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.test.tsx.snap
  75. 0 7
      public/app/core/components/InfoPopover/InfoPopover.tsx
  76. 2 1
      public/app/core/components/Label/Label.tsx
  77. 46 0
      public/app/core/components/Picker/SimplePicker.tsx
  78. 1 4
      public/app/core/components/Switch/Switch.tsx
  79. 16 25
      public/app/core/components/Tooltip/Tooltip.tsx
  80. 64 33
      public/app/core/components/colorpicker/SeriesColorPicker.tsx
  81. 70 0
      public/app/core/components/colorpicker/SeriesColorPickerPopover.tsx
  82. 1 1
      public/app/core/components/form_dropdown/form_dropdown.ts
  83. 46 0
      public/app/core/copy/appNotification.ts
  84. 1 1
      public/app/core/core.ts
  85. 14 0
      public/app/core/logs_model.ts
  86. 51 0
      public/app/core/reducers/appNotification.test.ts
  87. 19 0
      public/app/core/reducers/appNotification.ts
  88. 4 0
      public/app/core/reducers/index.ts
  89. 15 0
      public/app/core/reducers/user.ts
  90. 4 0
      public/app/core/services/AngularLoader.ts
  91. 4 92
      public/app/core/services/alert_srv.ts
  92. 7 6
      public/app/core/services/backend_srv.ts
  93. 1 1
      public/app/core/services/bridge_srv.ts
  94. 1 1
      public/app/core/specs/backend_srv.test.ts
  95. 4 0
      public/app/core/table_model.ts
  96. 11 0
      public/app/core/utils/connectWithReduxStore.tsx
  97. 4 1
      public/app/core/utils/rangeutil.ts
  98. 2 2
      public/app/features/alerting/AlertTabCtrl.ts
  99. 3 0
      public/app/features/all.ts
  100. 18 11
      public/app/features/annotations/annotations_srv.ts

+ 56 - 20
.circleci/config.yml

@@ -206,6 +206,10 @@ jobs:
       - run: docker info
       - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
       - run: cd packaging/docker && ./build-deploy.sh "master-${CIRCLE_SHA1}"
+      - run: rm packaging/docker/grafana-latest.linux-x64.tar.gz
+      - run: cp enterprise-dist/grafana-enterprise-*.linux-amd64.tar.gz packaging/docker/grafana-latest.linux-x64.tar.gz
+      - run: cd packaging/docker && ./build-enterprise.sh "master"
+
 
   grafana-docker-pr:
     docker:
@@ -230,6 +234,9 @@ jobs:
         - run: docker info
         - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
         - run: cd packaging/docker && ./build-deploy.sh "${CIRCLE_TAG}"
+        - run: rm packaging/docker/grafana-latest.linux-x64.tar.gz
+        - run: cp enterprise-dist/grafana-enterprise-*.linux-amd64.tar.gz packaging/docker/grafana-latest.linux-x64.tar.gz
+        - run: cd packaging/docker && ./build-enterprise.sh "${CIRCLE_TAG}"
 
   build-enterprise:
     docker:
@@ -312,39 +319,52 @@ jobs:
 
   deploy-enterprise-master:
     docker:
-      - image: circleci/python:2.7-stretch
+      - image: grafana/grafana-ci-deploy:1.0.0
     steps:
       - attach_workspace:
           at: .
       - run:
-          name: install awscli
-          command: 'sudo pip install awscli'
+          name: gcp credentials
+          command: 'echo ${GCP_GRAFANA_UPLOAD_KEY} > /tmp/gcpkey.json'
+      - run:
+          name: sign in to gcp
+          command: '/opt/google-cloud-sdk/bin/gcloud auth activate-service-account --key-file=/tmp/gcpkey.json'
       - run:
           name: deploy to s3
           command: 'aws s3 sync ./enterprise-dist s3://$ENTERPRISE_BUCKET_NAME/master'
+      - run:
+          name: deploy to gcp
+          command: '/opt/google-cloud-sdk/bin/gsutil cp ./enterprise-dist/* gs://$GCP_BUCKET_NAME/enterprise/master'
+      - run:
+          name: Deploy to grafana.com
+          command: 'cd enterprise-dist && ../scripts/build/release_publisher/release_publisher -apikey ${GRAFANA_COM_API_KEY} -enterprise -from-local'
+
 
   deploy-enterprise-release:
     docker:
-    - image: circleci/python:2.7-stretch
+    - image: grafana/grafana-ci-deploy:1.0.0
     steps:
-    - attach_workspace:
-        at: .
-    - run:
-        name: install awscli
-        command: 'sudo pip install awscli'
-    - run:
-        name: deploy to s3
-        command: 'aws s3 sync ./enterprise-dist s3://$ENTERPRISE_BUCKET_NAME/release'
+      - attach_workspace:
+         at: .
+      - run:
+          name: gcp credentials
+          command: 'echo ${GCP_GRAFANA_UPLOAD_KEY} > /tmp/gcpkey.json'
+      - run:
+          name: sign in to gcp
+          command: '/opt/google-cloud-sdk/bin/gcloud auth activate-service-account --key-file=/tmp/gcpkey.json'
+      - run:
+          name: deploy to s3
+          command: 'aws s3 sync ./enterprise-dist s3://$ENTERPRISE_BUCKET_NAME/release'
+      - run:
+          name: deploy to gcp
+          command: '/opt/google-cloud-sdk/bin/gsutil cp ./enterprise-dist/* gs://$GCP_BUCKET_NAME/enterprise/release'
 
   deploy-master:
     docker:
-      - image: circleci/python:2.7-stretch
+      - image: grafana/grafana-ci-deploy:1.0.0
     steps:
       - attach_workspace:
           at: .
-      - run:
-          name: install awscli
-          command: 'sudo pip install awscli'
       - run:
           name: deploy to s3
           command: |
@@ -354,6 +374,15 @@ jobs:
       - run:
           name: Trigger Windows build
           command: './scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} ${CIRCLE_SHA1} master'
+      - run:
+          name: gcp credentials
+          command: 'echo ${GCP_GRAFANA_UPLOAD_KEY} > /tmp/gcpkey.json'
+      - run:
+          name: sign in to gcp
+          command: '/opt/google-cloud-sdk/bin/gcloud auth activate-service-account --key-file=/tmp/gcpkey.json'
+      - run:
+          name: deploy to gcp
+          command: '/opt/google-cloud-sdk/bin/gsutil cp ./dist/* gs://$GCP_BUCKET_NAME/oss/master'
       - run:
           name: Publish to Grafana.com
           command: |
@@ -362,16 +391,22 @@ jobs:
 
   deploy-release:
     docker:
-      - image: circleci/python:2.7-stretch
+      - image: grafana/grafana-ci-deploy:1.0.0
     steps:
       - attach_workspace:
           at: .
-      - run:
-          name: install awscli
-          command: 'sudo pip install awscli'
       - run:
           name: deploy to s3
           command: 'aws s3 sync ./dist s3://$BUCKET_NAME/release'
+      - run:
+          name: gcp credentials
+          command: 'echo ${GCP_GRAFANA_UPLOAD_KEY} > /tmp/gcpkey.json'
+      - run:
+          name: sign in to gcp
+          command: '/opt/google-cloud-sdk/bin/gcloud auth activate-service-account --key-file=/tmp/gcpkey.json'
+      - run:
+          name: deploy to gcp
+          command: '/opt/google-cloud-sdk/bin/gsutil cp ./dist/* gs://$GCP_BUCKET_NAME/oss/release'
       - run:
           name: Deploy to Grafana.com
           command: './scripts/build/publish.sh'
@@ -409,6 +444,7 @@ workflows:
       - grafana-docker-master:
           requires:
             - build-all
+            - build-all-enterprise
             - test-backend
             - test-frontend
             - codespell

+ 2 - 0
.gitignore

@@ -8,6 +8,7 @@ awsconfig
 /dist
 /public/build
 /public/views/index.html
+/public/views/error.html
 /emails/dist
 /public_gen
 /public/vendor/npm
@@ -75,3 +76,4 @@ debug.test
 /devenv/bulk_alerting_dashboards/*.json
 
 /scripts/build/release_publisher/release_publisher
+*.patch

+ 15 - 1
CHANGELOG.md

@@ -4,20 +4,34 @@
 
 * **Alerting**: Option to disable OK alert notifications [#12330](https://github.com/grafana/grafana/issues/12330) & [#6696](https://github.com/grafana/grafana/issues/6696), thx [@davewat](https://github.com/davewat)
 * **Postgres/MySQL/MSSQL**: Adds support for configuration of max open/idle connections and connection max lifetime. Also, panels with multiple SQL queries will now be executed concurrently [#11711](https://github.com/grafana/grafana/issues/11711), thx [@connection-reset](https://github.com/connection-reset)
-* **MSSQL**: Add encrypt setting to allow configuration of how data sent between client and server are encrypted [#13629](https://github.com/grafana/grafana/issues/13629), thx [@ramiro](https://github.com/ramiro)
+* **MySQL**: Graphical query builder [#13762](https://github.com/grafana/grafana/issues/13762), thx [svenklemm](https://github.com/svenklemm)
 * **MySQL**: Support connecting thru Unix socket for MySQL datasource [#12342](https://github.com/grafana/grafana/issues/12342), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino)
+* **MSSQL**: Add encrypt setting to allow configuration of how data sent between client and server are encrypted [#13629](https://github.com/grafana/grafana/issues/13629), thx [@ramiro](https://github.com/ramiro)
 * **Stackdriver**: Not possible to authenticate using GCE metadata server [#13669](https://github.com/grafana/grafana/issues/13669)
 
 ### Minor
 
 * **Cloudwatch**: Show all available CloudWatch regions [#12308](https://github.com/grafana/grafana/issues/12308), thx [@mtanda](https://github.com/mtanda)
+* **Cloudwatch**: AWS/Connect metrics and dimensions [#13970](https://github.com/grafana/grafana/pull/13970), thx [@zcoffy](https://github.com/zcoffy)
+* **Postgres**: Add delta window function to postgres query builder [#13925](https://github.com/grafana/grafana/issues/13925), thx [svenklemm](https://github.com/svenklemm)
 * **Units**: New clock time format, to format ms or second values as for example `01h:59m`, [#13635](https://github.com/grafana/grafana/issues/13635), thx [@franciscocpg](https://github.com/franciscocpg)
 * **Datasource Proxy**: Keep trailing slash for datasource proxy requests [#13326](https://github.com/grafana/grafana/pull/13326), thx [@ryantxu](https://github.com/ryantxu)
+* **DingDing**: Can't receive DingDing alert when alert is triggered [#13723](https://github.com/grafana/grafana/issues/13723), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino)
+* **Internal metrics**: Renamed `grafana_info` to `grafana_build_info` and added branch, goversion and revision [#13876](https://github.com/grafana/grafana/pull/13876)
+* **Alerting**: Increaste default duration for queries [#13945](https://github.com/grafana/grafana/pull/13945)
+* **Table**: Fix CSS alpha background-color applied twice in table cell with link [#13606](https://github.com/grafana/grafana/issues/13606), thx [@grisme](https://github.com/grisme)
 
 ### Breaking changes
 
 * Postgres/MySQL/MSSQL datasources now per default uses `max open connections` = `unlimited` (earlier 10), `max idle connections` = `2` (earlier 10) and `connection max lifetime` = `4` hours (earlier unlimited)
 
+# 5.3.3 (unreleased)
+
+* **Alerting**: Delete alerts when parent folder was deleted [#13322](https://github.com/grafana/grafana/issues/13322)
+* **MySQL**: Fix `$__timeFilter()` should respect local time zone [#13769](https://github.com/grafana/grafana/issues/13769)
+* **Dashboard**: Fix datasource selection in panel by enter key [#13932](https://github.com/grafana/grafana/issues/13932)
+* **Graph**: Fix table legend height when positioned below graph and using Internet Explorer 11 [#13903](https://github.com/grafana/grafana/issues/13903)
+
 # 5.3.2 (2018-10-24)
 
 * **InfluxDB/Graphite/Postgres**: Prevent cross site scripting (XSS) in query editor [#13667](https://github.com/grafana/grafana/issues/13667), thx [@svenklemm](https://github.com/svenklemm)

+ 5 - 0
Gruntfile.js

@@ -9,12 +9,17 @@ module.exports = function (grunt) {
     destDir: 'dist',
     tempDir: 'tmp',
     platform: process.platform.replace('win32', 'windows'),
+    enterprise: false,
   };
 
   if (grunt.option('platform')) {
     config.platform = grunt.option('platform');
   }
 
+  if (grunt.option('enterprise')) {
+    config.enterprise = true;
+  }
+
   if (grunt.option('arch')) {
     config.arch = grunt.option('arch');
   } else {

+ 8 - 2
Makefile

@@ -5,8 +5,7 @@ all: deps build
 deps-go:
 	go run build.go setup
 
-deps-js:
-	yarn install --pure-lockfile --no-progress
+deps-js: node_modules
 
 deps: deps-js
 
@@ -43,3 +42,10 @@ test: test-go test-js
 
 run:
 	./bin/grafana-server
+
+clean:
+	rm -rf node_modules
+	rm -rf public/build
+
+node_modules: package.json yarn.lock
+	yarn install --pure-lockfile --no-progress

+ 33 - 8
build.go

@@ -41,8 +41,8 @@ var (
 	race                  bool
 	phjsToRelease         string
 	workingDir            string
-	includeBuildNumber    bool     = true
-	buildNumber           int      = 0
+	includeBuildId        bool     = true
+	buildId               string   = "0"
 	binaries              []string = []string{"grafana-server", "grafana-cli"}
 	isDev                 bool     = false
 	enterprise            bool     = false
@@ -54,6 +54,8 @@ func main() {
 
 	ensureGoPath()
 
+	var buildIdRaw string
+
 	flag.StringVar(&goarch, "goarch", runtime.GOARCH, "GOARCH")
 	flag.StringVar(&goos, "goos", runtime.GOOS, "GOOS")
 	flag.StringVar(&gocc, "cc", "", "CC")
@@ -61,12 +63,14 @@ func main() {
 	flag.StringVar(&pkgArch, "pkg-arch", "", "PKG ARCH")
 	flag.StringVar(&phjsToRelease, "phjs", "", "PhantomJS binary")
 	flag.BoolVar(&race, "race", race, "Use race detector")
-	flag.BoolVar(&includeBuildNumber, "includeBuildNumber", includeBuildNumber, "IncludeBuildNumber in package name")
+	flag.BoolVar(&includeBuildId, "includeBuildId", includeBuildId, "IncludeBuildId in package name")
 	flag.BoolVar(&enterprise, "enterprise", enterprise, "Build enterprise version of Grafana")
-	flag.IntVar(&buildNumber, "buildNumber", 0, "Build number from CI system")
+	flag.StringVar(&buildIdRaw, "buildId", "0", "Build ID from CI system")
 	flag.BoolVar(&isDev, "dev", isDev, "optimal for development, skips certain steps")
 	flag.Parse()
 
+	buildId = shortenBuildId(buildIdRaw)
+
 	readVersionFromPackageJson()
 
 	if pkgArch == "" {
@@ -197,9 +201,9 @@ func readVersionFromPackageJson() {
 	}
 
 	// add timestamp to iteration
-	if includeBuildNumber {
-		if buildNumber != 0 {
-			linuxPackageIteration = fmt.Sprintf("%d%s", buildNumber, linuxPackageIteration)
+	if includeBuildId {
+		if buildId != "0" {
+			linuxPackageIteration = fmt.Sprintf("%s%s", buildId, linuxPackageIteration)
 		} else {
 			linuxPackageIteration = fmt.Sprintf("%d%s", time.Now().Unix(), linuxPackageIteration)
 		}
@@ -392,7 +396,7 @@ func grunt(params ...string) {
 
 func gruntBuildArg(task string) []string {
 	args := []string{task}
-	if includeBuildNumber {
+	if includeBuildId {
 		args = append(args, fmt.Sprintf("--pkgVer=%v-%v", linuxPackageVersion, linuxPackageIteration))
 	} else {
 		args = append(args, fmt.Sprintf("--pkgVer=%v", version))
@@ -403,6 +407,10 @@ func gruntBuildArg(task string) []string {
 	if phjsToRelease != "" {
 		args = append(args, fmt.Sprintf("--phjsToRelease=%v", phjsToRelease))
 	}
+	if enterprise {
+		args = append(args, "--enterprise")
+	}
+
 	args = append(args, fmt.Sprintf("--platform=%v", goos))
 
 	return args
@@ -467,6 +475,7 @@ func ldflags() string {
 	b.WriteString(fmt.Sprintf(" -X main.version=%s", version))
 	b.WriteString(fmt.Sprintf(" -X main.commit=%s", getGitSha()))
 	b.WriteString(fmt.Sprintf(" -X main.buildstamp=%d", buildStamp()))
+	b.WriteString(fmt.Sprintf(" -X main.buildBranch=%s", getGitBranch()))
 	return b.String()
 }
 
@@ -514,6 +523,14 @@ func setBuildEnv() {
 	}
 }
 
+func getGitBranch() string {
+	v, err := runError("git", "rev-parse", "--abbrev-ref", "HEAD")
+	if err != nil {
+		return "master"
+	}
+	return string(v)
+}
+
 func getGitSha() string {
 	v, err := runError("git", "rev-parse", "--short", "HEAD")
 	if err != nil {
@@ -619,3 +636,11 @@ func shaFile(file string) error {
 
 	return out.Close()
 }
+
+func shortenBuildId(buildId string) string {
+	buildId = strings.Replace(buildId, "-", "", -1)
+	if (len(buildId) < 9) {
+		return buildId
+	}
+	return buildId[0:8]
+}

+ 4 - 0
conf/defaults.ini

@@ -557,3 +557,7 @@ callback_url =
 
 [panels]
 enable_alpha = false
+
+[enterprise]
+license_path =
+

+ 5 - 0
conf/sample.ini

@@ -475,3 +475,8 @@ log_queries =
 # Options to configure external image rendering server like https://github.com/grafana/grafana-image-renderer
 ;server_url =
 ;callback_url =
+
+[enterprise]
+# Path to a valid Grafana Enterprise license.jwt file
+;license_path =
+

+ 123 - 6
devenv/dev-dashboards/panel_tests_graph.json

@@ -927,6 +927,123 @@
       "title": "",
       "type": "text"
     },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "editable": true,
+      "error": false,
+      "fill": 0,
+      "gridPos": {
+        "h": 7,
+        "w": 16,
+        "x": 0,
+        "y": 44
+      },
+      "id": 21,
+      "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": "C-series",
+          "steppedLine": true
+        }
+      ],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "hide": false,
+          "refId": "B",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,null,40,null,90,null,null,100,null,null,100,null,null,80,null",
+          "target": ""
+        },
+        {
+          "alias": "",
+          "hide": false,
+          "refId": "C",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "20,null40,null,null,50,null,70,null,100,null,10,null,30,null",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Null between points",
+      "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": "Left is showing null between values for a normal line graph and staircase graph. Orphaned data points should be rendered as points",
+      "editable": true,
+      "error": false,
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 16,
+        "y": 44
+      },
+      "id": 22,
+      "links": [],
+      "mode": "markdown",
+      "title": "",
+      "type": "text"
+    },
     {
       "aliasColors": {},
       "bars": false,
@@ -939,7 +1056,7 @@
         "h": 7,
         "w": 24,
         "x": 0,
-        "y": 44
+        "y": 51
       },
       "id": 20,
       "legend": {
@@ -1024,7 +1141,7 @@
         "h": 7,
         "w": 12,
         "x": 0,
-        "y": 51
+        "y": 58
       },
       "id": 16,
       "legend": {
@@ -1127,7 +1244,7 @@
         "h": 7,
         "w": 12,
         "x": 12,
-        "y": 51
+        "y": 58
       },
       "id": 17,
       "legend": {
@@ -1266,7 +1383,7 @@
         "h": 7,
         "w": 12,
         "x": 0,
-        "y": 58
+        "y": 65
       },
       "id": 18,
       "legend": {
@@ -1370,7 +1487,7 @@
         "h": 7,
         "w": 12,
         "x": 12,
-        "y": 58
+        "y": 65
       },
       "id": 19,
       "legend": {
@@ -1554,5 +1671,5 @@
   "timezone": "browser",
   "title": "Panel Tests - Graph",
   "uid": "5SdHCadmz",
-  "version": 3
+  "version": 1
 }

+ 108 - 2
devenv/dev-dashboards/panel_tests_table.json

@@ -404,6 +404,112 @@
       "title": "Column style thresholds & units",
       "transform": "timeseries_to_columns",
       "type": "table"
+    },
+    {
+      "columns": [],
+      "datasource": "gdev-testdata",
+      "fontSize": "100%",
+      "gridPos": {
+        "h": 10,
+        "w": 24,
+        "x": 0,
+        "y": 26
+      },
+      "id": 6,
+      "links": [],
+      "pageSize": 20,
+      "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.5)",
+            "rgba(237, 129, 40, 0.5)",
+            "rgba(50, 172, 45, 0.5)"
+          ],
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "decimals": 2,
+          "link": true,
+          "linkTargetBlank": true,
+          "linkTooltip": "",
+          "linkUrl": "http://www.grafana.com",
+          "mappingType": 1,
+          "pattern": "ColorCell",
+          "thresholds": [
+            "5",
+            "10"
+          ],
+          "type": "number",
+          "unit": "currencyUSD"
+        },
+        {
+          "alias": "",
+          "colorMode": "value",
+          "colors": [
+            "rgba(245, 54, 54, 0.5)",
+            "rgba(237, 129, 40, 0.5)",
+            "rgba(50, 172, 45, 0.5)"
+          ],
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "decimals": 2,
+          "link": true,
+          "linkUrl": "http://www.grafana.com",
+          "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": "null,1,20,90,30,5,0,20,10"
+        },
+        {
+          "alias": "ColorCell",
+          "refId": "B",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "null,5,1,2,3,4,5,10,20"
+        }
+      ],
+      "title": "Column style thresholds and links",
+      "transform": "timeseries_to_columns",
+      "type": "table"
     }
   ],
   "refresh": false,
@@ -449,5 +555,5 @@
   "timezone": "browser",
   "title": "Panel Tests - Table",
   "uid": "pttable",
-  "version": 1
-}
+  "version": 2
+}

+ 1 - 1
docs/sources/administration/provisioning.md

@@ -158,7 +158,7 @@ Since not all datasources have the same configuration settings we only have the
 | timeInterval | string | Prometheus, Elasticsearch, InfluxDB, MySQL, PostgreSQL & MSSQL | Lowest interval/step value that should be used for this data source |
 | esVersion | number | Elasticsearch | Elasticsearch version as a number (2/5/56) |
 | timeField | string | Elasticsearch | Which field that should be used as timestamp |
-| interval | string | Elasticsearch | Index date time format |
+| interval | string | Elasticsearch | Index date time format. nil(No Pattern), 'Hourly', 'Daily', 'Weekly', 'Monthly' or 'Yearly' |
 | authType | string | Cloudwatch | Auth provider. keys/credentials/arn |
 | assumeRoleArn | string | Cloudwatch | ARN of Assume Role |
 | defaultRegion | string | Cloudwatch | AWS region |

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

@@ -140,7 +140,7 @@ In DingTalk PC Client:
 
 6. There will be a Webhook URL in the panel, looks like this: https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxx. Copy this URL to the grafana Dingtalk setting page and then click "finish".
 
-Dingtalk supports the following "message type": `text`, `link` and `markdown`. Only the `text` message type is supported.
+Dingtalk supports the following "message type": `text`, `link` and `markdown`. Only the `link` message type is supported.
 
 ### Kafka
 

+ 2 - 2
docs/sources/auth/gitlab.md

@@ -100,12 +100,12 @@ 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
+Here's a complete example with `allow_sign_up` enabled, and access limited to
 the `example` and `foo/bar` groups:
 
 ```ini
 [auth.gitlab]
-enabled = false
+enabled = true
 allow_sign_up = true
 client_id = GITLAB_APPLICATION_ID
 client_secret = GITLAB_SECRET

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

@@ -60,7 +60,8 @@ Here is a minimal policy example:
             "Effect": "Allow",
             "Action": [
                 "cloudwatch:ListMetrics",
-                "cloudwatch:GetMetricStatistics"
+                "cloudwatch:GetMetricStatistics",
+                "cloudwatch:GetMetricData"
             ],
             "Resource": "*"
         },

+ 52 - 0
docs/sources/features/datasources/mysql.md

@@ -73,6 +73,58 @@ Example:
 
 You can use wildcards (`*`)  in place of database or table if you want to grant access to more databases and tables.
 
+## Query Editor
+
+> Only available in Grafana v5.4+.
+
+{{< docs-imagebox img="/img/docs/v54/mysql_query_still.png" class="docs-image--no-shadow" animated-gif="/img/docs/v54/mysql_query.gif" >}}
+
+You find the MySQL query editor in the metrics tab in a panel's edit mode. You enter edit mode by clicking the
+panel title, then edit.
+
+The query editor has a link named `Generated SQL` that shows up after a query has been executed, while in panel edit mode. Click on it and it will expand and show the raw interpolated SQL string that was executed.
+
+### Select table, time column and metric column (FROM)
+
+When you enter edit mode for the first time or add a new query Grafana will try to prefill the query builder with the first table that has a timestamp column and a numeric column.
+
+In the FROM field, Grafana will suggest tables that are in the configured database. To select a table or view in another database that your database user has access to you can manually enter a fully qualified name (database.table) like `otherDb.metrics`.
+
+The Time column field refers to the name of the column holding your time values. Selecting a value for the Metric column field is optional. If a value is selected, the Metric column field will be used as the series name.
+
+The metric column suggestions will only contain columns with a text datatype (text, tinytext, mediumtext, longtext, varchar, char).
+If you want to use a column with a different datatype as metric column you may enter the column name with a cast: `CAST(numericColumn as CHAR)`.
+You may also enter arbitrary SQL expressions in the metric column field that evaluate to a text datatype like
+`CONCAT(column1, " ", CAST(numericColumn as CHAR))`.
+
+### Columns and Aggregation functions (SELECT)
+
+In the `SELECT` row you can specify what columns and functions you want to use.
+In the column field you may write arbitrary expressions instead of a column name like `column1 * column2 / column3`.
+
+If you use aggregate functions you need to group your resultset. The editor will automatically add a `GROUP BY time` if you add an aggregate function.
+
+You may add further value columns by clicking the plus button and selecting `Column` from the menu. Multiple value columns will be plotted as separate series in the graph panel.
+
+### Filter data (WHERE)
+To add a filter click the plus icon to the right of the `WHERE` condition. You can remove filters by clicking on
+the filter and selecting `Remove`. A filter for the current selected timerange is automatically added to new queries.
+
+### Group By
+To group by time or any other columns click the plus icon at the end of the GROUP BY row. The suggestion dropdown will only show text columns of your currently selected table but you may manually enter any column.
+You can remove the group by clicking on the item and then selecting `Remove`.
+
+If you add any grouping, all selected columns need to have an aggregate function applied. The query builder will automatically add aggregate functions to all columns without aggregate functions when you add groupings.
+
+#### Gap Filling
+
+Grafana can fill in missing values when you group by time. The time function accepts two arguments. The first argument is the time window that you would like to group by, and the second argument is the value you want Grafana to fill missing items with.
+
+### Text Editor Mode (RAW)
+You can switch to the raw query editor mode by clicking the hamburger icon and selecting `Switch editor mode` or by clicking `Edit SQL` below the query.
+
+> If you use the raw query editor, be sure your query at minimum has `ORDER BY time` and a filter on the returned time range.
+
 ## Macros
 
 To simplify syntax and to allow for dynamic parts, like date range filters, the query can contain macros.

+ 3 - 3
docs/sources/guides/whats-new-in-v5-3.md

@@ -18,7 +18,7 @@ Grafana v5.3 brings new features, many enhancements and bug fixes. This article
 - [TV mode]({{< relref "#tv-and-kiosk-mode" >}}) is improved and more accessible
 - [Alerting]({{< relref "#notification-reminders" >}}) with notification reminders
 - [Postgres]({{< relref "#postgres-query-builder" >}}) gets a new query builder!
-- [OAuth]({{< relref "#improved-oauth-support-for-gitlab" >}}) support for Gitlab is improved
+- [OAuth]({{< relref "#improved-oauth-support-for-gitlab" >}}) support for GitLab is improved
 - [Annotations]({{< relref "#annotations" >}}) with template variable filtering
 - [Variables]({{< relref "#variables" >}}) with free text support
 
@@ -69,9 +69,9 @@ Grafana 5.3 comes with a new graphical query builder for Postgres. This brings P
 
 {{< docs-imagebox img="/img/docs/v53/postgres_query_still.png" class="docs-image--no-shadow" animated-gif="/img/docs/v53/postgres_query.gif" >}}
 
-## Improved OAuth Support for Gitlab
+## Improved OAuth Support for GitLab
 
-Grafana 5.3 comes with a new OAuth integration for Gitlab that enables configuration to only allow users that are a member of certain Gitlab groups to authenticate. This makes it possible to use Gitlab OAuth with Grafana in a shared environment without giving everyone access to Grafana.
+Grafana 5.3 comes with a new OAuth integration for GitLab that enables configuration to only allow users that are a member of certain GitLab groups to authenticate. This makes it possible to use GitLab OAuth with Grafana in a shared environment without giving everyone access to Grafana.
 Learn how to enable and configure it in the [documentation](/auth/gitlab/).
 
 ## Annotations

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

@@ -290,7 +290,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
   "sendReminder": true,
   "frequency": "15m",
   "settings": {
-    "addresses: "carl@grafana.com;dev@grafana.com"
+    "addresses": "carl@grafana.com;dev@grafana.com"
   }
 }
 ```

+ 2 - 1
package.json

@@ -47,6 +47,7 @@
     "grunt-contrib-copy": "~1.0.0",
     "grunt-contrib-cssmin": "~1.0.2",
     "grunt-exec": "^1.0.1",
+    "grunt-newer": "^1.3.0",
     "grunt-notify": "^0.4.5",
     "grunt-postcss": "^0.8.0",
     "grunt-sass": "^2.0.0",
@@ -102,7 +103,7 @@
     "build": "grunt build",
     "test": "grunt test",
     "lint": "tslint -c tslint.json --project tsconfig.json",
-    "jest": "jest --config jest.config.json --notify --watch",
+    "jest": "jest --notify --watch",
     "api-tests": "jest --notify --watch --config=tests/api/jest.js",
     "precommit": "lint-staged && grunt precommit"
   },

+ 12 - 0
packaging/docker/build-enterprise.sh

@@ -0,0 +1,12 @@
+#!/bin/sh
+set -e
+
+_grafana_tag=$1
+_docker_repo=${2:-grafana/grafana-enterprise}
+
+docker build \
+  --tag "${_docker_repo}:${_grafana_tag}"\
+  --no-cache=true \
+  .
+
+docker push "${_docker_repo}:${_grafana_tag}"

+ 0 - 29
packaging/release_process.md

@@ -1,29 +0,0 @@
-# New Grafana Release Processes
-
-## Building release packages
-
-1) Update package.json so that it has the right version.
-2) Create a git tag for the release: `git tag -a v3.0.4 -m "3.0.4 release"`
-3) Push branch & tag to github!
-2) Packages from master a built automatically by circle CI for this repo [grafana/grafana-packer](https://github.com/grafana/grafana-packer)
-
-### Non master branch
-
-When building from non master branch create a new branch in repo [grafana/grafana-packer](https://github.com/grafana/grafana-packer)
-and configure circle.yml to deploy that branch as well, https://github.com/grafana/grafana-packer/blob/master/circle.yml#L25,
-you also need to update https://github.com/grafana/grafana-packer/blob/v3.1.x/deploy.sh#L7.
-
-### Windows build
-
-Sign into ci.appveyor.com and the Grafana project's build history page. Builds for windows take a long time (around 20min)
-and fail quite often for random reasons so I usually continue with the release process without a windows build already built.
-
-1) Click on the green build that has the correct version and tag
-2) Click on `DEPLOYMENTS`
-3) Click on `NEW DEPLOYMENT`
-4) Select GrafanaBuildS3
-4) Select the build you want to deploy.
-
-The deployment should be quick (just uploads the release zip file to S3)
-
-

+ 4 - 0
pkg/api/alerting.go

@@ -134,12 +134,16 @@ func AlertTest(c *m.ReqContext, dto dtos.AlertTestCommand) Response {
 		OrgId:     c.OrgId,
 		Dashboard: dto.Dashboard,
 		PanelId:   dto.PanelId,
+		User:      c.SignedInUser,
 	}
 
 	if err := bus.Dispatch(&backendCmd); err != nil {
 		if validationErr, ok := err.(alerting.ValidationError); ok {
 			return Error(422, validationErr.Error(), nil)
 		}
+		if err == m.ErrDataSourceAccessDenied {
+			return Error(403, "Access denied to datasource", err)
+		}
 		return Error(500, "Failed to test rule", err)
 	}
 

+ 5 - 45
pkg/api/dataproxy.go

@@ -1,62 +1,22 @@
 package api
 
 import (
-	"fmt"
-	"github.com/pkg/errors"
-	"time"
-
 	"github.com/grafana/grafana/pkg/api/pluginproxy"
-	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/metrics"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/plugins"
 )
 
-const HeaderNameNoBackendCache = "X-Grafana-NoCache"
-
-func (hs *HTTPServer) getDatasourceFromCache(id int64, c *m.ReqContext) (*m.DataSource, error) {
-	userPermissionsQuery := m.GetDataSourcePermissionsForUserQuery{
-		User: c.SignedInUser,
-	}
-	if err := bus.Dispatch(&userPermissionsQuery); err != nil {
-		if err != bus.ErrHandlerNotFound {
-			return nil, err
-		}
-	} else {
-		permissionType, exists := userPermissionsQuery.Result[id]
-		if exists && permissionType != m.DsPermissionQuery {
-			return nil, errors.New("User not allowed to access datasource")
-		}
-	}
-
-	nocache := c.Req.Header.Get(HeaderNameNoBackendCache) == "true"
-	cacheKey := fmt.Sprintf("ds-%d", id)
-
-	if !nocache {
-		if cached, found := hs.cache.Get(cacheKey); found {
-			ds := cached.(*m.DataSource)
-			if ds.OrgId == c.OrgId {
-				return ds, nil
-			}
-		}
-	}
-
-	query := m.GetDataSourceByIdQuery{Id: id, OrgId: c.OrgId}
-	if err := bus.Dispatch(&query); err != nil {
-		return nil, err
-	}
-
-	hs.cache.Set(cacheKey, query.Result, time.Second*5)
-	return query.Result, nil
-}
-
 func (hs *HTTPServer) ProxyDataSourceRequest(c *m.ReqContext) {
 	c.TimeRequest(metrics.M_DataSource_ProxyReq_Timer)
 
 	dsId := c.ParamsInt64(":id")
-	ds, err := hs.getDatasourceFromCache(dsId, c)
-
+	ds, err := hs.DatasourceCache.GetDatasource(dsId, c.SignedInUser, c.SkipCache)
 	if err != nil {
+		if err == m.ErrDataSourceAccessDenied {
+			c.JsonApiErr(403, "Access denied to datasource", err)
+			return
+		}
 		c.JsonApiErr(500, "Unable to load datasource meta data", err)
 		return
 	}

+ 1 - 0
pkg/api/dtos/index.go

@@ -14,6 +14,7 @@ type IndexViewData struct {
 	NewGrafanaVersionExists bool
 	NewGrafanaVersion       string
 	AppName                 string
+	AppNameBodyClass        string
 }
 
 type PluginCss struct {

+ 10 - 8
pkg/api/http_server.go

@@ -16,7 +16,6 @@ import (
 
 	"github.com/prometheus/client_golang/prometheus/promhttp"
 
-	gocache "github.com/patrickmn/go-cache"
 	macaron "gopkg.in/macaron.v1"
 
 	"github.com/grafana/grafana/pkg/api/live"
@@ -28,6 +27,8 @@ import (
 	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/plugins"
 	"github.com/grafana/grafana/pkg/registry"
+	"github.com/grafana/grafana/pkg/services/cache"
+	"github.com/grafana/grafana/pkg/services/datasources"
 	"github.com/grafana/grafana/pkg/services/hooks"
 	"github.com/grafana/grafana/pkg/services/rendering"
 	"github.com/grafana/grafana/pkg/setting"
@@ -46,19 +47,19 @@ type HTTPServer struct {
 	macaron       *macaron.Macaron
 	context       context.Context
 	streamManager *live.StreamManager
-	cache         *gocache.Cache
 	httpSrv       *http.Server
 
-	RouteRegister routing.RouteRegister `inject:""`
-	Bus           bus.Bus               `inject:""`
-	RenderService rendering.Service     `inject:""`
-	Cfg           *setting.Cfg          `inject:""`
-	HooksService  *hooks.HooksService   `inject:""`
+	RouteRegister   routing.RouteRegister    `inject:""`
+	Bus             bus.Bus                  `inject:""`
+	RenderService   rendering.Service        `inject:""`
+	Cfg             *setting.Cfg             `inject:""`
+	HooksService    *hooks.HooksService      `inject:""`
+	CacheService    *cache.CacheService      `inject:""`
+	DatasourceCache datasources.CacheService `inject:""`
 }
 
 func (hs *HTTPServer) Init() error {
 	hs.log = log.New("http.server")
-	hs.cache = gocache.New(5*time.Minute, 10*time.Minute)
 
 	hs.streamManager = live.NewStreamManager()
 	hs.macaron = hs.newMacaron()
@@ -231,6 +232,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
 		m.Use(middleware.ValidateHostHeader(setting.Domain))
 	}
 
+	m.Use(middleware.HandleNoCacheHeader())
 	m.Use(middleware.AddDefaultResponseHeaders())
 }
 

+ 12 - 0
pkg/api/index.go

@@ -83,6 +83,7 @@ func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, er
 		NewGrafanaVersion:       plugins.GrafanaLatestVersion,
 		NewGrafanaVersionExists: plugins.GrafanaHasUpdate,
 		AppName:                 setting.ApplicationName,
+		AppNameBodyClass:        getAppNameBodyClass(setting.ApplicationName),
 	}
 
 	if setting.DisableGravatar {
@@ -377,3 +378,14 @@ func (hs *HTTPServer) NotFoundHandler(c *m.ReqContext) {
 
 	c.HTML(404, "index", data)
 }
+
+func getAppNameBodyClass(name string) string {
+	switch name {
+	case setting.APP_NAME:
+		return "app-grafana"
+	case setting.APP_NAME_ENTERPRISE:
+		return "app-enterprise"
+	default:
+		return ""
+	}
+}

+ 4 - 1
pkg/api/metrics.go

@@ -25,8 +25,11 @@ func (hs *HTTPServer) QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) R
 		return Error(400, "Query missing datasourceId", nil)
 	}
 
-	ds, err := hs.getDatasourceFromCache(datasourceId, c)
+	ds, err := hs.DatasourceCache.GetDatasource(datasourceId, c.SignedInUser, c.SkipCache)
 	if err != nil {
+		if err == m.ErrDataSourceAccessDenied {
+			return Error(403, "Access denied to datasource", err)
+		}
 		return Error(500, "Unable to load datasource meta data", err)
 	}
 

+ 8 - 8
pkg/cmd/grafana-server/main.go

@@ -3,6 +3,8 @@ package main
 import (
 	"flag"
 	"fmt"
+	"net/http"
+	_ "net/http/pprof"
 	"os"
 	"os/signal"
 	"runtime"
@@ -11,16 +13,12 @@ import (
 	"syscall"
 	"time"
 
-	"net/http"
-	_ "net/http/pprof"
-
+	extensions "github.com/grafana/grafana/pkg/extensions"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/metrics"
-	"github.com/grafana/grafana/pkg/setting"
-
-	extensions "github.com/grafana/grafana/pkg/extensions"
 	_ "github.com/grafana/grafana/pkg/services/alerting/conditions"
 	_ "github.com/grafana/grafana/pkg/services/alerting/notifiers"
+	"github.com/grafana/grafana/pkg/setting"
 	_ "github.com/grafana/grafana/pkg/tsdb/cloudwatch"
 	_ "github.com/grafana/grafana/pkg/tsdb/elasticsearch"
 	_ "github.com/grafana/grafana/pkg/tsdb/graphite"
@@ -35,6 +33,7 @@ import (
 
 var version = "5.0.0"
 var commit = "NA"
+var buildBranch = "master"
 var buildstamp string
 
 var configFile = flag.String("config", "", "path to config file")
@@ -47,7 +46,7 @@ func main() {
 	profilePort := flag.Int("profile-port", 6060, "Define custom port for profiling")
 	flag.Parse()
 	if *v {
-		fmt.Printf("Version %s (commit: %s)\n", version, commit)
+		fmt.Printf("Version %s (commit: %s, branch: %s)\n", version, commit, buildBranch)
 		os.Exit(0)
 	}
 
@@ -78,9 +77,10 @@ func main() {
 	setting.BuildVersion = version
 	setting.BuildCommit = commit
 	setting.BuildStamp = buildstampInt64
+	setting.BuildBranch = buildBranch
 	setting.IsEnterprise = extensions.IsEnterprise
 
-	metrics.M_Grafana_Version.WithLabelValues(version).Set(1)
+	metrics.SetBuildInformation(version, commit, buildBranch)
 
 	server := NewGrafanaServer()
 

+ 6 - 6
pkg/cmd/grafana-server/server.go

@@ -12,20 +12,20 @@ import (
 	"time"
 
 	"github.com/facebookgo/inject"
+	"github.com/grafana/grafana/pkg/api"
 	"github.com/grafana/grafana/pkg/api/routing"
 	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/login"
 	"github.com/grafana/grafana/pkg/middleware"
 	"github.com/grafana/grafana/pkg/registry"
+	"github.com/grafana/grafana/pkg/social"
 
 	"golang.org/x/sync/errgroup"
 
-	"github.com/grafana/grafana/pkg/api"
 	"github.com/grafana/grafana/pkg/log"
-	"github.com/grafana/grafana/pkg/login"
+	"github.com/grafana/grafana/pkg/services/cache"
 	"github.com/grafana/grafana/pkg/setting"
 
-	"github.com/grafana/grafana/pkg/social"
-
 	// self registering services
 	_ "github.com/grafana/grafana/pkg/extensions"
 	_ "github.com/grafana/grafana/pkg/metrics"
@@ -77,6 +77,7 @@ func (g *GrafanaServerImpl) Run() error {
 	serviceGraph.Provide(&inject.Object{Value: bus.GetBus()})
 	serviceGraph.Provide(&inject.Object{Value: g.cfg})
 	serviceGraph.Provide(&inject.Object{Value: routing.NewRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)})
+	serviceGraph.Provide(&inject.Object{Value: cache.New(5*time.Minute, 10*time.Minute)})
 
 	// self registered services
 	services := registry.GetServices()
@@ -143,7 +144,6 @@ func (g *GrafanaServerImpl) Run() error {
 	}
 
 	sendSystemdNotification("READY=1")
-
 	return g.childRoutines.Wait()
 }
 
@@ -159,7 +159,7 @@ func (g *GrafanaServerImpl) loadConfiguration() {
 		os.Exit(1)
 	}
 
-	g.log.Info("Starting "+setting.ApplicationName, "version", version, "commit", commit, "compiled", time.Unix(setting.BuildStamp, 0))
+	g.log.Info("Starting "+setting.ApplicationName, "version", version, "commit", commit, "branch", buildBranch, "compiled", time.Unix(setting.BuildStamp, 0))
 	g.cfg.LogConfigSources()
 }
 

+ 1 - 0
pkg/login/auth.go

@@ -2,6 +2,7 @@ package login
 
 import (
 	"errors"
+
 	"github.com/grafana/grafana/pkg/bus"
 	m "github.com/grafana/grafana/pkg/models"
 )

+ 1 - 1
pkg/login/ldap.go

@@ -185,7 +185,7 @@ func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo
 
 		if ldapUser.isMemberOf(group.GroupDN) {
 			extUser.OrgRoles[group.OrgId] = group.OrgRole
-			if extUser.IsGrafanaAdmin == nil || *extUser.IsGrafanaAdmin == false {
+			if extUser.IsGrafanaAdmin == nil || !*extUser.IsGrafanaAdmin {
 				extUser.IsGrafanaAdmin = group.IsGrafanaAdmin
 			}
 		}

+ 27 - 3
pkg/metrics/metrics.go

@@ -58,7 +58,14 @@ var (
 	M_StatActive_Users       prometheus.Gauge
 	M_StatTotal_Orgs         prometheus.Gauge
 	M_StatTotal_Playlists    prometheus.Gauge
-	M_Grafana_Version        *prometheus.GaugeVec
+
+	// M_Grafana_Version is a gauge that contains build info about this binary
+	//
+	// Deprecated: use M_Grafana_Build_Version instead.
+	M_Grafana_Version *prometheus.GaugeVec
+
+	// grafanaBuildVersion is a gauge that contains build info about this binary
+	grafanaBuildVersion *prometheus.GaugeVec
 )
 
 func newCounterVecStartingAtZero(opts prometheus.CounterOpts, labels []string, labelValues ...string) *prometheus.CounterVec {
@@ -293,9 +300,25 @@ func init() {
 
 	M_Grafana_Version = prometheus.NewGaugeVec(prometheus.GaugeOpts{
 		Name:      "info",
-		Help:      "Information about the Grafana",
+		Help:      "Information about the Grafana. This metric is deprecated. please use `grafana_build_info`",
 		Namespace: exporterName,
 	}, []string{"version"})
+
+	grafanaBuildVersion = prometheus.NewGaugeVec(prometheus.GaugeOpts{
+		Name:      "build_info",
+		Help:      "A metric with a constant '1' value labeled by version, revision, branch, and goversion from which Grafana was built.",
+		Namespace: exporterName,
+	}, []string{"version", "revision", "branch", "goversion"})
+}
+
+// SetBuildInformation sets the build information for this binary
+func SetBuildInformation(version, revision, branch string) {
+	// We export this info twice for backwards compability.
+	// Once this have been released for some time we should be able to remote `M_Grafana_Version`
+	// The reason we added a new one is that its common practice in the prometheus community
+	// to name this metric `*_build_info` so its easy to do aggregation on all programs.
+	M_Grafana_Version.WithLabelValues(version).Set(1)
+	grafanaBuildVersion.WithLabelValues(version, revision, branch, runtime.Version()).Set(1)
 }
 
 func initMetricVars() {
@@ -334,7 +357,8 @@ func initMetricVars() {
 		M_StatActive_Users,
 		M_StatTotal_Orgs,
 		M_StatTotal_Playlists,
-		M_Grafana_Version)
+		M_Grafana_Version,
+		grafanaBuildVersion)
 
 }
 

+ 14 - 0
pkg/middleware/headers.go

@@ -0,0 +1,14 @@
+package middleware
+
+import (
+	m "github.com/grafana/grafana/pkg/models"
+	macaron "gopkg.in/macaron.v1"
+)
+
+const HeaderNameNoBackendCache = "X-Grafana-NoCache"
+
+func HandleNoCacheHeader() macaron.Handler {
+	return func(ctx *m.ReqContext) {
+		ctx.SkipCache = ctx.Req.Header.Get(HeaderNameNoBackendCache) == "true"
+	}
+}

+ 8 - 6
pkg/middleware/middleware.go

@@ -29,6 +29,7 @@ func GetContextHandler() macaron.Handler {
 			Session:        session.GetSession(),
 			IsSignedIn:     false,
 			AllowAnonymous: false,
+			SkipCache:      false,
 			Logger:         log.New("context"),
 		}
 
@@ -43,12 +44,13 @@ func GetContextHandler() macaron.Handler {
 		// then init session and look for userId in session
 		// then look for api key in session (special case for render calls via api)
 		// then test if anonymous access is enabled
-		if initContextWithRenderAuth(ctx) ||
-			initContextWithApiKey(ctx) ||
-			initContextWithBasicAuth(ctx, orgId) ||
-			initContextWithAuthProxy(ctx, orgId) ||
-			initContextWithUserSessionCookie(ctx, orgId) ||
-			initContextWithAnonymousUser(ctx) {
+		switch {
+		case initContextWithRenderAuth(ctx):
+		case initContextWithApiKey(ctx):
+		case initContextWithBasicAuth(ctx, orgId):
+		case initContextWithAuthProxy(ctx, orgId):
+		case initContextWithUserSessionCookie(ctx, orgId):
+		case initContextWithAnonymousUser(ctx):
 		}
 
 		ctx.Logger = log.New("context", "userId", ctx.UserId, "orgId", ctx.OrgId, "uname", ctx.Login)

+ 1 - 0
pkg/middleware/middleware_test.go

@@ -18,6 +18,7 @@ import (
 )
 
 func TestMiddlewareContext(t *testing.T) {
+	setting.ERR_TEMPLATE_NAME = "error-template"
 
 	Convey("Given the grafana middleware", t, func() {
 		middlewareScenario("middleware should add context to injector", func(sc *scenarioContext) {

+ 1 - 1
pkg/middleware/recovery.go

@@ -138,7 +138,7 @@ func Recovery() macaron.Handler {
 
 					c.JSON(500, resp)
 				} else {
-					c.HTML(500, "error")
+					c.HTML(500, setting.ERR_TEMPLATE_NAME)
 				}
 			}
 		}()

+ 4 - 0
pkg/middleware/recovery_test.go

@@ -8,11 +8,14 @@ import (
 	"github.com/grafana/grafana/pkg/bus"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/services/session"
+	"github.com/grafana/grafana/pkg/setting"
 	. "github.com/smartystreets/goconvey/convey"
 	"gopkg.in/macaron.v1"
 )
 
 func TestRecoveryMiddleware(t *testing.T) {
+	setting.ERR_TEMPLATE_NAME = "error-template"
+
 	Convey("Given an api route that panics", t, func() {
 		apiURL := "/api/whatever"
 		recoveryScenario("recovery middleware should return json", apiURL, func(sc *scenarioContext) {
@@ -50,6 +53,7 @@ func recoveryScenario(desc string, url string, fn scenarioFunc) {
 		sc := &scenarioContext{
 			url: url,
 		}
+
 		viewsPath, _ := filepath.Abs("../../public/views")
 
 		sc.m = macaron.New()

+ 2 - 1
pkg/models/alert.go

@@ -215,13 +215,14 @@ type AlertStateInfoDTO struct {
 // "Internal" commands
 
 type UpdateDashboardAlertsCommand struct {
-	UserId    int64
 	OrgId     int64
 	Dashboard *Dashboard
+	User      *SignedInUser
 }
 
 type ValidateDashboardAlertsCommand struct {
 	UserId    int64
 	OrgId     int64
 	Dashboard *Dashboard
+	User      *SignedInUser
 }

+ 2 - 1
pkg/models/context.go

@@ -20,6 +20,7 @@ type ReqContext struct {
 	IsSignedIn     bool
 	IsRenderCall   bool
 	AllowAnonymous bool
+	SkipCache      bool
 	Logger         log.Logger
 }
 
@@ -36,7 +37,7 @@ func (ctx *ReqContext) Handle(status int, title string, err error) {
 	ctx.Data["AppSubUrl"] = setting.AppSubUrl
 	ctx.Data["Theme"] = "dark"
 
-	ctx.HTML(status, "error")
+	ctx.HTML(status, setting.ERR_TEMPLATE_NAME)
 }
 
 func (ctx *ReqContext) JsonOK(message string) {

+ 0 - 5
pkg/models/datasource.go

@@ -207,11 +207,6 @@ func (p DsPermissionType) String() string {
 	return names[int(p)]
 }
 
-type GetDataSourcePermissionsForUserQuery struct {
-	User   *SignedInUser
-	Result map[int64]DsPermissionType
-}
-
 type DatasourcesPermissionFilterQuery struct {
 	User        *SignedInUser
 	Datasources []*DataSource

+ 1 - 0
pkg/models/user.go

@@ -165,6 +165,7 @@ type SignedInUser struct {
 	IsAnonymous    bool
 	HelpFlags1     HelpFlags1
 	LastSeenAt     time.Time
+	Teams          []int64
 }
 
 func (u *SignedInUser) ShouldUpdateLastSeenAt() bool {

+ 0 - 1
pkg/plugins/plugins.go

@@ -121,7 +121,6 @@ func (pm *PluginManager) Run(ctx context.Context) error {
 			pm.checkForUpdates()
 		case <-ctx.Done():
 			run = false
-			break
 		}
 	}
 

+ 34 - 3
pkg/registry/registry.go

@@ -29,11 +29,42 @@ func Register(descriptor *Descriptor) {
 }
 
 func GetServices() []*Descriptor {
-	sort.Slice(services, func(i, j int) bool {
-		return services[i].InitPriority > services[j].InitPriority
+	slice := getServicesWithOverrides()
+
+	sort.Slice(slice, func(i, j int) bool {
+		return slice[i].InitPriority > slice[j].InitPriority
 	})
 
-	return services
+	return slice
+}
+
+type OverrideServiceFunc func(descriptor Descriptor) (*Descriptor, bool)
+
+var overrides []OverrideServiceFunc
+
+func RegisterOverride(fn OverrideServiceFunc) {
+	overrides = append(overrides, fn)
+}
+
+func getServicesWithOverrides() []*Descriptor {
+	slice := []*Descriptor{}
+	for _, s := range services {
+		var descriptor *Descriptor
+		for _, fn := range overrides {
+			if newDescriptor, override := fn(*s); override {
+				descriptor = newDescriptor
+				break
+			}
+		}
+
+		if descriptor != nil {
+			slice = append(slice, descriptor)
+		} else {
+			slice = append(slice, s)
+		}
+	}
+
+	return slice
 }
 
 // Service interface is the lowest common shape that services

+ 3 - 3
pkg/services/alerting/commands.go

@@ -11,7 +11,7 @@ func init() {
 }
 
 func validateDashboardAlerts(cmd *m.ValidateDashboardAlertsCommand) error {
-	extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId)
+	extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId, cmd.User)
 
 	return extractor.ValidateAlerts()
 }
@@ -19,11 +19,11 @@ func validateDashboardAlerts(cmd *m.ValidateDashboardAlertsCommand) error {
 func updateDashboardAlerts(cmd *m.UpdateDashboardAlertsCommand) error {
 	saveAlerts := m.SaveAlertsCommand{
 		OrgId:       cmd.OrgId,
-		UserId:      cmd.UserId,
+		UserId:      cmd.User.UserId,
 		DashboardId: cmd.Dashboard.Id,
 	}
 
-	extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId)
+	extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId, cmd.User)
 
 	alerts, err := extractor.GetAlerts()
 	if err != nil {

+ 18 - 0
pkg/services/alerting/conditions/reducer_test.go

@@ -52,6 +52,24 @@ func TestSimpleReducer(t *testing.T) {
 			So(result, ShouldEqual, float64(1))
 		})
 
+		Convey("median should ignore null values", func() {
+			reducer := NewSimpleReducer("median")
+			series := &tsdb.TimeSeries{
+				Name: "test time serie",
+			}
+
+			series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 1))
+			series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 2))
+			series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 3))
+			series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(float64(1)), 4))
+			series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(float64(2)), 5))
+			series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(float64(3)), 6))
+
+			result := reducer.Reduce(series)
+			So(result.Valid, ShouldEqual, true)
+			So(result.Float64, ShouldEqual, float64(2))
+		})
+
 		Convey("avg", func() {
 			result := testReducer("avg", 1, 2, 3)
 			So(result, ShouldEqual, float64(2))

+ 18 - 1
pkg/services/alerting/extractor.go

@@ -13,14 +13,16 @@ import (
 
 // DashAlertExtractor extracts alerts from the dashboard json
 type DashAlertExtractor struct {
+	User  *m.SignedInUser
 	Dash  *m.Dashboard
 	OrgID int64
 	log   log.Logger
 }
 
 // NewDashAlertExtractor returns a new DashAlertExtractor
-func NewDashAlertExtractor(dash *m.Dashboard, orgID int64) *DashAlertExtractor {
+func NewDashAlertExtractor(dash *m.Dashboard, orgID int64, user *m.SignedInUser) *DashAlertExtractor {
 	return &DashAlertExtractor{
+		User:  user,
 		Dash:  dash,
 		OrgID: orgID,
 		log:   log.New("alerting.extractor"),
@@ -149,6 +151,21 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
 				return nil, ValidationError{Reason: fmt.Sprintf("Data source used by alert rule not found, alertName=%v, datasource=%s", alert.Name, dsName)}
 			}
 
+			dsFilterQuery := m.DatasourcesPermissionFilterQuery{
+				User:        e.User,
+				Datasources: []*m.DataSource{datasource},
+			}
+
+			if err := bus.Dispatch(&dsFilterQuery); err != nil {
+				if err != bus.ErrHandlerNotFound {
+					return nil, err
+				}
+			} else {
+				if len(dsFilterQuery.Result) == 0 {
+					return nil, m.ErrDataSourceAccessDenied
+				}
+			}
+
 			jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id)
 
 			if interval, err := panel.Get("interval").String(); err == nil {

+ 8 - 8
pkg/services/alerting/extractor_test.go

@@ -69,7 +69,7 @@ func TestAlertRuleExtraction(t *testing.T) {
 				So(getTarget(dashJson), ShouldEqual, "")
 			})
 
-			extractor := NewDashAlertExtractor(dash, 1)
+			extractor := NewDashAlertExtractor(dash, 1, nil)
 			_, _ = extractor.GetAlerts()
 
 			Convey("Dashboard json should not be updated after extracting rules", func() {
@@ -83,7 +83,7 @@ func TestAlertRuleExtraction(t *testing.T) {
 			So(err, ShouldBeNil)
 
 			dash := m.NewDashboardFromJson(dashJson)
-			extractor := NewDashAlertExtractor(dash, 1)
+			extractor := NewDashAlertExtractor(dash, 1, nil)
 
 			alerts, err := extractor.GetAlerts()
 
@@ -146,7 +146,7 @@ func TestAlertRuleExtraction(t *testing.T) {
 			dashJson, err := simplejson.NewJson(panelWithoutId)
 			So(err, ShouldBeNil)
 			dash := m.NewDashboardFromJson(dashJson)
-			extractor := NewDashAlertExtractor(dash, 1)
+			extractor := NewDashAlertExtractor(dash, 1, nil)
 
 			_, err = extractor.GetAlerts()
 
@@ -162,7 +162,7 @@ func TestAlertRuleExtraction(t *testing.T) {
 			dashJson, err := simplejson.NewJson(panelWithIdZero)
 			So(err, ShouldBeNil)
 			dash := m.NewDashboardFromJson(dashJson)
-			extractor := NewDashAlertExtractor(dash, 1)
+			extractor := NewDashAlertExtractor(dash, 1, nil)
 
 			_, err = extractor.GetAlerts()
 
@@ -178,7 +178,7 @@ func TestAlertRuleExtraction(t *testing.T) {
 			dashJson, err := simplejson.NewJson(json)
 			So(err, ShouldBeNil)
 			dash := m.NewDashboardFromJson(dashJson)
-			extractor := NewDashAlertExtractor(dash, 1)
+			extractor := NewDashAlertExtractor(dash, 1, nil)
 
 			alerts, err := extractor.GetAlerts()
 
@@ -198,7 +198,7 @@ func TestAlertRuleExtraction(t *testing.T) {
 			dashJson, err := simplejson.NewJson(json)
 			So(err, ShouldBeNil)
 			dash := m.NewDashboardFromJson(dashJson)
-			extractor := NewDashAlertExtractor(dash, 1)
+			extractor := NewDashAlertExtractor(dash, 1, nil)
 
 			alerts, err := extractor.GetAlerts()
 
@@ -228,7 +228,7 @@ func TestAlertRuleExtraction(t *testing.T) {
 			So(err, ShouldBeNil)
 
 			dash := m.NewDashboardFromJson(dashJson)
-			extractor := NewDashAlertExtractor(dash, 1)
+			extractor := NewDashAlertExtractor(dash, 1, nil)
 
 			alerts, err := extractor.GetAlerts()
 
@@ -248,7 +248,7 @@ func TestAlertRuleExtraction(t *testing.T) {
 			dashJSON, err := simplejson.NewJson(json)
 			So(err, ShouldBeNil)
 			dash := m.NewDashboardFromJson(dashJSON)
-			extractor := NewDashAlertExtractor(dash, 1)
+			extractor := NewDashAlertExtractor(dash, 1, nil)
 
 			err = extractor.ValidateAlerts()
 

+ 3 - 0
pkg/services/alerting/notifiers/dingding.go

@@ -57,6 +57,9 @@ func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error {
 	message := evalContext.Rule.Message
 	picUrl := evalContext.ImagePublicUrl
 	title := evalContext.GetNotificationTitle()
+	if message == "" {
+		message = title
+	}
 
 	bodyJSON, err := simplejson.NewJson([]byte(`{
 		"msgtype": "link",

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

@@ -14,7 +14,7 @@ import (
 )
 
 const (
-	captionLengthLimit = 200
+	captionLengthLimit = 1024
 )
 
 var (

+ 8 - 8
pkg/services/alerting/notifiers/telegram_test.go

@@ -61,7 +61,7 @@ func TestTelegramNotifier(t *testing.T) {
 					})
 
 				caption := generateImageCaption(evalContext, "http://grafa.url/abcdef", "")
-				So(len(caption), ShouldBeLessThanOrEqualTo, 200)
+				So(len(caption), ShouldBeLessThanOrEqualTo, 1024)
 				So(caption, ShouldContainSubstring, "Some kind of message.")
 				So(caption, ShouldContainSubstring, "[OK] This is an alarm")
 				So(caption, ShouldContainSubstring, "http://grafa.url/abcdef")
@@ -78,9 +78,9 @@ func TestTelegramNotifier(t *testing.T) {
 						})
 
 					caption := generateImageCaption(evalContext,
-						"http://grafa.url/abcdefaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+						"http://grafa.url/abcdefaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
 						"foo bar")
-					So(len(caption), ShouldBeLessThanOrEqualTo, 200)
+					So(len(caption), ShouldBeLessThanOrEqualTo, 1024)
 					So(caption, ShouldContainSubstring, "Some kind of message.")
 					So(caption, ShouldContainSubstring, "[OK] This is an alarm")
 					So(caption, ShouldContainSubstring, "foo bar")
@@ -91,31 +91,31 @@ func TestTelegramNotifier(t *testing.T) {
 					evalContext := alerting.NewEvalContext(context.Background(),
 						&alerting.Rule{
 							Name:    "This is an alarm",
-							Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise I will. Yes siree that's it.",
+							Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise I will. Yes siree that's it. But suddenly Telegram increased the length so now we need some lorem ipsum to fix this test. Here we go: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus consectetur molestie cursus. Donec suscipit egestas nisi. Proin ut efficitur ex. Mauris mi augue, volutpat a nisi vel, euismod dictum arcu. Sed quis tempor eros, sed malesuada dolor. Ut orci augue, viverra sit amet blandit quis, faucibus sit amet ex. Duis condimentum efficitur lectus, id dignissim quam tempor id. Morbi sollicitudin rhoncus diam, id tincidunt lectus scelerisque vitae. Etiam imperdiet semper sem, vel eleifend ligula mollis eget. Etiam ultrices fringilla lacus, sit amet pharetra ex blandit quis. Suspendisse in egestas neque, et posuere lectus. Vestibulum eu ex dui. Sed molestie nulla a lobortis scelerisque. Nulla ipsum ex, iaculis vitae vehicula sit amet, fermentum eu eros.",
 							State:   m.AlertStateOK,
 						})
 
 					caption := generateImageCaption(evalContext,
 						"http://grafa.url/foo",
 						"")
-					So(len(caption), ShouldBeLessThanOrEqualTo, 200)
+					So(len(caption), ShouldBeLessThanOrEqualTo, 1024)
 					So(caption, ShouldContainSubstring, "[OK] This is an alarm")
 					So(caption, ShouldNotContainSubstring, "http")
-					So(caption, ShouldContainSubstring, "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise ")
+					So(caption, ShouldContainSubstring, "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise I will. Yes siree that's it. But suddenly Telegram increased the length so now we need some lorem ipsum to fix this test. Here we go: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus consectetur molestie cursus. Donec suscipit egestas nisi. Proin ut efficitur ex. Mauris mi augue, volutpat a nisi vel, euismod dictum arcu. Sed quis tempor eros, sed malesuada dolor. Ut orci augue, viverra sit amet blandit quis, faucibus sit amet ex. Duis condimentum efficitur lectus, id dignissim quam tempor id. Morbi sollicitudin rhoncus diam, id tincidunt lectus scelerisque vitae. Etiam imperdiet semper sem, vel eleifend ligula mollis eget. Etiam ultrices fringilla lacus, sit amet pharetra ex blandit quis. Suspendisse in egestas neque, et posuere lectus. Vestibulum eu ex dui. Sed molestie nulla a lobortis sceleri")
 				})
 
 				Convey("Metrics should be skipped if they don't fit", func() {
 					evalContext := alerting.NewEvalContext(context.Background(),
 						&alerting.Rule{
 							Name:    "This is an alarm",
-							Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I ",
+							Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise I will. Yes siree that's it. But suddenly Telegram increased the length so now we need some lorem ipsum to fix this test. Here we go: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus consectetur molestie cursus. Donec suscipit egestas nisi. Proin ut efficitur ex. Mauris mi augue, volutpat a nisi vel, euismod dictum arcu. Sed quis tempor eros, sed malesuada dolor. Ut orci augue, viverra sit amet blandit quis, faucibus sit amet ex. Duis condimentum efficitur lectus, id dignissim quam tempor id. Morbi sollicitudin rhoncus diam, id tincidunt lectus scelerisque vitae. Etiam imperdiet semper sem, vel eleifend ligula mollis eget. Etiam ultrices fringilla lacus, sit amet pharetra ex blandit quis. Suspendisse in egestas neque, et posuere lectus. Vestibulum eu ex dui. Sed molestie nulla a lobortis sceleri",
 							State:   m.AlertStateOK,
 						})
 
 					caption := generateImageCaption(evalContext,
 						"http://grafa.url/foo",
 						"foo bar long song")
-					So(len(caption), ShouldBeLessThanOrEqualTo, 200)
+					So(len(caption), ShouldBeLessThanOrEqualTo, 1024)
 					So(caption, ShouldContainSubstring, "[OK] This is an alarm")
 					So(caption, ShouldNotContainSubstring, "http")
 					So(caption, ShouldNotContainSubstring, "foo bar")

+ 2 - 5
pkg/services/alerting/reader.go

@@ -34,11 +34,8 @@ func NewRuleReader() *DefaultRuleReader {
 func (arr *DefaultRuleReader) initReader() {
 	heartbeat := time.NewTicker(time.Second * 10)
 
-	for {
-		select {
-		case <-heartbeat.C:
-			arr.heartbeat()
-		}
+	for range heartbeat.C {
+		arr.heartbeat()
 	}
 }
 

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

@@ -13,6 +13,7 @@ type AlertTestCommand struct {
 	Dashboard *simplejson.Json
 	PanelId   int64
 	OrgId     int64
+	User      *m.SignedInUser
 
 	Result *EvalContext
 }
@@ -25,7 +26,7 @@ func handleAlertTestCommand(cmd *AlertTestCommand) error {
 
 	dash := m.NewDashboardFromJson(cmd.Dashboard)
 
-	extractor := NewDashAlertExtractor(dash, cmd.OrgId)
+	extractor := NewDashAlertExtractor(dash, cmd.OrgId, cmd.User)
 	alerts, err := extractor.GetAlerts()
 	if err != nil {
 		return err

+ 17 - 0
pkg/services/cache/cache.go

@@ -0,0 +1,17 @@
+package cache
+
+import (
+	"time"
+
+	gocache "github.com/patrickmn/go-cache"
+)
+
+type CacheService struct {
+	*gocache.Cache
+}
+
+func New(defaultExpiration, cleanupInterval time.Duration) *CacheService {
+	return &CacheService{
+		Cache: gocache.New(defaultExpiration, cleanupInterval),
+	}
+}

+ 2 - 1
pkg/services/dashboards/dashboard_service.go

@@ -90,6 +90,7 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO,
 		validateAlertsCmd := models.ValidateDashboardAlertsCommand{
 			OrgId:     dto.OrgId,
 			Dashboard: dash,
+			User:      dto.User,
 		}
 
 		if err := bus.Dispatch(&validateAlertsCmd); err != nil {
@@ -159,8 +160,8 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO,
 func (dr *dashboardServiceImpl) updateAlerting(cmd *models.SaveDashboardCommand, dto *SaveDashboardDTO) error {
 	alertCmd := models.UpdateDashboardAlertsCommand{
 		OrgId:     dto.OrgId,
-		UserId:    dto.User.UserId,
 		Dashboard: cmd.Result,
+		User:      dto.User,
 	}
 
 	if err := bus.Dispatch(&alertCmd); err != nil {

+ 53 - 0
pkg/services/datasources/cache.go

@@ -0,0 +1,53 @@
+package datasources
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/grafana/grafana/pkg/bus"
+	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/registry"
+	"github.com/grafana/grafana/pkg/services/cache"
+)
+
+type CacheService interface {
+	GetDatasource(datasourceID int64, user *m.SignedInUser, skipCache bool) (*m.DataSource, error)
+}
+
+type CacheServiceImpl struct {
+	Bus          bus.Bus             `inject:""`
+	CacheService *cache.CacheService `inject:""`
+}
+
+func init() {
+	registry.Register(&registry.Descriptor{
+		Name:         "DatasourceCacheService",
+		Instance:     &CacheServiceImpl{},
+		InitPriority: registry.Low,
+	})
+}
+
+func (dc *CacheServiceImpl) Init() error {
+	return nil
+}
+
+func (dc *CacheServiceImpl) GetDatasource(datasourceID int64, user *m.SignedInUser, skipCache bool) (*m.DataSource, error) {
+	cacheKey := fmt.Sprintf("ds-%d", datasourceID)
+
+	if !skipCache {
+		if cached, found := dc.CacheService.Get(cacheKey); found {
+			ds := cached.(*m.DataSource)
+			if ds.OrgId == user.OrgId {
+				return ds, nil
+			}
+		}
+	}
+
+	query := m.GetDataSourceByIdQuery{Id: datasourceID, OrgId: user.OrgId}
+	if err := dc.Bus.Dispatch(&query); err != nil {
+		return nil, err
+	}
+
+	dc.CacheService.Set(cacheKey, query.Result, time.Second*5)
+	return query.Result, nil
+}

+ 18 - 4
pkg/services/sqlstore/dashboard.go

@@ -327,20 +327,34 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
 		if dashboard.IsFolder {
 			deletes = append(deletes, "DELETE FROM dashboard_provisioning WHERE dashboard_id in (select id from dashboard where folder_id = ?)")
 			deletes = append(deletes, "DELETE FROM dashboard WHERE folder_id = ?")
-		}
-
-		for _, sql := range deletes {
-			_, err := sess.Exec(sql, dashboard.Id)
 
+			dashIds := []struct {
+				Id int64
+			}{}
+			err := sess.SQL("select id from dashboard where folder_id = ?", dashboard.Id).Find(&dashIds)
 			if err != nil {
 				return err
 			}
+
+			for _, id := range dashIds {
+				if err := deleteAlertDefinition(id.Id, sess); err != nil {
+					return nil
+				}
+			}
 		}
 
 		if err := deleteAlertDefinition(dashboard.Id, sess); err != nil {
 			return nil
 		}
 
+		for _, sql := range deletes {
+			_, err := sess.Exec(sql, dashboard.Id)
+
+			if err != nil {
+				return err
+			}
+		}
+
 		return nil
 	})
 }

+ 8 - 3
pkg/services/sqlstore/sqlstore.go

@@ -16,6 +16,7 @@ import (
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/registry"
 	"github.com/grafana/grafana/pkg/services/annotations"
+	"github.com/grafana/grafana/pkg/services/cache"
 	"github.com/grafana/grafana/pkg/services/sqlstore/migrations"
 	"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
 	"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
@@ -47,8 +48,9 @@ func init() {
 }
 
 type SqlStore struct {
-	Cfg *setting.Cfg `inject:""`
-	Bus bus.Bus      `inject:""`
+	Cfg          *setting.Cfg        `inject:""`
+	Bus          bus.Bus             `inject:""`
+	CacheService *cache.CacheService `inject:""`
 
 	dbCfg           DatabaseConfig
 	engine          *xorm.Engine
@@ -148,9 +150,11 @@ func (ss *SqlStore) Init() error {
 
 	// Init repo instances
 	annotations.SetRepository(&SqlAnnotationRepo{})
-
 	ss.Bus.SetTransactionManager(ss)
 
+	// Register handlers
+	ss.addUserQueryAndCommandHandlers()
+
 	// ensure admin user
 	if ss.skipEnsureAdmin {
 		return nil
@@ -322,6 +326,7 @@ func InitTestDB(t *testing.T) *SqlStore {
 	sqlstore := &SqlStore{}
 	sqlstore.skipEnsureAdmin = true
 	sqlstore.Bus = bus.New()
+	sqlstore.CacheService = cache.New(5*time.Minute, 10*time.Minute)
 
 	dbType := migrator.SQLITE
 

+ 30 - 3
pkg/services/sqlstore/user.go

@@ -15,8 +15,9 @@ import (
 	"github.com/grafana/grafana/pkg/util"
 )
 
-func init() {
-	//bus.AddHandler("sql", CreateUser)
+func (ss *SqlStore) addUserQueryAndCommandHandlers() {
+	ss.Bus.AddHandler(ss.GetSignedInUserWithCache)
+
 	bus.AddHandler("sql", GetUserById)
 	bus.AddHandler("sql", UpdateUser)
 	bus.AddHandler("sql", ChangeUserPassword)
@@ -25,7 +26,6 @@ func init() {
 	bus.AddHandler("sql", SetUsingOrg)
 	bus.AddHandler("sql", UpdateUserLastSeenAt)
 	bus.AddHandler("sql", GetUserProfile)
-	bus.AddHandler("sql", GetSignedInUser)
 	bus.AddHandler("sql", SearchUsers)
 	bus.AddHandler("sql", GetUserOrgList)
 	bus.AddHandler("sql", DeleteUser)
@@ -345,6 +345,22 @@ func GetUserOrgList(query *m.GetUserOrgListQuery) error {
 	return err
 }
 
+func (ss *SqlStore) GetSignedInUserWithCache(query *m.GetSignedInUserQuery) error {
+	cacheKey := fmt.Sprintf("signed-in-user-%d-%d", query.UserId, query.OrgId)
+	if cached, found := ss.CacheService.Get(cacheKey); found {
+		query.Result = cached.(*m.SignedInUser)
+		return nil
+	}
+
+	err := GetSignedInUser(query)
+	if err != nil {
+		return err
+	}
+
+	ss.CacheService.Set(cacheKey, query.Result, time.Second*5)
+	return nil
+}
+
 func GetSignedInUser(query *m.GetSignedInUserQuery) error {
 	orgId := "u.org_id"
 	if query.OrgId > 0 {
@@ -389,6 +405,17 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error {
 		user.OrgName = "Org missing"
 	}
 
+	getTeamsByUserQuery := &m.GetTeamsByUserQuery{OrgId: user.OrgId, UserId: user.UserId}
+	err = GetTeamsByUser(getTeamsByUserQuery)
+	if err != nil {
+		return err
+	}
+
+	user.Teams = make([]int64, len(getTeamsByUserQuery.Result))
+	for i, t := range getTeamsByUserQuery.Result {
+		user.Teams[i] = t.Id
+	}
+
 	query.Result = &user
 	return err
 }

+ 22 - 16
pkg/setting/setting.go

@@ -13,15 +13,12 @@ import (
 	"regexp"
 	"runtime"
 	"strings"
-
-	"gopkg.in/ini.v1"
-
-	"github.com/go-macaron/session"
-
 	"time"
 
+	"github.com/go-macaron/session"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/util"
+	"gopkg.in/ini.v1"
 )
 
 type Scheme string
@@ -34,9 +31,15 @@ const (
 )
 
 const (
-	DEV  string = "development"
-	PROD string = "production"
-	TEST string = "test"
+	DEV                 = "development"
+	PROD                = "production"
+	TEST                = "test"
+	APP_NAME            = "Grafana"
+	APP_NAME_ENTERPRISE = "Grafana Enterprise"
+)
+
+var (
+	ERR_TEMPLATE_NAME = "error"
 )
 
 var (
@@ -49,6 +52,7 @@ var (
 	// build
 	BuildVersion    string
 	BuildCommit     string
+	BuildBranch     string
 	BuildStamp      int64
 	IsEnterprise    bool
 	ApplicationName string
@@ -209,12 +213,10 @@ type Cfg struct {
 	RendererLimitAlerting int
 
 	DisableBruteForceLoginProtection bool
-
-	TempDataLifetime time.Duration
-
-	MetricsEndpointEnabled bool
-
-	EnableAlphaPanels bool
+	TempDataLifetime                 time.Duration
+	MetricsEndpointEnabled           bool
+	EnableAlphaPanels                bool
+	EnterpriseLicensePath            string
 }
 
 type CommandLineArgs struct {
@@ -533,9 +535,9 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
 	// Temporary keep global, to make refactor in steps
 	Raw = cfg.Raw
 
-	ApplicationName = "Grafana"
+	ApplicationName = APP_NAME
 	if IsEnterprise {
-		ApplicationName += " Enterprise"
+		ApplicationName = APP_NAME_ENTERPRISE
 	}
 
 	Env = iniFile.Section("").Key("app_mode").MustString("development")
@@ -715,6 +717,10 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
 
 	imageUploadingSection := iniFile.Section("external_image_storage")
 	ImageUploadProvider = imageUploadingSection.Key("provider").MustString("")
+
+	enterprise := iniFile.Section("enterprise")
+	cfg.EnterpriseLicensePath = enterprise.Key("license_path").MustString(filepath.Join(cfg.DataPath, "license.jwt"))
+
 	return nil
 }
 

+ 13 - 2
pkg/tsdb/cloudwatch/metric_find_query.go

@@ -35,6 +35,7 @@ type CustomMetricsCache struct {
 
 var customMetricsMetricsMap map[string]map[string]map[string]*CustomMetricsCache
 var customMetricsDimensionsMap map[string]map[string]map[string]*CustomMetricsCache
+var regionCache sync.Map
 
 func init() {
 	metricsMap = map[string][]string{
@@ -45,6 +46,7 @@ func init() {
 		"AWS/Billing":        {"EstimatedCharges"},
 		"AWS/CloudFront":     {"Requests", "BytesDownloaded", "BytesUploaded", "TotalErrorRate", "4xxErrorRate", "5xxErrorRate"},
 		"AWS/CloudSearch":    {"SuccessfulRequests", "SearchableDocuments", "IndexUtilization", "Partitions"},
+		"AWS/Connect":        {"CallsBreachingConcurrencyQuota", "CallBackNotDialableNumber", "CallRecordingUploadError", "CallsPerInterval", "ConcurrentCalls", "ConcurrentCallsPercentage", "ContactFlowErrors", "ContactFlowFatalErrors", "LongestQueueWaitTime", "MissedCalls", "MisconfiguredPhoneNumbers", "PublicSigningKeyUsage", "QueueCapacityExceededError", "QueueSize", "ThrottledCalls", "ToInstancePacketLossRate"},
 		"AWS/DMS":            {"FreeableMemory", "WriteIOPS", "ReadIOPS", "WriteThroughput", "ReadThroughput", "WriteLatency", "ReadLatency", "SwapUsage", "NetworkTransmitThroughput", "NetworkReceiveThroughput", "FullLoadThroughputBandwidthSource", "FullLoadThroughputBandwidthTarget", "FullLoadThroughputRowsSource", "FullLoadThroughputRowsTarget", "CDCIncomingChanges", "CDCChangesMemorySource", "CDCChangesMemoryTarget", "CDCChangesDiskSource", "CDCChangesDiskTarget", "CDCThroughputBandwidthTarget", "CDCThroughputRowsSource", "CDCThroughputRowsTarget", "CDCLatencySource", "CDCLatencyTarget"},
 		"AWS/DX":             {"ConnectionState", "ConnectionBpsEgress", "ConnectionBpsIngress", "ConnectionPpsEgress", "ConnectionPpsIngress", "ConnectionCRCErrorCount", "ConnectionLightLevelTx", "ConnectionLightLevelRx"},
 		"AWS/DynamoDB":       {"ConditionalCheckFailedRequests", "ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits", "OnlineIndexConsumedWriteCapacity", "OnlineIndexPercentageProgress", "OnlineIndexThrottleEvents", "ProvisionedReadCapacityUnits", "ProvisionedWriteCapacityUnits", "ReadThrottleEvents", "ReturnedBytes", "ReturnedItemCount", "ReturnedRecordsCount", "SuccessfulRequestLatency", "SystemErrors", "TimeToLiveDeletedItemCount", "ThrottledRequests", "UserErrors", "WriteThrottleEvents"},
@@ -119,6 +121,7 @@ func init() {
 		"AWS/Billing":          {"ServiceName", "LinkedAccount", "Currency"},
 		"AWS/CloudFront":       {"DistributionId", "Region"},
 		"AWS/CloudSearch":      {},
+		"AWS/Connect":          {"InstanceId", "MetricGroup", "Participant", "QueueName", "Stream Type", "Type of Connection"},
 		"AWS/DMS":              {"ReplicationInstanceIdentifier", "ReplicationTaskIdentifier"},
 		"AWS/DX":               {"ConnectionId"},
 		"AWS/DynamoDB":         {"TableName", "GlobalSecondaryIndexName", "Operation", "StreamLabel"},
@@ -233,13 +236,20 @@ func parseMultiSelectValue(input string) []string {
 // Whenever this list is updated, frontend list should also be updated.
 // Please update the region list in public/app/plugins/datasource/cloudwatch/partials/config.html
 func (e *CloudWatchExecutor) handleGetRegions(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) {
+	dsInfo := e.getDsInfo("default")
+	profile := dsInfo.Profile
+	if cache, ok := regionCache.Load(profile); ok {
+		if cache2, ok2 := cache.([]suggestData); ok2 {
+			return cache2, nil
+		}
+	}
+
 	regions := []string{
 		"ap-northeast-1", "ap-northeast-2", "ap-northeast-3", "ap-south-1", "ap-southeast-1", "ap-southeast-2", "ca-central-1",
 		"eu-central-1", "eu-north-1", "eu-west-1", "eu-west-2", "eu-west-3", "me-south-1", "sa-east-1", "us-east-1", "us-east-2", "us-west-1", "us-west-2",
 		"cn-north-1", "cn-northwest-1", "us-gov-east-1", "us-gov-west-1", "us-isob-east-1", "us-iso-east-1",
 	}
-
-	err := e.ensureClientSession("us-east-1")
+	err := e.ensureClientSession("default")
 	if err != nil {
 		return nil, err
 	}
@@ -269,6 +279,7 @@ func (e *CloudWatchExecutor) handleGetRegions(ctx context.Context, parameters *s
 	for _, region := range regions {
 		result = append(result, suggestData{Text: region, Value: region})
 	}
+	regionCache.Store(profile, result)
 
 	return result, nil
 }

+ 32 - 1
pkg/tsdb/cloudwatch/metric_find_query_test.go

@@ -9,20 +9,26 @@ import (
 	"github.com/aws/aws-sdk-go/service/ec2"
 	"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
 	"github.com/bmizerany/assert"
+	"github.com/grafana/grafana/pkg/components/securejsondata"
 	"github.com/grafana/grafana/pkg/components/simplejson"
+	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/tsdb"
 	. "github.com/smartystreets/goconvey/convey"
 )
 
 type mockedEc2 struct {
 	ec2iface.EC2API
-	Resp ec2.DescribeInstancesOutput
+	Resp        ec2.DescribeInstancesOutput
+	RespRegions ec2.DescribeRegionsOutput
 }
 
 func (m mockedEc2) DescribeInstancesPages(in *ec2.DescribeInstancesInput, fn func(*ec2.DescribeInstancesOutput, bool) bool) error {
 	fn(&m.Resp, true)
 	return nil
 }
+func (m mockedEc2) DescribeRegions(in *ec2.DescribeRegionsInput) (*ec2.DescribeRegionsOutput, error) {
+	return &m.RespRegions, nil
+}
 
 func TestCloudWatchMetrics(t *testing.T) {
 
@@ -82,6 +88,31 @@ func TestCloudWatchMetrics(t *testing.T) {
 		})
 	})
 
+	Convey("When calling handleGetRegions", t, func() {
+		executor := &CloudWatchExecutor{
+			ec2Svc: mockedEc2{RespRegions: ec2.DescribeRegionsOutput{
+				Regions: []*ec2.Region{
+					{
+						RegionName: aws.String("ap-northeast-2"),
+					},
+				},
+			}},
+		}
+		jsonData := simplejson.New()
+		jsonData.Set("defaultRegion", "default")
+		executor.DataSource = &models.DataSource{
+			JsonData:       jsonData,
+			SecureJsonData: securejsondata.SecureJsonData{},
+		}
+
+		result, _ := executor.handleGetRegions(context.Background(), simplejson.New(), &tsdb.TsdbQuery{})
+
+		Convey("Should return regions", func() {
+			So(result[0].Text, ShouldEqual, "ap-northeast-1")
+			So(result[1].Text, ShouldEqual, "ap-northeast-2")
+		})
+	})
+
 	Convey("When calling handleGetEc2InstanceAttribute", t, func() {
 		executor := &CloudWatchExecutor{
 			ec2Svc: mockedEc2{Resp: ec2.DescribeInstancesOutput{

+ 2 - 4
pkg/tsdb/graphite/graphite.go

@@ -164,14 +164,12 @@ func formatTimeRange(input string) string {
 
 func fixIntervalFormat(target string) string {
 	rMinute := regexp.MustCompile(`'(\d+)m'`)
-	rMin := regexp.MustCompile("m")
 	target = rMinute.ReplaceAllStringFunc(target, func(m string) string {
-		return rMin.ReplaceAllString(m, "min")
+		return strings.Replace(m, "m", "min", -1)
 	})
 	rMonth := regexp.MustCompile(`'(\d+)M'`)
-	rMon := regexp.MustCompile("M")
 	target = rMonth.ReplaceAllStringFunc(target, func(M string) string {
-		return rMon.ReplaceAllString(M, "mon")
+		return strings.Replace(M, "M", "mon", -1)
 	})
 	return target
 }

+ 1 - 1
pkg/tsdb/mysql/macros.go

@@ -60,7 +60,7 @@ func (m *mySqlMacroEngine) evaluateMacro(name string, args []string) (string, er
 			return "", fmt.Errorf("missing time column argument for macro %v", name)
 		}
 
-		return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
+		return fmt.Sprintf("%s BETWEEN FROM_UNIXTIME(%d) AND FROM_UNIXTIME(%d)", args[0], m.timeRange.GetFromAsSecondsEpoch(), m.timeRange.GetToAsSecondsEpoch()), nil
 	case "__timeGroup":
 		if len(args) < 2 {
 			return "", fmt.Errorf("macro %v needs time column and interval", name)

+ 3 - 3
pkg/tsdb/mysql/macros_test.go

@@ -60,7 +60,7 @@ func TestMacroEngine(t *testing.T) {
 				sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
 				So(err, ShouldBeNil)
 
-				So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
+				So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN FROM_UNIXTIME(%d) AND FROM_UNIXTIME(%d)", from.Unix(), to.Unix()))
 			})
 
 			Convey("interpolate __unixEpochFilter function", func() {
@@ -92,7 +92,7 @@ func TestMacroEngine(t *testing.T) {
 				sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
 				So(err, ShouldBeNil)
 
-				So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
+				So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN FROM_UNIXTIME(%d) AND FROM_UNIXTIME(%d)", from.Unix(), to.Unix()))
 			})
 
 			Convey("interpolate __unixEpochFilter function", func() {
@@ -112,7 +112,7 @@ func TestMacroEngine(t *testing.T) {
 				sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
 				So(err, ShouldBeNil)
 
-				So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
+				So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN FROM_UNIXTIME(%d) AND FROM_UNIXTIME(%d)", from.Unix(), to.Unix()))
 			})
 
 			Convey("interpolate __unixEpochFilter function", func() {

+ 1 - 2
pkg/tsdb/stackdriver/stackdriver.go

@@ -186,8 +186,7 @@ func reverse(s string) string {
 }
 
 func interpolateFilterWildcards(value string) string {
-	re := regexp.MustCompile("[*]")
-	matches := len(re.FindAllStringIndex(value, -1))
+	matches := strings.Count(value, "*")
 	if matches == 2 && strings.HasSuffix(value, "*") && strings.HasPrefix(value, "*") {
 		value = strings.Replace(value, "*", "", -1)
 		value = fmt.Sprintf(`has_substring("%s")`, value)

+ 28 - 0
public/app/core/actions/appNotification.ts

@@ -0,0 +1,28 @@
+import { AppNotification } from 'app/types/';
+
+export enum ActionTypes {
+  AddAppNotification = 'ADD_APP_NOTIFICATION',
+  ClearAppNotification = 'CLEAR_APP_NOTIFICATION',
+}
+
+interface AddAppNotificationAction {
+  type: ActionTypes.AddAppNotification;
+  payload: AppNotification;
+}
+
+interface ClearAppNotificationAction {
+  type: ActionTypes.ClearAppNotification;
+  payload: number;
+}
+
+export type Action = AddAppNotificationAction | ClearAppNotificationAction;
+
+export const clearAppNotification = (appNotificationId: number) => ({
+  type: ActionTypes.ClearAppNotification,
+  payload: appNotificationId,
+});
+
+export const notifyApp = (appNotification: AppNotification) => ({
+  type: ActionTypes.AddAppNotification,
+  payload: appNotification,
+});

+ 2 - 1
public/app/core/actions/index.ts

@@ -1,4 +1,5 @@
 import { updateLocation } from './location';
 import { updateNavIndex, UpdateNavIndexAction } from './navModel';
+import { notifyApp, clearAppNotification } from './appNotification';
 
-export { updateLocation, updateNavIndex, UpdateNavIndexAction };
+export { updateLocation, updateNavIndex, UpdateNavIndexAction, notifyApp, clearAppNotification };

+ 28 - 0
public/app/core/actions/user.ts

@@ -0,0 +1,28 @@
+import { ThunkAction } from 'redux-thunk';
+import { getBackendSrv } from '../services/backend_srv';
+import { DashboardAcl, DashboardSearchHit, StoreState } from '../../types';
+
+type ThunkResult<R> = ThunkAction<R, StoreState, undefined, any>;
+
+export type Action = LoadStarredDashboardsAction;
+
+export enum ActionTypes {
+  LoadStarredDashboards = 'LOAD_STARRED_DASHBOARDS',
+}
+
+interface LoadStarredDashboardsAction {
+  type: ActionTypes.LoadStarredDashboards;
+  payload: DashboardSearchHit[];
+}
+
+const starredDashboardsLoaded = (dashboards: DashboardAcl[]) => ({
+  type: ActionTypes.LoadStarredDashboards,
+  payload: dashboards,
+});
+
+export function loadStarredDashboards(): ThunkResult<void> {
+  return async dispatch => {
+    const starredDashboards = await getBackendSrv().search({ starred: true });
+    dispatch(starredDashboardsLoaded(starredDashboards));
+  };
+}

+ 2 - 0
public/app/core/angular_wrappers.ts

@@ -5,10 +5,12 @@ import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA';
 import { SearchResult } from './components/search/SearchResult';
 import { TagFilter } from './components/TagFilter/TagFilter';
 import { SideMenu } from './components/sidemenu/SideMenu';
+import AppNotificationList from './components/AppNotifications/AppNotificationList';
 
 export function registerAngularDirectives() {
   react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
   react2AngularDirective('sidemenu', SideMenu, []);
+  react2AngularDirective('appNotificationsList', AppNotificationList, []);
   react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']);
   react2AngularDirective('emptyListCta', EmptyListCTA, ['model']);
   react2AngularDirective('searchResult', SearchResult, []);

+ 12 - 5
public/app/core/components/Animations/SlideDown.tsx

@@ -1,15 +1,22 @@
-import React from 'react';
+import React from 'react';
 import Transition from 'react-transition-group/Transition';
 
-const defaultMaxHeight = '200px'; // When animating using max-height we need to use a static value.
+interface Style {
+  transition?: string;
+  overflow?: string;
+}
+
+// When animating using max-height we need to use a static value.
 // If this is not enough, pass in <SlideDown maxHeight="....
+const defaultMaxHeight = '200px';
 const defaultDuration = 200;
-const defaultStyle = {
+
+export const defaultStyle: Style = {
   transition: `max-height ${defaultDuration}ms ease-in-out`,
   overflow: 'hidden',
 };
 
-export default ({ children, in: inProp, maxHeight = defaultMaxHeight }) => {
+export default ({ children, in: inProp, maxHeight = defaultMaxHeight, style = defaultStyle }) => {
   // There are 4 main states a Transition can be in:
   // ENTERING, ENTERED, EXITING, EXITED
   // https://reactcommunity.org/react-transition-group/
@@ -25,7 +32,7 @@ export default ({ children, in: inProp, maxHeight = defaultMaxHeight }) => {
       {state => (
         <div
           style={{
-            ...defaultStyle,
+            ...style,
             ...transitionStyles[state],
           }}
         >

+ 38 - 0
public/app/core/components/AppNotifications/AppNotificationItem.tsx

@@ -0,0 +1,38 @@
+import React, { Component } from 'react';
+import { AppNotification } from 'app/types';
+
+interface Props {
+  appNotification: AppNotification;
+  onClearNotification: (id) => void;
+}
+
+export default class AppNotificationItem extends Component<Props> {
+  shouldComponentUpdate(nextProps) {
+    return this.props.appNotification.id !== nextProps.appNotification.id;
+  }
+
+  componentDidMount() {
+    const { appNotification, onClearNotification } = this.props;
+    setTimeout(() => {
+      onClearNotification(appNotification.id);
+    }, appNotification.timeout);
+  }
+
+  render() {
+    const { appNotification, onClearNotification } = this.props;
+    return (
+      <div className={`alert-${appNotification.severity} alert`}>
+        <div className="alert-icon">
+          <i className={appNotification.icon} />
+        </div>
+        <div className="alert-body">
+          <div className="alert-title">{appNotification.title}</div>
+          <div className="alert-text">{appNotification.text}</div>
+        </div>
+        <button type="button" className="alert-close" onClick={() => onClearNotification(appNotification.id)}>
+          <i className="fa fa fa-remove" />
+        </button>
+      </div>
+    );
+  }
+}

+ 60 - 0
public/app/core/components/AppNotifications/AppNotificationList.tsx

@@ -0,0 +1,60 @@
+import React, { PureComponent } from 'react';
+import appEvents from 'app/core/app_events';
+import AppNotificationItem from './AppNotificationItem';
+import { notifyApp, clearAppNotification } from 'app/core/actions';
+import { connectWithStore } from 'app/core/utils/connectWithReduxStore';
+import { AppNotification, StoreState } from 'app/types';
+import {
+  createErrorNotification,
+  createSuccessNotification,
+  createWarningNotification,
+} from '../../copy/appNotification';
+
+export interface Props {
+  appNotifications: AppNotification[];
+  notifyApp: typeof notifyApp;
+  clearAppNotification: typeof clearAppNotification;
+}
+
+export class AppNotificationList extends PureComponent<Props> {
+  componentDidMount() {
+    const { notifyApp } = this.props;
+
+    appEvents.on('alert-warning', options => notifyApp(createWarningNotification(options[0], options[1])));
+    appEvents.on('alert-success', options => notifyApp(createSuccessNotification(options[0], options[1])));
+    appEvents.on('alert-error', options => notifyApp(createErrorNotification(options[0], options[1])));
+  }
+
+  onClearAppNotification = id => {
+    this.props.clearAppNotification(id);
+  };
+
+  render() {
+    const { appNotifications } = this.props;
+
+    return (
+      <div>
+        {appNotifications.map((appNotification, index) => {
+          return (
+            <AppNotificationItem
+              key={`${appNotification.id}-${index}`}
+              appNotification={appNotification}
+              onClearNotification={id => this.onClearAppNotification(id)}
+            />
+          );
+        })}
+      </div>
+    );
+  }
+}
+
+const mapStateToProps = (state: StoreState) => ({
+  appNotifications: state.appNotifications.appNotifications,
+});
+
+const mapDispatchToProps = {
+  notifyApp,
+  clearAppNotification,
+};
+
+export default connectWithStore(AppNotificationList, mapStateToProps, mapDispatchToProps);

+ 1 - 0
public/app/core/components/EmptyListCTA/EmptyListCTA.test.tsx

@@ -7,6 +7,7 @@ const model = {
   buttonIcon: 'ga css class',
   buttonLink: 'http://url/to/destination',
   buttonTitle: 'Click me',
+  onClick: jest.fn(),
   proTip: 'This is a tip',
   proTipLink: 'http://url/to/tip/destination',
   proTipLinkTitle: 'Learn more',

+ 2 - 1
public/app/core/components/EmptyListCTA/EmptyListCTA.tsx

@@ -11,6 +11,7 @@ class EmptyListCTA extends Component<Props, any> {
       buttonIcon,
       buttonLink,
       buttonTitle,
+      onClick,
       proTip,
       proTipLink,
       proTipLinkTitle,
@@ -19,7 +20,7 @@ class EmptyListCTA extends Component<Props, any> {
     return (
       <div className="empty-list-cta">
         <div className="empty-list-cta__title">{title}</div>
-        <a href={buttonLink} className="empty-list-cta__button btn btn-xlarge btn-success">
+        <a onClick={onClick} href={buttonLink} className="empty-list-cta__button btn btn-xlarge btn-success">
           <i className={buttonIcon} />
           {buttonTitle}
         </a>

+ 1 - 0
public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.test.tsx.snap

@@ -12,6 +12,7 @@ exports[`EmptyListCTA renders correctly 1`] = `
   <a
     className="empty-list-cta__button btn btn-xlarge btn-success"
     href="http://url/to/destination"
+    onClick={[MockFunction]}
   >
     <i
       className="ga css class"

+ 0 - 7
public/app/core/components/InfoPopover/InfoPopover.tsx

@@ -1,7 +0,0 @@
-import React, { SFC } from 'react';
-interface Props {}
-const InfoPopover: SFC<Props> = props => {
-  return <div />;
-};
-
-export default InfoPopover;

+ 2 - 1
public/app/core/components/Forms/Forms.tsx → public/app/core/components/Label/Label.tsx

@@ -5,11 +5,12 @@ interface Props {
   tooltip?: string;
   for?: string;
   children: ReactNode;
+  width?: number;
 }
 
 export const Label: SFC<Props> = props => {
   return (
-    <span className="gf-form-label width-10">
+    <span className={`gf-form-label width-${props.width ? props.width : '10'}`}>
       <span>{props.children}</span>
       {props.tooltip && (
         <Tooltip className="gf-form-help-icon--right-normal" placement="auto" content={props.tooltip}>

+ 46 - 0
public/app/core/components/Picker/SimplePicker.tsx

@@ -0,0 +1,46 @@
+import React, { SFC } from 'react';
+import Select from 'react-select';
+import DescriptionOption from './DescriptionOption';
+import ResetStyles from './ResetStyles';
+
+interface Props {
+  className?: string;
+  defaultValue: any;
+  getOptionLabel: (item: any) => string;
+  getOptionValue: (item: any) => string;
+  onSelected: (item: any) => {} | void;
+  options: any[];
+  placeholder?: string;
+  width: number;
+}
+
+const SimplePicker: SFC<Props> = ({
+  className,
+  defaultValue,
+  getOptionLabel,
+  getOptionValue,
+  onSelected,
+  options,
+  placeholder,
+  width,
+}) => {
+  return (
+    <Select
+      classNamePrefix={`gf-form-select-box`}
+      className={`width-${width} gf-form-input gf-form-input--form-dropdown ${className || ''}`}
+      components={{
+        Option: DescriptionOption,
+      }}
+      defaultValue={defaultValue}
+      getOptionLabel={getOptionLabel}
+      getOptionValue={getOptionValue}
+      isSearchable={false}
+      onChange={onSelected}
+      options={options}
+      placeholder={placeholder || 'Choose'}
+      styles={ResetStyles}
+    />
+  );
+};
+
+export default SimplePicker;

+ 1 - 4
public/app/core/components/FormSwitch/FormSwitch.tsx → public/app/core/components/Switch/Switch.tsx

@@ -13,14 +13,13 @@ export interface State {
   id: any;
 }
 
-export class FormSwitch extends PureComponent<Props, State> {
+export class Switch extends PureComponent<Props, State> {
   state = {
     id: _.uniqueId(),
   };
 
   internalOnChange = event => {
     event.stopPropagation();
-
     this.props.onChange(event);
   };
 
@@ -45,5 +44,3 @@ export class FormSwitch extends PureComponent<Props, State> {
     );
   }
 }
-
-export default FormSwitch;

+ 16 - 25
public/app/core/components/Tooltip/Tooltip.tsx

@@ -1,37 +1,28 @@
-import React from 'react';
+import React, { PureComponent } from 'react';
 import withTooltip from './withTooltip';
 import { Target } from 'react-popper';
 
-interface TooltipProps {
+interface Props {
   tooltipSetState: (prevState: object) => void;
 }
 
-class Tooltip extends React.Component<TooltipProps, any> {
-  constructor(props) {
-    super(props);
-    this.showTooltip = this.showTooltip.bind(this);
-    this.hideTooltip = this.hideTooltip.bind(this);
-  }
-
-  showTooltip() {
+class Tooltip extends PureComponent<Props> {
+  showTooltip = () => {
     const { tooltipSetState } = this.props;
-    tooltipSetState(prevState => {
-      return {
-        ...prevState,
-        show: true,
-      };
-    });
-  }
 
-  hideTooltip() {
+    tooltipSetState(prevState => ({
+      ...prevState,
+      show: true,
+    }));
+  };
+
+  hideTooltip = () => {
     const { tooltipSetState } = this.props;
-    tooltipSetState(prevState => {
-      return {
-        ...prevState,
-        show: false,
-      };
-    });
-  }
+    tooltipSetState(prevState => ({
+      ...prevState,
+      show: false,
+    }));
+  };
 
   render() {
     return (

+ 64 - 33
public/app/core/components/colorpicker/SeriesColorPicker.tsx

@@ -1,53 +1,84 @@
 import React from 'react';
-import { ColorPickerPopover } from './ColorPickerPopover';
-import { react2AngularDirective } from 'app/core/utils/react2angular';
+import ReactDOM from 'react-dom';
+import Drop from 'tether-drop';
+import { SeriesColorPickerPopover } from './SeriesColorPickerPopover';
 
-export interface Props {
-  series: any;
-  onColorChange: (color: string) => void;
-  onToggleAxis: () => void;
+export interface SeriesColorPickerProps {
+  color: string;
+  yaxis?: number;
+  optionalClass?: string;
+  onColorChange: (newColor: string) => void;
+  onToggleAxis?: () => void;
 }
 
-export class SeriesColorPicker extends React.Component<Props, any> {
+export class SeriesColorPicker extends React.Component<SeriesColorPickerProps> {
+  pickerElem: any;
+  colorPickerDrop: any;
+
+  static defaultProps = {
+    optionalClass: '',
+    yaxis: undefined,
+    onToggleAxis: () => {},
+  };
+
   constructor(props) {
     super(props);
-    this.onColorChange = this.onColorChange.bind(this);
-    this.onToggleAxis = this.onToggleAxis.bind(this);
-  }
-
-  onColorChange(color) {
-    this.props.onColorChange(color);
   }
 
-  onToggleAxis() {
-    this.props.onToggleAxis();
+  componentWillUnmount() {
+    this.destroyDrop();
   }
 
-  renderAxisSelection() {
-    const leftButtonClass = this.props.series.yaxis === 1 ? 'btn-success' : 'btn-inverse';
-    const rightButtonClass = this.props.series.yaxis === 2 ? 'btn-success' : 'btn-inverse';
+  onClickToOpen = () => {
+    if (this.colorPickerDrop) {
+      this.destroyDrop();
+    }
 
-    return (
-      <div className="p-b-1">
-        <label className="small p-r-1">Y Axis:</label>
-        <button onClick={this.onToggleAxis} className={'btn btn-small ' + leftButtonClass}>
-          Left
-        </button>
-        <button onClick={this.onToggleAxis} className={'btn btn-small ' + rightButtonClass}>
-          Right
-        </button>
-      </div>
+    const { color, yaxis, onColorChange, onToggleAxis } = this.props;
+    const dropContent = (
+      <SeriesColorPickerPopover color={color} yaxis={yaxis} onColorChange={onColorChange} onToggleAxis={onToggleAxis} />
     );
+    const dropContentElem = document.createElement('div');
+    ReactDOM.render(dropContent, dropContentElem);
+
+    const drop = new Drop({
+      target: this.pickerElem,
+      content: dropContentElem,
+      position: 'top center',
+      classes: 'drop-popover',
+      openOn: 'hover',
+      hoverCloseDelay: 200,
+      remove: true,
+      tetherOptions: {
+        constraints: [{ to: 'scrollParent', attachment: 'none both' }],
+      },
+    });
+
+    drop.on('close', this.closeColorPicker.bind(this));
+
+    this.colorPickerDrop = drop;
+    this.colorPickerDrop.open();
+  };
+
+  closeColorPicker() {
+    setTimeout(() => {
+      this.destroyDrop();
+    }, 100);
+  }
+
+  destroyDrop() {
+    if (this.colorPickerDrop && this.colorPickerDrop.tether) {
+      this.colorPickerDrop.destroy();
+      this.colorPickerDrop = null;
+    }
   }
 
   render() {
+    const { optionalClass, children } = this.props;
     return (
-      <div className="graph-legend-popover">
-        {this.props.series.yaxis && this.renderAxisSelection()}
-        <ColorPickerPopover color={this.props.series.color} onColorSelect={this.onColorChange} />
+      <div className={optionalClass} ref={e => (this.pickerElem = e)} onClick={this.onClickToOpen}>
+        {children}
       </div>
     );
   }
 }
-
-react2AngularDirective('seriesColorPicker', SeriesColorPicker, ['series', 'onColorChange', 'onToggleAxis']);

+ 70 - 0
public/app/core/components/colorpicker/SeriesColorPickerPopover.tsx

@@ -0,0 +1,70 @@
+import React from 'react';
+import { ColorPickerPopover } from './ColorPickerPopover';
+import { react2AngularDirective } from 'app/core/utils/react2angular';
+
+export interface SeriesColorPickerPopoverProps {
+  color: string;
+  yaxis?: number;
+  onColorChange: (color: string) => void;
+  onToggleAxis?: () => void;
+}
+
+export class SeriesColorPickerPopover extends React.PureComponent<SeriesColorPickerPopoverProps, any> {
+  render() {
+    return (
+      <div className="graph-legend-popover">
+        {this.props.yaxis && <AxisSelector yaxis={this.props.yaxis} onToggleAxis={this.props.onToggleAxis} />}
+        <ColorPickerPopover color={this.props.color} onColorSelect={this.props.onColorChange} />
+      </div>
+    );
+  }
+}
+
+interface AxisSelectorProps {
+  yaxis: number;
+  onToggleAxis: () => void;
+}
+
+interface AxisSelectorState {
+  yaxis: number;
+}
+
+export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSelectorState> {
+  constructor(props) {
+    super(props);
+    this.state = {
+      yaxis: this.props.yaxis,
+    };
+    this.onToggleAxis = this.onToggleAxis.bind(this);
+  }
+
+  onToggleAxis() {
+    this.setState({
+      yaxis: this.state.yaxis === 2 ? 1 : 2,
+    });
+    this.props.onToggleAxis();
+  }
+
+  render() {
+    const leftButtonClass = this.state.yaxis === 1 ? 'btn-success' : 'btn-inverse';
+    const rightButtonClass = this.state.yaxis === 2 ? 'btn-success' : 'btn-inverse';
+
+    return (
+      <div className="p-b-1">
+        <label className="small p-r-1">Y Axis:</label>
+        <button onClick={this.onToggleAxis} className={'btn btn-small ' + leftButtonClass}>
+          Left
+        </button>
+        <button onClick={this.onToggleAxis} className={'btn btn-small ' + rightButtonClass}>
+          Right
+        </button>
+      </div>
+    );
+  }
+}
+
+react2AngularDirective('seriesColorPickerPopover', SeriesColorPickerPopover, [
+  'series',
+  'onColorChange',
+  'onToggleAxis',
+]);

+ 1 - 1
public/app/core/components/form_dropdown/form_dropdown.ts

@@ -88,7 +88,7 @@ export class FormDropdownCtrl {
       if (evt.keyCode === 13) {
         setTimeout(() => {
           this.inputElement.blur();
-        }, 100);
+        }, 300);
       }
     });
 

+ 46 - 0
public/app/core/copy/appNotification.ts

@@ -0,0 +1,46 @@
+import { AppNotification, AppNotificationSeverity, AppNotificationTimeout } from 'app/types';
+
+const defaultSuccessNotification: AppNotification = {
+  title: '',
+  text: '',
+  severity: AppNotificationSeverity.Success,
+  icon: 'fa fa-check',
+  timeout: AppNotificationTimeout.Success,
+};
+
+const defaultWarningNotification: AppNotification = {
+  title: '',
+  text: '',
+  severity: AppNotificationSeverity.Warning,
+  icon: 'fa fa-exclamation',
+  timeout: AppNotificationTimeout.Warning,
+};
+
+const defaultErrorNotification: AppNotification = {
+  title: '',
+  text: '',
+  severity: AppNotificationSeverity.Error,
+  icon: 'fa fa-exclamation-triangle',
+  timeout: AppNotificationTimeout.Error,
+};
+
+export const createSuccessNotification = (title: string, text?: string): AppNotification => ({
+  ...defaultSuccessNotification,
+  title: title,
+  text: text,
+  id: Date.now(),
+});
+
+export const createErrorNotification = (title: string, text?: string): AppNotification => ({
+  ...defaultErrorNotification,
+  title: title,
+  text: text,
+  id: Date.now(),
+});
+
+export const createWarningNotification = (title: string, text?: string): AppNotification => ({
+  ...defaultWarningNotification,
+  title: title,
+  text: text,
+  id: Date.now(),
+});

+ 1 - 1
public/app/core/core.ts

@@ -14,7 +14,7 @@ import './components/jsontree/jsontree';
 import './components/code_editor/code_editor';
 import './utils/outline';
 import './components/colorpicker/ColorPicker';
-import './components/colorpicker/SeriesColorPicker';
+import './components/colorpicker/SeriesColorPickerPopover';
 import './components/colorpicker/spectrum_picker';
 import './services/search_srv';
 import './services/ng_react';

+ 14 - 0
public/app/core/logs_model.ts

@@ -1,3 +1,5 @@
+import _ from 'lodash';
+
 export enum LogLevel {
   crit = 'crit',
   warn = 'warn',
@@ -27,3 +29,15 @@ export interface LogRow {
 export interface LogsModel {
   rows: LogRow[];
 }
+
+export function mergeStreams(streams: LogsModel[], limit?: number): LogsModel {
+  const combinedEntries = streams.reduce((acc, stream) => {
+    return [...acc, ...stream.rows];
+  }, []);
+  const sortedEntries = _.chain(combinedEntries)
+    .sortBy('timestamp')
+    .reverse()
+    .slice(0, limit || combinedEntries.length)
+    .value();
+  return { rows: sortedEntries };
+}

+ 51 - 0
public/app/core/reducers/appNotification.test.ts

@@ -0,0 +1,51 @@
+import { appNotificationsReducer } from './appNotification';
+import { ActionTypes } from '../actions/appNotification';
+import { AppNotificationSeverity, AppNotificationTimeout } from 'app/types/';
+
+describe('clear alert', () => {
+  it('should filter alert', () => {
+    const id1 = 1540301236048;
+    const id2 = 1540301248293;
+
+    const initialState = {
+      appNotifications: [
+        {
+          id: id1,
+          severity: AppNotificationSeverity.Success,
+          icon: 'success',
+          title: 'test',
+          text: 'test alert',
+          timeout: AppNotificationTimeout.Success,
+        },
+        {
+          id: id2,
+          severity: AppNotificationSeverity.Warning,
+          icon: 'warning',
+          title: 'test2',
+          text: 'test alert fail 2',
+          timeout: AppNotificationTimeout.Warning,
+        },
+      ],
+    };
+
+    const result = appNotificationsReducer(initialState, {
+      type: ActionTypes.ClearAppNotification,
+      payload: id2,
+    });
+
+    const expectedResult = {
+      appNotifications: [
+        {
+          id: id1,
+          severity: AppNotificationSeverity.Success,
+          icon: 'success',
+          title: 'test',
+          text: 'test alert',
+          timeout: AppNotificationTimeout.Success,
+        },
+      ],
+    };
+
+    expect(result).toEqual(expectedResult);
+  });
+});

+ 19 - 0
public/app/core/reducers/appNotification.ts

@@ -0,0 +1,19 @@
+import { AppNotification, AppNotificationsState } from 'app/types/';
+import { Action, ActionTypes } from '../actions/appNotification';
+
+export const initialState: AppNotificationsState = {
+  appNotifications: [] as AppNotification[],
+};
+
+export const appNotificationsReducer = (state = initialState, action: Action): AppNotificationsState => {
+  switch (action.type) {
+    case ActionTypes.AddAppNotification:
+      return { ...state, appNotifications: state.appNotifications.concat([action.payload]) };
+    case ActionTypes.ClearAppNotification:
+      return {
+        ...state,
+        appNotifications: state.appNotifications.filter(appNotification => appNotification.id !== action.payload),
+      };
+  }
+  return state;
+};

+ 4 - 0
public/app/core/reducers/index.ts

@@ -1,7 +1,11 @@
 import { navIndexReducer as navIndex } from './navModel';
 import { locationReducer as location } from './location';
+import { appNotificationsReducer as appNotifications } from './appNotification';
+import { userReducer as user } from './user';
 
 export default {
   navIndex,
   location,
+  appNotifications,
+  user,
 };

+ 15 - 0
public/app/core/reducers/user.ts

@@ -0,0 +1,15 @@
+import { DashboardSearchHit, UserState } from '../../types';
+import { Action, ActionTypes } from '../actions/user';
+
+const initialState: UserState = {
+  starredDashboards: [] as DashboardSearchHit[],
+};
+
+export const userReducer = (state: UserState = initialState, action: Action): UserState => {
+  switch (action.type) {
+    case ActionTypes.LoadStarredDashboards:
+      return { ...state, starredDashboards: action.payload };
+  }
+
+  return state;
+};

+ 4 - 0
public/app/core/services/AngularLoader.ts

@@ -4,6 +4,7 @@ import _ from 'lodash';
 
 export interface AngularComponent {
   destroy();
+  digest();
 }
 
 export class AngularLoader {
@@ -24,6 +25,9 @@ export class AngularLoader {
         scope.$destroy();
         compiledElem.remove();
       },
+      digest: () => {
+        scope.$digest();
+      },
     };
   }
 }

+ 4 - 92
public/app/core/services/alert_srv.ts

@@ -1,100 +1,12 @@
-import angular from 'angular';
-import _ from 'lodash';
 import coreModule from 'app/core/core_module';
-import appEvents from 'app/core/app_events';
 
 export class AlertSrv {
-  list: any[];
+  constructor() {}
 
-  /** @ngInject */
-  constructor(private $timeout, private $rootScope) {
-    this.list = [];
-  }
-
-  init() {
-    this.$rootScope.onAppEvent(
-      'alert-error',
-      (e, alert) => {
-        this.set(alert[0], alert[1], 'error', 12000);
-      },
-      this.$rootScope
-    );
-
-    this.$rootScope.onAppEvent(
-      'alert-warning',
-      (e, alert) => {
-        this.set(alert[0], alert[1], 'warning', 5000);
-      },
-      this.$rootScope
-    );
-
-    this.$rootScope.onAppEvent(
-      'alert-success',
-      (e, alert) => {
-        this.set(alert[0], alert[1], 'success', 3000);
-      },
-      this.$rootScope
-    );
-
-    appEvents.on('alert-warning', options => this.set(options[0], options[1], 'warning', 5000));
-    appEvents.on('alert-success', options => this.set(options[0], options[1], 'success', 3000));
-    appEvents.on('alert-error', options => this.set(options[0], options[1], 'error', 7000));
-  }
-
-  getIconForSeverity(severity) {
-    switch (severity) {
-      case 'success':
-        return 'fa fa-check';
-      case 'error':
-        return 'fa fa-exclamation-triangle';
-      default:
-        return 'fa fa-exclamation';
-    }
-  }
-
-  set(title, text, severity, timeout) {
-    if (_.isObject(text)) {
-      console.log('alert error', text);
-      if (text.statusText) {
-        text = `HTTP Error (${text.status}) ${text.statusText}`;
-      }
-    }
-
-    const newAlert = {
-      title: title || '',
-      text: text || '',
-      severity: severity || 'info',
-      icon: this.getIconForSeverity(severity),
-    };
-
-    const newAlertJson = angular.toJson(newAlert);
-
-    // remove same alert if it already exists
-    _.remove(this.list, value => {
-      return angular.toJson(value) === newAlertJson;
-    });
-
-    this.list.push(newAlert);
-    if (timeout > 0) {
-      this.$timeout(() => {
-        this.list = _.without(this.list, newAlert);
-      }, timeout);
-    }
-
-    if (!this.$rootScope.$$phase) {
-      this.$rootScope.$digest();
-    }
-
-    return newAlert;
-  }
-
-  clear(alert) {
-    this.list = _.without(this.list, alert);
-  }
-
-  clearAll() {
-    this.list = [];
+  set() {
+    console.log('old depricated alert srv being used');
   }
 }
 
+// this is just added to not break old plugins that might be using it
 coreModule.service('alertSrv', AlertSrv);

+ 7 - 6
public/app/core/services/backend_srv.ts

@@ -9,7 +9,7 @@ export class BackendSrv {
   private noBackendCache: boolean;
 
   /** @ngInject */
-  constructor(private $http, private alertSrv, private $q, private $timeout, private contextSrv) {}
+  constructor(private $http, private $q, private $timeout, private contextSrv) {}
 
   get(url, params?) {
     return this.request({ method: 'GET', url: url, params: params });
@@ -49,14 +49,14 @@ export class BackendSrv {
     }
 
     if (err.status === 422) {
-      this.alertSrv.set('Validation failed', data.message, 'warning', 4000);
+      appEvents.emit('alert-warning', ['Validation failed', data.message]);
       throw data;
     }
 
-    data.severity = 'error';
+    let severity = 'error';
 
     if (err.status < 500) {
-      data.severity = 'warning';
+      severity = 'warning';
     }
 
     if (data.message) {
@@ -66,7 +66,8 @@ export class BackendSrv {
         description = message;
         message = 'Error';
       }
-      this.alertSrv.set(message, description, data.severity, 10000);
+
+      appEvents.emit('alert-' + severity, [message, description]);
     }
 
     throw data;
@@ -93,7 +94,7 @@ export class BackendSrv {
         if (options.method !== 'GET') {
           if (results && results.data.message) {
             if (options.showSuccessAlert !== false) {
-              this.alertSrv.set(results.data.message, '', 'success', 3000);
+              appEvents.emit('alert-success', [results.data.message]);
             }
           }
         }

+ 1 - 1
public/app/core/services/bridge_srv.ts

@@ -1,6 +1,6 @@
 import coreModule from 'app/core/core_module';
 import appEvents from 'app/core/app_events';
-import { store } from 'app/store/configureStore';
+import { store } from 'app/store/store';
 import locationUtil from 'app/core/utils/location_util';
 import { updateLocation } from 'app/core/actions';
 

+ 1 - 1
public/app/core/specs/backend_srv.test.ts

@@ -9,7 +9,7 @@ describe('backend_srv', () => {
     return Promise.resolve({});
   };
 
-  const _backendSrv = new BackendSrv(_httpBackend, {}, {}, {}, {});
+  const _backendSrv = new BackendSrv(_httpBackend, {}, {}, {});
 
   describe('when handling errors', () => {
     it('should return the http status code', async () => {

+ 4 - 0
public/app/core/table_model.ts

@@ -83,6 +83,10 @@ function areRowsMatching(columns, row, otherRow) {
 export function mergeTablesIntoModel(dst?: TableModel, ...tables: TableModel[]): TableModel {
   const model = dst || new TableModel();
 
+  if (arguments.length === 1) {
+    return model;
+  }
+
   // Single query returns data columns and rows as is
   if (arguments.length === 2) {
     model.columns = [...tables[0].columns];

+ 11 - 0
public/app/core/utils/connectWithReduxStore.tsx

@@ -0,0 +1,11 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import { store } from '../../store/store';
+
+export function connectWithStore(WrappedComponent, ...args) {
+  const ConnectedWrappedComponent = connect(...args)(WrappedComponent);
+
+  return props => {
+    return <ConnectedWrappedComponent {...props} store={store} />;
+  };
+}

+ 4 - 1
public/app/core/utils/rangeutil.ts

@@ -1,5 +1,8 @@
 import _ from 'lodash';
 import moment from 'moment';
+
+import { RawTimeRange } from 'app/types/series';
+
 import * as dateMath from './datemath';
 
 const spans = {
@@ -129,7 +132,7 @@ export function describeTextRange(expr: any) {
   return opt;
 }
 
-export function describeTimeRange(range) {
+export function describeTimeRange(range: RawTimeRange): string {
   const option = rangeIndex[range.from.toString() + ' to ' + range.to.toString()];
   if (option) {
     return option.display;

+ 2 - 2
public/app/features/alerting/AlertTabCtrl.ts

@@ -166,7 +166,7 @@ export class AlertTabCtrl {
 
     alert.noDataState = alert.noDataState || config.alertingNoDataOrNullValues;
     alert.executionErrorState = alert.executionErrorState || config.alertingErrorOrTimeout;
-    alert.frequency = alert.frequency || '60s';
+    alert.frequency = alert.frequency || '1m';
     alert.handler = alert.handler || 1;
     alert.notifications = alert.notifications || [];
 
@@ -217,7 +217,7 @@ export class AlertTabCtrl {
   buildDefaultCondition() {
     return {
       type: 'query',
-      query: { params: ['A', '5m', 'now'] },
+      query: { params: ['A', '15m', 'now'] },
       reducer: { type: 'avg', params: [] },
       evaluator: { type: 'gt', params: [null] },
       operator: { type: 'and' },

+ 3 - 0
public/app/features/all.ts

@@ -9,3 +9,6 @@ import './admin';
 import './alerting/NotificationsEditCtrl';
 import './alerting/NotificationsListCtrl';
 import './manage-dashboards';
+import './teams/CreateTeamCtrl';
+import './profile/ProfileCtrl';
+import './profile/ChangePasswordCtrl';

+ 18 - 11
public/app/features/annotations/annotations_srv.ts

@@ -1,25 +1,32 @@
-import './editor_ctrl';
-
+// Libaries
 import angular from 'angular';
 import _ from 'lodash';
+
+// Components
+import './editor_ctrl';
 import coreModule from 'app/core/core_module';
+
+// Utils & Services
 import { makeRegions, dedupAnnotations } from './events_processing';
 
+// Types
+import { DashboardModel } from '../dashboard/dashboard_model';
+
 export class AnnotationsSrv {
   globalAnnotationsPromise: any;
   alertStatesPromise: any;
   datasourcePromises: any;
 
   /** @ngInject */
-  constructor(private $rootScope, private $q, private datasourceSrv, private backendSrv, private timeSrv) {
-    $rootScope.onAppEvent('refresh', this.clearCache.bind(this), $rootScope);
-    $rootScope.onAppEvent('dashboard-initialized', this.clearCache.bind(this), $rootScope);
-  }
-
-  clearCache() {
-    this.globalAnnotationsPromise = null;
-    this.alertStatesPromise = null;
-    this.datasourcePromises = null;
+  constructor(private $rootScope, private $q, private datasourceSrv, private backendSrv, private timeSrv) {}
+
+  init(dashboard: DashboardModel) {
+    // clear promises on refresh events
+    dashboard.on('refresh', () => {
+      this.globalAnnotationsPromise = null;
+      this.alertStatesPromise = null;
+      this.datasourcePromises = null;
+    });
   }
 
   getAnnotations(options) {

部分文件因文件數量過多而無法顯示