Bläddra i källkod

Merge remote-tracking branch 'grafana/master'

* grafana/master: (52 commits)
  Moved tooltip icon from input to label #12945 (#13059)
  added empty cta to playlist page + hid playlist table when empty (#12841)
  Update provisioning.md
  changelog: add notes about closing #12865
  fixed so validation of empty fields works again
  tslint: tslint to const fixes part3 (#13036)
  tslint: more const fixes (#13035)
  tslint: changing vars -> const (#13034)
  tslint: autofix of let -> const (#13033)
  WIP Update tslint (#12922)
  changelog: add notes about closing #12952 #12965
  build: fixes rpm build when using defaults.
  changelog: add notes about closing #12486
  docs: changes
  fixed so animation starts as soon as one pushes the button and animation stops if login failed
  added link to getting started to all, changed wording
  tests: fix missing tests (with .jest suffix)
  heatmap: fix tooltip bug in firefox
  Update notifications.md
  sql: added code migration type
  ...
ryan 7 år sedan
förälder
incheckning
8cfad74af3
100 ändrade filer med 1352 tillägg och 396 borttagningar
  1. 1 1
      .gitignore
  2. 7 0
      CHANGELOG.md
  3. 15 18
      build.go
  4. 1 1
      devenv/bulk-dashboards/bulk-dashboards.yaml
  5. 207 17
      devenv/dev-dashboards/datasource_tests_mssql_unittest.json
  6. 199 15
      devenv/dev-dashboards/datasource_tests_mysql_unittest.json
  7. 187 15
      devenv/dev-dashboards/datasource_tests_postgres_unittest.json
  8. 2 2
      devenv/setup.sh
  9. 1 1
      docs/sources/administration/provisioning.md
  10. 2 1
      docs/sources/alerting/notifications.md
  11. 2 2
      docs/sources/features/datasources/elasticsearch.md
  12. 2 0
      docs/sources/features/datasources/mssql.md
  13. 2 0
      docs/sources/features/datasources/mysql.md
  14. 2 0
      docs/sources/features/datasources/postgres.md
  15. 2 2
      docs/sources/guides/basic_concepts.md
  16. 29 1
      docs/sources/guides/getting_started.md
  17. 5 0
      docs/sources/installation/debian.md
  18. 13 3
      docs/sources/installation/docker.md
  19. 4 0
      docs/sources/installation/mac.md
  20. 4 0
      docs/sources/installation/rpm.md
  21. 5 0
      docs/sources/installation/windows.md
  22. 5 0
      docs/sources/project/building_from_source.md
  23. 22 8
      docs/sources/reference/templating.md
  24. 3 3
      package.json
  25. 2 2
      packaging/docker/push_to_docker_hub.sh
  26. 7 1
      pkg/api/login.go
  27. 8 2
      pkg/api/pluginproxy/ds_proxy.go
  28. 20 1
      pkg/api/pluginproxy/ds_proxy_test.go
  29. 1 1
      pkg/services/alerting/notifiers/slack.go
  30. 3 3
      pkg/services/provisioning/datasources/config_reader.go
  31. 14 0
      pkg/services/provisioning/datasources/config_reader_test.go
  32. 1 1
      pkg/services/provisioning/datasources/datasources.go
  33. 25 0
      pkg/services/provisioning/datasources/testdata/multiple-org-default/config.yaml
  34. 40 1
      pkg/services/sqlstore/migrations/user_mig.go
  35. 11 5
      pkg/services/sqlstore/migrator/migrator.go
  36. 7 0
      pkg/services/sqlstore/migrator/types.go
  37. 3 2
      pkg/services/sqlstore/user.go
  38. 22 0
      pkg/services/sqlstore/user_test.go
  39. 21 0
      pkg/tsdb/mssql/macros.go
  40. 12 0
      pkg/tsdb/mssql/macros_test.go
  41. 21 0
      pkg/tsdb/mysql/macros.go
  42. 12 0
      pkg/tsdb/mysql/macros_test.go
  43. 21 0
      pkg/tsdb/postgres/macros.go
  44. 12 0
      pkg/tsdb/postgres/macros_test.go
  45. 5 5
      public/app/app.ts
  46. 1 1
      public/app/containers/AlertRuleList/AlertRuleList.test.tsx
  47. 6 6
      public/app/containers/AlertRuleList/AlertRuleList.tsx
  48. 2 2
      public/app/containers/ContainerProps.ts
  49. 16 5
      public/app/containers/Explore/Explore.tsx
  50. 4 4
      public/app/containers/Explore/Graph.tsx
  51. 19 0
      public/app/containers/Explore/PromQueryField.test.tsx
  52. 11 5
      public/app/containers/Explore/PromQueryField.tsx
  53. 1 1
      public/app/containers/Explore/QueryField.tsx
  54. 1 1
      public/app/containers/Explore/Table.tsx
  55. 45 17
      public/app/containers/Explore/utils/prometheus.test.ts
  56. 11 8
      public/app/containers/Explore/utils/prometheus.ts
  57. 2 2
      public/app/containers/ManageDashboards/FolderPermissions.tsx
  58. 2 2
      public/app/containers/ManageDashboards/FolderSettings.tsx
  59. 2 2
      public/app/containers/ServerStats/ServerStats.tsx
  60. 4 4
      public/app/containers/Teams/TeamGroupSync.tsx
  61. 4 4
      public/app/containers/Teams/TeamList.tsx
  62. 5 5
      public/app/containers/Teams/TeamMembers.tsx
  63. 2 2
      public/app/containers/Teams/TeamPages.tsx
  64. 2 2
      public/app/containers/Teams/TeamSettings.tsx
  65. 30 27
      public/app/core/components/EmptyListCTA/EmptyListCTA.tsx
  66. 4 4
      public/app/core/components/PageHeader/PageHeader.tsx
  67. 8 11
      public/app/core/components/PasswordStrength.tsx
  68. 1 1
      public/app/core/components/Permissions/AddPermissions.test.tsx
  69. 2 2
      public/app/core/components/Permissions/DisabledPermissionsListItem.tsx
  70. 2 2
      public/app/core/components/Permissions/Permissions.tsx
  71. 2 2
      public/app/core/components/Permissions/PermissionsList.tsx
  72. 2 2
      public/app/core/components/Picker/DescriptionOption.tsx
  73. 2 2
      public/app/core/components/Picker/PickerOption.tsx
  74. 2 2
      public/app/core/components/TagFilter/TagBadge.tsx
  75. 3 3
      public/app/core/components/TagFilter/TagFilter.tsx
  76. 2 2
      public/app/core/components/TagFilter/TagOption.tsx
  77. 2 2
      public/app/core/components/TagFilter/TagValue.tsx
  78. 2 2
      public/app/core/components/Tooltip/Popover.tsx
  79. 2 2
      public/app/core/components/Tooltip/Tooltip.tsx
  80. 15 15
      public/app/core/components/code_editor/code_editor.ts
  81. 4 4
      public/app/core/components/colorpicker/ColorPalette.tsx
  82. 4 4
      public/app/core/components/colorpicker/ColorPicker.tsx
  83. 27 22
      public/app/core/components/colorpicker/ColorPickerPopover.tsx
  84. 2 2
      public/app/core/components/colorpicker/SeriesColorPicker.tsx
  85. 14 14
      public/app/core/components/colorpicker/SpectrumPicker.tsx
  86. 6 6
      public/app/core/components/form_dropdown/form_dropdown.ts
  87. 1 1
      public/app/core/components/grafana_app.ts
  88. 7 7
      public/app/core/components/info_popover.ts
  89. 8 8
      public/app/core/components/manage_dashboards/manage_dashboards.ts
  90. 3 3
      public/app/core/components/scroll/scroll.ts
  91. 1 1
      public/app/core/components/search/SearchResult.tsx
  92. 2 2
      public/app/core/components/sidemenu/sidemenu.ts
  93. 3 3
      public/app/core/config.ts
  94. 5 5
      public/app/core/controllers/inspect_ctrl.ts
  95. 1 1
      public/app/core/controllers/json_editor_ctrl.ts
  96. 21 13
      public/app/core/controllers/login_ctrl.ts
  97. 1 1
      public/app/core/controllers/reset_password_ctrl.ts
  98. 1 1
      public/app/core/controllers/signup_ctrl.ts
  99. 16 16
      public/app/core/directives/dropdown_typeahead.ts
  100. 14 14
      public/app/core/directives/metric_segment.ts

+ 1 - 1
.gitignore

@@ -71,4 +71,4 @@ debug.test
 /vendor/**/appengine*
 *.orig
 
-/devenv/dashboards/bulk-testing/*.json
+/devenv/bulk-dashboards/*.json

+ 7 - 0
CHANGELOG.md

@@ -20,6 +20,10 @@
 * **Prometheus**: Heatmap - fix unhandled error when some points are missing [#12484](https://github.com/grafana/grafana/issues/12484)
 * **Prometheus**: Add $__interval, $__interval_ms, $__range, $__range_s & $__range_ms support for dashboard and template queries [#12597](https://github.com/grafana/grafana/issues/12597) [#12882](https://github.com/grafana/grafana/issues/12882), thx [@roidelapluie](https://github.com/roidelapluie)
 * **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda)
+* **Variables**: Limit amount of queries executed when updating variable that other variable(s) are dependent on [#11890](https://github.com/grafana/grafana/issues/11890)
+* **Variables**: Support query variable refresh when another variable referenced in `Regex` field change its value [#12952](https://github.com/grafana/grafana/issues/12952), thx [@franciscocpg](https://github.com/franciscocpg)
+* **Variables**: Support variables in query variable `Custom all value` field [#12965](https://github.com/grafana/grafana/issues/12965), thx [@franciscocpg](https://github.com/franciscocpg)
+* **Postgres/MySQL/MSSQL**: New $__unixEpochGroup and $__unixEpochGroupAlias macros [#12892](https://github.com/grafana/grafana/issues/12892), thx [@svenklemm](https://github.com/svenklemm)
 * **Postgres/MySQL/MSSQL**: Add previous fill mode to $__timeGroup macro which will fill in previously seen value when point is missing [#12756](https://github.com/grafana/grafana/issues/12756), thx [@svenklemm](https://github.com/svenklemm)
 * **Postgres/MySQL/MSSQL**: Use floor rounding in $__timeGroup macro function [#12460](https://github.com/grafana/grafana/issues/12460), thx [@svenklemm](https://github.com/svenklemm)
 * **Postgres/MySQL/MSSQL**: Use metric column as prefix when returning multiple value columns [#12727](https://github.com/grafana/grafana/issues/12727), thx [@svenklemm](https://github.com/svenklemm)
@@ -51,6 +55,9 @@ om/grafana/grafana/issues/12668)
 * **Docker**: Make it possible to set a specific plugin url [#12861](https://github.com/grafana/grafana/pull/12861), thx [ClementGautier](https://github.com/ClementGautier)
 * **Graphite**: Fix for quoting of int function parameters (when using variables) [#11927](https://github.com/grafana/grafana/pull/11927)
 * **InfluxDB**: Support timeFilter in query templating for InfluxDB [#12598](https://github.com/grafana/grafana/pull/12598), thx [kichristensen](https://github.com/kichristensen)
+* **Provisioning**: Should allow one default datasource per organisation [#12229](https://github.com/grafana/grafana/issues/12229)
+* **Heatmap**: Fix broken tooltip and crosshair on Firefox [#12486](https://github.com/grafana/grafana/issues/12486)
+* **Login**: Show loading animation while waiting for authentication response on login [#12865](https://github.com/grafana/grafana/issues/12865)
 
 ### Breaking changes
 

+ 15 - 18
build.go

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

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

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

+ 207 - 17
devenv/dev-dashboards/datasource_tests_mssql_unittest.json

@@ -64,7 +64,7 @@
   "editable": true,
   "gnetId": null,
   "graphTooltip": 0,
-  "iteration": 1533713720618,
+  "iteration": 1534507501976,
   "links": [],
   "panels": [
     {
@@ -1197,6 +1197,196 @@
         "x": 0,
         "y": 27
       },
+      "id": 38,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "hideEmpty": false,
+        "hideZero": false,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT \n  $__unixEpochGroupAlias(timeInt32, '$summarize'), \n  measurement as metric, \n  avg(valueOne) as valueOne,\n  avg(valueTwo) as valueTwo\nFROM\n  metric_values \nWHERE\n  $__unixEpochFilter(timeInt32) AND\n  ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n  $__unixEpochGroup(timeInt32, '$summarize'), \n  measurement \nORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series with metric column using unixEpochGroup macro ($summarize)",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-mssql-ds-tests",
+      "fill": 2,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 27
+      },
+      "id": 39,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [
+        {
+          "alias": "MovingAverageValueOne",
+          "dashes": true,
+          "lines": false
+        },
+        {
+          "alias": "MovingAverageValueTwo",
+          "dashes": true,
+          "lines": false,
+          "yaxis": 1
+        }
+      ],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT \n  $__unixEpochGroupAlias(timeInt32, '$summarize'), \n  avg(valueOne) as valueOne,\n  avg(valueTwo) as valueTwo\nFROM\n  metric_values \nWHERE\n  $__unixEpochFilter(timeInt32) AND\n  ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n  $__unixEpochGroup(timeInt32, '$summarize')\nORDER BY 1",
+          "refId": "A"
+        },
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT \n  time,\n  avg(valueOne) OVER (ORDER BY time ROWS BETWEEN 6 PRECEDING AND 6 FOLLOWING) as MovingAverageValueOne,\n  avg(valueTwo) OVER (ORDER BY time ROWS BETWEEN 6 PRECEDING AND 6 FOLLOWING) as MovingAverageValueTwo\nFROM\n  metric_values \nWHERE \n  $__timeFilter(time) AND \n  ($metric = 'ALL' OR measurement = $metric)\nORDER BY 1",
+          "refId": "B"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series without metric column using unixEpochGroup macro ($summarize)",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-mssql-ds-tests",
+      "fill": 2,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 35
+      },
       "id": 4,
       "legend": {
         "alignAsTable": true,
@@ -1282,7 +1472,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 27
+        "y": 35
       },
       "id": 28,
       "legend": {
@@ -1367,7 +1557,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 35
+        "y": 43
       },
       "id": 19,
       "legend": {
@@ -1454,7 +1644,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 35
+        "y": 43
       },
       "id": 18,
       "legend": {
@@ -1539,7 +1729,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 43
+        "y": 51
       },
       "id": 17,
       "legend": {
@@ -1626,7 +1816,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 43
+        "y": 51
       },
       "id": 20,
       "legend": {
@@ -1711,7 +1901,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 51
+        "y": 59
       },
       "id": 29,
       "legend": {
@@ -1798,7 +1988,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 51
+        "y": 59
       },
       "id": 30,
       "legend": {
@@ -1885,7 +2075,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 59
+        "y": 67
       },
       "id": 14,
       "legend": {
@@ -1973,7 +2163,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 59
+        "y": 67
       },
       "id": 15,
       "legend": {
@@ -2060,7 +2250,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 67
+        "y": 75
       },
       "id": 25,
       "legend": {
@@ -2148,7 +2338,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 67
+        "y": 75
       },
       "id": 22,
       "legend": {
@@ -2235,7 +2425,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 75
+        "y": 83
       },
       "id": 21,
       "legend": {
@@ -2323,7 +2513,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 75
+        "y": 83
       },
       "id": 26,
       "legend": {
@@ -2410,7 +2600,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 83
+        "y": 91
       },
       "id": 23,
       "legend": {
@@ -2498,7 +2688,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 83
+        "y": 91
       },
       "id": 24,
       "legend": {
@@ -2708,5 +2898,5 @@
   "timezone": "",
   "title": "Datasource tests - MSSQL (unit test)",
   "uid": "GlAqcPgmz",
-  "version": 10
+  "version": 2
 }

+ 199 - 15
devenv/dev-dashboards/datasource_tests_mysql_unittest.json

@@ -64,7 +64,7 @@
   "editable": true,
   "gnetId": null,
   "graphTooltip": 0,
-  "iteration": 1533714324007,
+  "iteration": 1534508678095,
   "links": [],
   "panels": [
     {
@@ -1191,6 +1191,190 @@
         "x": 0,
         "y": 27
       },
+      "id": 38,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "hideEmpty": false,
+        "hideZero": false,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT \n  $__unixEpochGroupAlias(timeInt32, '$summarize'), \n  measurement, \n  avg(valueOne) as valueOne,\n  avg(valueTwo) as valueTwo\nFROM\n  metric_values \nWHERE\n  $__unixEpochFilter(timeInt32) AND\n  measurement in($metric)\nGROUP BY 1, 2\nORDER BY 1, 2",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series with metric column using unixEpochGroup macro ($summarize)",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-mysql-ds-tests",
+      "fill": 2,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 27
+      },
+      "id": 39,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [
+        {
+          "alias": "MovingAverageValueOne",
+          "dashes": true,
+          "lines": false
+        },
+        {
+          "alias": "MovingAverageValueTwo",
+          "dashes": true,
+          "lines": false,
+          "yaxis": 1
+        }
+      ],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT \n  $__unixEpochGroupAlias(timeInt32, '$summarize'), \n  avg(valueOne) as valueOne,\n  avg(valueTwo) as valueTwo\nFROM\n  metric_values \nWHERE\n  $__unixEpochFilter(timeInt32) AND\n  measurement in($metric)\nGROUP BY 1\nORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series without metric column using unixEpochGroup macro ($summarize)",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-mysql-ds-tests",
+      "fill": 2,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 35
+      },
       "id": 4,
       "legend": {
         "alignAsTable": true,
@@ -1276,7 +1460,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 27
+        "y": 35
       },
       "id": 28,
       "legend": {
@@ -1361,7 +1545,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 35
+        "y": 43
       },
       "id": 19,
       "legend": {
@@ -1448,7 +1632,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 35
+        "y": 43
       },
       "id": 18,
       "legend": {
@@ -1533,7 +1717,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 43
+        "y": 51
       },
       "id": 17,
       "legend": {
@@ -1620,7 +1804,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 43
+        "y": 51
       },
       "id": 20,
       "legend": {
@@ -1705,7 +1889,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 51
+        "y": 59
       },
       "id": 14,
       "legend": {
@@ -1793,7 +1977,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 51
+        "y": 59
       },
       "id": 15,
       "legend": {
@@ -1880,7 +2064,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 59
+        "y": 67
       },
       "id": 25,
       "legend": {
@@ -1968,7 +2152,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 59
+        "y": 67
       },
       "id": 22,
       "legend": {
@@ -2055,7 +2239,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 67
+        "y": 75
       },
       "id": 21,
       "legend": {
@@ -2143,7 +2327,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 67
+        "y": 75
       },
       "id": 26,
       "legend": {
@@ -2230,7 +2414,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 75
+        "y": 83
       },
       "id": 23,
       "legend": {
@@ -2318,7 +2502,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 75
+        "y": 83
       },
       "id": 24,
       "legend": {
@@ -2526,5 +2710,5 @@
   "timezone": "",
   "title": "Datasource tests - MySQL (unittest)",
   "uid": "Hmf8FDkmz",
-  "version": 9
+  "version": 2
 }

+ 187 - 15
devenv/dev-dashboards/datasource_tests_postgres_unittest.json

@@ -64,7 +64,7 @@
   "editable": true,
   "gnetId": null,
   "graphTooltip": 0,
-  "iteration": 1533714184500,
+  "iteration": 1534507993194,
   "links": [],
   "panels": [
     {
@@ -1179,6 +1179,178 @@
         "x": 0,
         "y": 27
       },
+      "id": 38,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "hideEmpty": false,
+        "hideZero": false,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT \n  $__unixEpochGroupAlias(\"timeInt32\", '$summarize'), \n  measurement, \n  avg(\"valueOne\") as \"valueOne\",\n  avg(\"valueTwo\") as \"valueTwo\"\nFROM\n  metric_values \nWHERE\n  $__unixEpochFilter(\"timeInt32\") AND\n  measurement in($metric)\nGROUP BY 1, 2\nORDER BY 1, 2",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series with metric column using unixEpochGroup macro ($summarize)",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-postgres-ds-tests",
+      "fill": 2,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 27
+      },
+      "id": 39,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT \n  $__unixEpochGroupAlias(\"timeInt32\", '$summarize'), \n  avg(\"valueOne\") as \"valueOne\",\n  avg(\"valueTwo\") as \"valueTwo\"\nFROM\n  metric_values \nWHERE\n  $__unixEpochFilter(\"timeInt32\") AND\n  measurement in($metric)\nGROUP BY 1\nORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series without metric column using timeGroup macro ($summarize)",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-postgres-ds-tests",
+      "fill": 2,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 35
+      },
       "id": 4,
       "legend": {
         "alignAsTable": true,
@@ -1264,7 +1436,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 27
+        "y": 35
       },
       "id": 28,
       "legend": {
@@ -1349,7 +1521,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 35
+        "y": 43
       },
       "id": 19,
       "legend": {
@@ -1436,7 +1608,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 35
+        "y": 43
       },
       "id": 18,
       "legend": {
@@ -1521,7 +1693,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 43
+        "y": 51
       },
       "id": 17,
       "legend": {
@@ -1608,7 +1780,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 43
+        "y": 51
       },
       "id": 20,
       "legend": {
@@ -1693,7 +1865,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 51
+        "y": 59
       },
       "id": 14,
       "legend": {
@@ -1781,7 +1953,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 51
+        "y": 59
       },
       "id": 15,
       "legend": {
@@ -1868,7 +2040,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 59
+        "y": 67
       },
       "id": 25,
       "legend": {
@@ -1956,7 +2128,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 59
+        "y": 67
       },
       "id": 22,
       "legend": {
@@ -2043,7 +2215,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 67
+        "y": 75
       },
       "id": 21,
       "legend": {
@@ -2131,7 +2303,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 67
+        "y": 75
       },
       "id": 26,
       "legend": {
@@ -2218,7 +2390,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 75
+        "y": 83
       },
       "id": 23,
       "legend": {
@@ -2306,7 +2478,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 75
+        "y": 83
       },
       "id": 24,
       "legend": {
@@ -2518,5 +2690,5 @@
   "timezone": "",
   "title": "Datasource tests - Postgres (unittest)",
   "uid": "vHQdlVziz",
-  "version": 9
+  "version": 1
 }

+ 2 - 2
devenv/setup.sh

@@ -7,11 +7,11 @@ bulkDashboard() {
 		COUNTER=0
 		MAX=400
 		while [  $COUNTER -lt $MAX ]; do
-				jsonnet -o "dashboards/bulk-testing/dashboard${COUNTER}.json" -e "local bulkDash = import 'dashboards/bulk-testing/bulkdash.jsonnet'; bulkDash + {  uid: 'uid-${COUNTER}',  title: 'title-${COUNTER}' }"
+				jsonnet -o "bulk-dashboards/dashboard${COUNTER}.json" -e "local bulkDash = import 'bulk-dashboards/bulkdash.jsonnet'; bulkDash + {  uid: 'uid-${COUNTER}',  title: 'title-${COUNTER}' }"
 				let COUNTER=COUNTER+1
 		done
 
-		ln -s -f -r ./dashboards/bulk-testing/bulk-dashboards.yaml ../conf/provisioning/dashboards/custom.yaml
+		ln -s -f -r ./bulk-dashboards/bulk-dashboards.yaml ../conf/provisioning/dashboards/custom.yaml
 }
 
 requiresJsonnet() {

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

@@ -155,7 +155,7 @@ Since not all datasources have the same configuration settings we only have the
 | tlsSkipVerify | boolean | *All* | Controls whether a client verifies the server's certificate chain and host name. |
 | graphiteVersion | string | Graphite |  Graphite version  |
 | timeInterval | string | Elastic, InfluxDB & Prometheus | Lowest interval/step value that should be used for this data source |
-| esVersion | string | Elastic | Elasticsearch version as an number (2/5/56) |
+| esVersion | number | Elastic | Elasticsearch version as an number (2/5/56) |
 | timeField | string | Elastic | Which field that should be used as timestamp |
 | interval | string | Elastic | Index date time format |
 | authType | string | Cloudwatch | Auth provider. keys/credentials/arn |

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

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

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

@@ -58,8 +58,8 @@ a time pattern for the index name or a wildcard.
 
 ### Elasticsearch version
 
-Be sure to specify your Elasticsearch version in the version selection dropdown. This is very important as there are differences how queries are composed. Currently only 2.x and 5.x
-are supported.
+Be sure to specify your Elasticsearch version in the version selection dropdown. This is very important as there are differences how queries are composed.
+Currently the versions available is 2.x, 5.x and 5.6+ where 5.6+ means a version of 5.6 or higher, 6.3.2 for example.
 
 ### Min time interval
 A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example `1m` if your data is written every minute.

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

@@ -88,6 +88,8 @@ Macro example | Description
 *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183*
 *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
 *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183*
+*$__unixEpochGroup(dateColumn,'5m', [fillmode])* | Same as $__timeGroup but for times stored as unix timestamp (only available in Grafana 5.3+).
+*$__unixEpochGroupAlias(dateColumn,'5m', [fillmode])* | Same as above but also adds a column alias (only available in Grafana 5.3+).
 
 We plan to add many more macros. If you have suggestions for what macros you would like to see, please [open an issue](https://github.com/grafana/grafana) in our GitHub repo.
 

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

@@ -71,6 +71,8 @@ Macro example | Description
 *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183*
 *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
 *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183*
+*$__unixEpochGroup(dateColumn,'5m', [fillmode])* | Same as $__timeGroup but for times stored as unix timestamp (only available in Grafana 5.3+).
+*$__unixEpochGroupAlias(dateColumn,'5m', [fillmode])* | Same as above but also adds a column alias (only available in Grafana 5.3+).
 
 We plan to add many more macros. If you have suggestions for what macros you would like to see, please [open an issue](https://github.com/grafana/grafana) in our GitHub repo.
 

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

@@ -69,6 +69,8 @@ Macro example | Description
 *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn >= 1494410783 AND dateColumn <= 1494497183*
 *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
 *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183*
+*$__unixEpochGroup(dateColumn,'5m', [fillmode])* | Same as $__timeGroup but for times stored as unix timestamp (only available in Grafana 5.3+).
+*$__unixEpochGroupAlias(dateColumn,'5m', [fillmode])* | Same as above but also adds a column alias (only available in Grafana 5.3+).
 
 We plan to add many more macros. If you have suggestions for what macros you would like to see, please [open an issue](https://github.com/grafana/grafana) in our GitHub repo.
 

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

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

+ 29 - 1
docs/sources/guides/getting_started.md

@@ -13,7 +13,35 @@ weight = 1
 
 # Getting started
 
-This guide will help you get started and acquainted with Grafana. It assumes you have a working Grafana server up and running and have added at least one [Data Source](/features/datasources/).
+This guide will help you get started and acquainted with Grafana. It assumes you have a working Grafana server up and running. If not please read the [installation guide](/installation/).
+
+## Logging in for the first time
+
+To run Grafana open your browser and go to http://localhost:3000/. 3000 is the default http port that Grafana listens to if you haven't [configured a different port](/installation/configuration/#http-port).
+
+There you will see the login page. Default username is admin and default password is admin. When you log in for the first time you will be asked to change your password. We strongly encourage you to
+follow Grafana’s best practices and change the default administrator password. You can later go to user preferences and change your user name.
+
+
+## How to add a data source
+
+{{< docs-imagebox img="/img/docs/v52/sidemenu-datasource.png" max-width="250px" class="docs-image--right docs-image--no-shadow">}}
+
+Before you create your first dashboard you need to add your data source.
+
+First move your cursor to the cog on the side menu which will show you the configuration menu. If the side menu is not visible click the Grafana icon in the upper left corner. The first item on the configuration menu is data sources, click on that and you'll be taken to the data sources page where you can add and edit data sources. You can also simply click the cog.
+
+
+Click Add data source and you will come to the settings page of your new data source.
+
+{{< docs-imagebox img="/img/docs/v52/add-datasource.png" max-width="700px" class="docs-image--no-shadow">}}
+
+First, give the data source a Name and then select which Type of data source you'll want to create, see [Supported data sources](/features/datasources/#supported-data-sources/) for more information and how to configure your data source.
+
+
+{{< docs-imagebox img="/img/docs/v52/datasource-settings.png" max-width="700px" class="docs-image--no-shadow">}}
+
+After you have configuered your data source you are ready to save and test.
 
 ## Beginner guides
 

+ 5 - 0
docs/sources/installation/debian.md

@@ -166,3 +166,8 @@ To configure Grafana add a configuration file named `custom.ini` to the
 Start Grafana by executing `./bin/grafana-server web`. The `grafana-server`
 binary needs the working directory to be the root install directory (where the
 binary and the `public` folder is located).
+
+## Logging in for the first time
+
+To run Grafana open your browser and go to http://localhost:3000/. 3000 is the default http port that Grafana listens to if you haven't [configured a different port](/installation/configuration/#http-port).
+Then follow the instructions [here](/guides/getting_started/).

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

@@ -38,6 +38,8 @@ The back-end web server has a number of configuration options. Go to the
 [Configuration]({{< relref "configuration.md" >}}) page for details on all
 those options.
 
+> For any changes to `conf/grafana.ini` (or corresponding environment variables) to take effect you need to restart Grafana by restarting the Docker container.
+
 ## Running a Specific Version of Grafana
 
 ```bash
@@ -49,10 +51,13 @@ $ docker run \
   grafana/grafana:5.1.0
 ```
 
-## Running of the master branch
+## Running the master branch
+
+For every successful build of the master branch we update the `grafana/grafana:master` tag and create a new tag `grafana/grafana-dev:master-<commit hash>` with the hash of the git commit that was built. This means you can always get the latest version of Grafana.
+
+When running Grafana master in production we **strongly** recommend that you use the `grafana/grafana-dev:master-<commit hash>` tag as that will guarantee that you use a specific version of Grafana instead of whatever was the most recent commit at the time.
 
-For every successful commit we publish a Grafana container to [`grafana/grafana`](https://hub.docker.com/r/grafana/grafana/tags/) and [`grafana/grafana-dev`](https://hub.docker.com/r/grafana/grafana-dev/tags/). In `grafana/grafana` container we will always overwrite the `master` tag with the latest version. In `grafana/grafana-dev` we will include
-the git commit in the tag. If you run Grafana master in production we **strongly** recommend that you use the later since different machines might run different version of grafana if they pull the master tag at different times.
+For a list of available tags, check out [grafana/grafana](https://hub.docker.com/r/grafana/grafana/tags/) and [grafana/grafana-dev](https://hub.docker.com/r/grafana/grafana-dev/tags/). 
 
 ## Installing Plugins for Grafana
 
@@ -212,3 +217,8 @@ chown -R root:root /etc/grafana && \
   chown -R grafana:grafana /var/lib/grafana && \
   chown -R grafana:grafana /usr/share/grafana
 ```
+
+## Logging in for the first time
+
+To run Grafana open your browser and go to http://localhost:3000/. 3000 is the default http port that Grafana listens to if you haven't [configured a different port](/installation/configuration/#http-port).
+Then follow the instructions [here](/guides/getting_started/).

+ 4 - 0
docs/sources/installation/mac.md

@@ -92,3 +92,7 @@ Start Grafana by executing `./bin/grafana-server web`. The `grafana-server`
 binary needs the working directory to be the root install directory (where the
 binary and the `public` folder is located).
 
+## Logging in for the first time
+
+To run Grafana open your browser and go to http://localhost:3000/. 3000 is the default http port that Grafana listens to if you haven't [configured a different port](/installation/configuration/#http-port).
+Then follow the instructions [here](/guides/getting_started/).

+ 4 - 0
docs/sources/installation/rpm.md

@@ -193,3 +193,7 @@ Start Grafana by executing `./bin/grafana-server web`. The `grafana-server`
 binary needs the working directory to be the root install directory (where the
 binary and the `public` folder is located).
 
+## Logging in for the first time
+
+To run Grafana open your browser and go to http://localhost:3000/. 3000 is the default http port that Grafana listens to if you haven't [configured a different port](/installation/configuration/#http-port).
+Then follow the instructions [here](/guides/getting_started/).

+ 5 - 0
docs/sources/installation/windows.md

@@ -38,6 +38,11 @@ service using that tool.
 
 Read more about the [configuration options]({{< relref "configuration.md" >}}).
 
+## Logging in for the first time
+
+To run Grafana open your browser and go to the port you configured above, e.g. http://localhost:8080/.
+Then follow the instructions [here](/guides/getting_started/).
+
 ## Building on Windows
 
 The Grafana backend includes Sqlite3 which requires GCC to compile. So

+ 5 - 0
docs/sources/project/building_from_source.md

@@ -141,3 +141,8 @@ Please contribute to the Grafana project and submit a pull request! Build new fe
 **Problem**: On Windows, getting errors about a tool not being installed even though you just installed that tool.
 
 **Solution**: It is usually because it got added to the path and you have to restart your command prompt to use it.
+
+## Logging in for the first time
+
+To run Grafana open your browser and go to the default port http://localhost:3000 or the port you have configured.
+Then follow the instructions [here](/guides/getting_started/).

+ 22 - 8
docs/sources/reference/templating.md

@@ -284,24 +284,38 @@ Currently only supported for Prometheus data sources. This variable represents t
 Template variables can be very useful to dynamically change your queries across a whole dashboard. If you want
 Grafana to dynamically create new panels or rows based on what values you have selected you can use the *Repeat* feature.
 
-If you have a variable with `Multi-value` or `Include all value` options enabled you can choose one panel or one row and have Grafana repeat that row
-for every selected value. You find this option under the General tab in panel edit mode. Select the variable to repeat by, and a `min span`.
-The `min span` controls how small Grafana will make the panels (if you have many values selected). Grafana will automatically adjust the width of
-each repeated panel so that the whole row is filled. Currently, you cannot mix other panels on a row with a repeated panel.
+If you have a variable with `Multi-value` or `Include all value` options enabled you can choose one panel and have Grafana repeat that panel
+for every selected value. You find the *Repeat* feature under the *General tab* in panel edit mode.
+
+The `direction` controls how the panels will be arranged.
+
+By choosing `horizontal` the panels will be arranged side-by-side. Grafana will automatically adjust the width
+of each repeated panel so that the whole row is filled. Currently, you cannot mix other panels on a row with a repeated
+panel. Each panel will never be smaller that the provided `Min width` if you have many selected values.
+
+By choosing `vertical` the panels will be arranged from top to bottom in a column. The `Min width` doesn't have any effect in this case. The width of the repeated panels will be the same as of the first panel (the original template) being repeated.
 
 Only make changes to the first panel (the original template). To have the changes take effect on all panels you need to trigger a dynamic dashboard re-build.
 You can do this by either changing the variable value (that is the basis for the repeat) or reload the dashboard.
 
 ## Repeating Rows
 
-This option requires you to open the row options view. Hover over the row left side to trigger the row menu, in this menu click `Row Options`. This
-opens the row options view. Here you find a *Repeat* dropdown where you can select the variable to repeat by.
+As seen above with the *Panels* you can also repeat *Rows* if you have variables set with  `Multi-value` or
+`Include all value` selection option.
+
+To enable this feature you need to first add a new *Row* using the *Add Panel* menu. Then by hovering the row title and
+clicking on the cog button, you will access the `Row Options` configuration panel. You can then select the variable
+you want to repeat the row for.
+
+It may be a good idea to use a variable in the row title as well.
+
+Example: [Repeated Rows Dashboard](http://play.grafana.org/dashboard/db/repeated-rows)
 
-### URL state
+## URL state
 
 Variable values are always synced to the URL using the syntax `var-<varname>=value`.
 
-### Examples
+## Examples
 
 - [Graphite Templated Dashboard](http://play.grafana.org/dashboard/db/graphite-templated-nested)
 - [Elasticsearch Templated Dashboard](http://play.grafana.org/dashboard/db/elasticsearch-templated)

+ 3 - 3
package.json

@@ -32,7 +32,6 @@
     "es6-shim": "^0.35.3",
     "expect.js": "~0.2.0",
     "expose-loader": "^0.7.3",
-    "extract-text-webpack-plugin": "^4.0.0-beta.0",
     "file-loader": "^1.1.11",
     "fork-ts-checker-webpack-plugin": "^0.4.2",
     "gaze": "^1.1.2",
@@ -63,7 +62,7 @@
     "mobx-react-devtools": "^4.2.15",
     "mocha": "^4.0.1",
     "ng-annotate-loader": "^0.6.1",
-    "ng-annotate-webpack-plugin": "^0.2.1-pre",
+    "ng-annotate-webpack-plugin": "^0.3.0",
     "ngtemplate-loader": "^2.0.1",
     "npm": "^5.4.2",
     "optimize-css-assets-webpack-plugin": "^4.0.2",
@@ -170,7 +169,8 @@
     "slate-react": "^0.12.4",
     "tether": "^1.4.0",
     "tether-drop": "https://github.com/torkelo/drop/tarball/master",
-    "tinycolor2": "^1.4.1"
+    "tinycolor2": "^1.4.1",
+    "tslint-react": "^3.6.0"
   },
   "resolutions": {
     "caniuse-db": "1.0.30000772"

+ 2 - 2
packaging/docker/push_to_docker_hub.sh

@@ -15,10 +15,10 @@ fi
 echo "pushing ${_docker_repo}:${_grafana_version}"
 docker push "${_docker_repo}:${_grafana_version}"
 
-if echo "$_grafana_tag" | grep -q "^v"; then
+if echo "$_grafana_tag" | grep -q "^v" && echo "$_grafana_tag" | grep -vq "beta"; then
 	echo "pushing ${_docker_repo}:latest"
 	docker push "${_docker_repo}:latest"
-else
+elif echo "$_grafana_tag" | grep -q "master"; then
 	echo "pushing grafana/grafana:master"
 	docker push grafana/grafana:master
 fi

+ 7 - 1
pkg/api/login.go

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

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

@@ -320,9 +320,15 @@ func (proxy *DataSourceProxy) applyRoute(req *http.Request) {
 		SecureJsonData: proxy.ds.SecureJsonData.Decrypt(),
 	}
 
-	routeURL, err := url.Parse(proxy.route.Url)
+	interpolatedURL, err := interpolateString(proxy.route.Url, data)
 	if err != nil {
-		logger.Error("Error parsing plugin route url")
+		logger.Error("Error interpolating proxy url", "error", err)
+		return
+	}
+
+	routeURL, err := url.Parse(interpolatedURL)
+	if err != nil {
+		logger.Error("Error parsing plugin route url", "error", err)
 		return
 	}
 

+ 20 - 1
pkg/api/pluginproxy/ds_proxy_test.go

@@ -49,6 +49,13 @@ func TestDSRouteRule(t *testing.T) {
 							{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
 						},
 					},
+					{
+						Path: "api/common",
+						Url:  "{{.JsonData.dynamicUrl}}",
+						Headers: []plugins.AppPluginRouteHeader{
+							{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
+						},
+					},
 				},
 			}
 
@@ -57,7 +64,8 @@ func TestDSRouteRule(t *testing.T) {
 
 			ds := &m.DataSource{
 				JsonData: simplejson.NewFromAny(map[string]interface{}{
-					"clientId": "asd",
+					"clientId":   "asd",
+					"dynamicUrl": "https://dynamic.grafana.com",
 				}),
 				SecureJsonData: map[string][]byte{
 					"key": key,
@@ -83,6 +91,17 @@ func TestDSRouteRule(t *testing.T) {
 				})
 			})
 
+			Convey("When matching route path and has dynamic url", func() {
+				proxy := NewDataSourceProxy(ds, plugin, ctx, "api/common/some/method")
+				proxy.route = plugin.Routes[3]
+				proxy.applyRoute(req)
+
+				Convey("should add headers and interpolate the url", func() {
+					So(req.URL.String(), ShouldEqual, "https://dynamic.grafana.com/some/method")
+					So(req.Header.Get("x-header"), ShouldEqual, "my secret 123")
+				})
+			})
+
 			Convey("Validating request", func() {
 				Convey("plugin route with valid role", func() {
 					proxy := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method")

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -113,9 +113,10 @@ func CreateUser(ctx context.Context, cmd *m.CreateUserCommand) error {
 			LastSeenAt:    time.Now().AddDate(-10, 0, 0),
 		}
 
+		user.Salt = util.GetRandomString(10)
+		user.Rands = util.GetRandomString(10)
+
 		if len(cmd.Password) > 0 {
-			user.Salt = util.GetRandomString(10)
-			user.Rands = util.GetRandomString(10)
 			user.Password = util.EncodePassword(cmd.Password, user.Salt)
 		}
 

+ 22 - 0
pkg/services/sqlstore/user_test.go

@@ -15,6 +15,28 @@ func TestUserDataAccess(t *testing.T) {
 	Convey("Testing DB", t, func() {
 		InitTestDB(t)
 
+		Convey("Creating a user", func() {
+			cmd := &m.CreateUserCommand{
+				Email: "usertest@test.com",
+				Name:  "user name",
+				Login: "user_test_login",
+			}
+
+			err := CreateUser(context.Background(), cmd)
+			So(err, ShouldBeNil)
+
+			Convey("Loading a user", func() {
+				query := m.GetUserByIdQuery{Id: cmd.Result.Id}
+				err := GetUserById(&query)
+				So(err, ShouldBeNil)
+
+				So(query.Result.Email, ShouldEqual, "usertest@test.com")
+				So(query.Result.Password, ShouldEqual, "")
+				So(query.Result.Rands, ShouldHaveLength, 10)
+				So(query.Result.Salt, ShouldHaveLength, 10)
+			})
+		})
+
 		Convey("Given 5 users", func() {
 			var err error
 			var cmd *m.CreateUserCommand

+ 21 - 0
pkg/tsdb/mssql/macros.go

@@ -116,6 +116,27 @@ func (m *msSqlMacroEngine) evaluateMacro(name string, args []string) (string, er
 		return fmt.Sprintf("%d", m.timeRange.GetFromAsSecondsEpoch()), nil
 	case "__unixEpochTo":
 		return fmt.Sprintf("%d", m.timeRange.GetToAsSecondsEpoch()), nil
+	case "__unixEpochGroup":
+		if len(args) < 2 {
+			return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name)
+		}
+		interval, err := time.ParseDuration(strings.Trim(args[1], `'`))
+		if err != nil {
+			return "", fmt.Errorf("error parsing interval %v", args[1])
+		}
+		if len(args) == 3 {
+			err := tsdb.SetupFillmode(m.query, interval, args[2])
+			if err != nil {
+				return "", err
+			}
+		}
+		return fmt.Sprintf("FLOOR(%s/%v)*%v", args[0], interval.Seconds(), interval.Seconds()), nil
+	case "__unixEpochGroupAlias":
+		tg, err := m.evaluateMacro("__unixEpochGroup", args)
+		if err == nil {
+			return tg + " AS [time]", err
+		}
+		return "", err
 	default:
 		return "", fmt.Errorf("Unknown macro %v", name)
 	}

+ 12 - 0
pkg/tsdb/mssql/macros_test.go

@@ -145,6 +145,18 @@ func TestMacroEngine(t *testing.T) {
 
 				So(sql, ShouldEqual, fmt.Sprintf("select %d", to.Unix()))
 			})
+
+			Convey("interpolate __unixEpochGroup function", func() {
+
+				sql, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroup(time_column,'5m')")
+				So(err, ShouldBeNil)
+				sql2, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroupAlias(time_column,'5m')")
+				So(err, ShouldBeNil)
+
+				So(sql, ShouldEqual, "SELECT FLOOR(time_column/300)*300")
+				So(sql2, ShouldEqual, sql+" AS [time]")
+			})
+
 		})
 
 		Convey("Given a time range between 1960-02-01 07:00 and 1965-02-03 08:00", func() {

+ 21 - 0
pkg/tsdb/mysql/macros.go

@@ -112,6 +112,27 @@ func (m *mySqlMacroEngine) evaluateMacro(name string, args []string) (string, er
 		return fmt.Sprintf("%d", m.timeRange.GetFromAsSecondsEpoch()), nil
 	case "__unixEpochTo":
 		return fmt.Sprintf("%d", m.timeRange.GetToAsSecondsEpoch()), nil
+	case "__unixEpochGroup":
+		if len(args) < 2 {
+			return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name)
+		}
+		interval, err := time.ParseDuration(strings.Trim(args[1], `'`))
+		if err != nil {
+			return "", fmt.Errorf("error parsing interval %v", args[1])
+		}
+		if len(args) == 3 {
+			err := tsdb.SetupFillmode(m.query, interval, args[2])
+			if err != nil {
+				return "", err
+			}
+		}
+		return fmt.Sprintf("%s DIV %v * %v", args[0], interval.Seconds(), interval.Seconds()), nil
+	case "__unixEpochGroupAlias":
+		tg, err := m.evaluateMacro("__unixEpochGroup", args)
+		if err == nil {
+			return tg + " AS \"time\"", err
+		}
+		return "", err
 	default:
 		return "", fmt.Errorf("Unknown macro %v", name)
 	}

+ 12 - 0
pkg/tsdb/mysql/macros_test.go

@@ -97,6 +97,18 @@ func TestMacroEngine(t *testing.T) {
 
 				So(sql, ShouldEqual, fmt.Sprintf("select %d", to.Unix()))
 			})
+
+			Convey("interpolate __unixEpochGroup function", func() {
+
+				sql, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroup(time_column,'5m')")
+				So(err, ShouldBeNil)
+				sql2, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroupAlias(time_column,'5m')")
+				So(err, ShouldBeNil)
+
+				So(sql, ShouldEqual, "SELECT time_column DIV 300 * 300")
+				So(sql2, ShouldEqual, sql+" AS \"time\"")
+			})
+
 		})
 
 		Convey("Given a time range between 1960-02-01 07:00 and 1965-02-03 08:00", func() {

+ 21 - 0
pkg/tsdb/postgres/macros.go

@@ -140,6 +140,27 @@ func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string,
 		return fmt.Sprintf("%d", m.timeRange.GetFromAsSecondsEpoch()), nil
 	case "__unixEpochTo":
 		return fmt.Sprintf("%d", m.timeRange.GetToAsSecondsEpoch()), nil
+	case "__unixEpochGroup":
+		if len(args) < 2 {
+			return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name)
+		}
+		interval, err := time.ParseDuration(strings.Trim(args[1], `'`))
+		if err != nil {
+			return "", fmt.Errorf("error parsing interval %v", args[1])
+		}
+		if len(args) == 3 {
+			err := tsdb.SetupFillmode(m.query, interval, args[2])
+			if err != nil {
+				return "", err
+			}
+		}
+		return fmt.Sprintf("floor(%s/%v)*%v", args[0], interval.Seconds(), interval.Seconds()), nil
+	case "__unixEpochGroupAlias":
+		tg, err := m.evaluateMacro("__unixEpochGroup", args)
+		if err == nil {
+			return tg + " AS \"time\"", err
+		}
+		return "", err
 	default:
 		return "", fmt.Errorf("Unknown macro %v", name)
 	}

+ 12 - 0
pkg/tsdb/postgres/macros_test.go

@@ -129,6 +129,18 @@ func TestMacroEngine(t *testing.T) {
 
 				So(sql, ShouldEqual, fmt.Sprintf("select %d", to.Unix()))
 			})
+
+			Convey("interpolate __unixEpochGroup function", func() {
+
+				sql, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroup(time_column,'5m')")
+				So(err, ShouldBeNil)
+				sql2, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroupAlias(time_column,'5m')")
+				So(err, ShouldBeNil)
+
+				So(sql, ShouldEqual, "SELECT floor(time_column/300)*300")
+				So(sql2, ShouldEqual, sql+" AS \"time\"")
+			})
+
 		})
 
 		Convey("Given a time range between 1960-02-01 07:00 and 1965-02-03 08:00", func() {

+ 5 - 5
public/app/app.ts

@@ -53,7 +53,7 @@ export class GrafanaApp {
   }
 
   init() {
-    var app = angular.module('grafana', []);
+    const app = angular.module('grafana', []);
 
     moment.locale(config.bootData.user.locale);
 
@@ -77,7 +77,7 @@ export class GrafanaApp {
         '$delegate',
         '$templateCache',
         function($delegate, $templateCache) {
-          var get = $delegate.get;
+          const get = $delegate.get;
           $delegate.get = function(url, config) {
             if (url.match(/\.html$/)) {
               // some template's already exist in the cache
@@ -105,10 +105,10 @@ export class GrafanaApp {
       'react',
     ];
 
-    var module_types = ['controllers', 'directives', 'factories', 'services', 'filters', 'routes'];
+    const module_types = ['controllers', 'directives', 'factories', 'services', 'filters', 'routes'];
 
     _.each(module_types, type => {
-      var moduleName = 'grafana.' + type;
+      const moduleName = 'grafana.' + type;
       this.useModule(angular.module(moduleName, []));
     });
 
@@ -119,7 +119,7 @@ export class GrafanaApp {
     coreModule.config(setupAngularRoutes);
     registerAngularDirectives();
 
-    var preBootRequires = [System.import('app/features/all')];
+    const preBootRequires = [System.import('app/features/all')];
 
     Promise.all(preBootRequires)
       .then(() => {

+ 1 - 1
public/app/containers/AlertRuleList/AlertRuleList.test.tsx

@@ -46,7 +46,7 @@ describe('AlertRuleList', () => {
 
   it('should render 1 rule', () => {
     page.update();
-    let ruleNode = page.find('.alert-rule-item');
+    const ruleNode = page.find('.alert-rule-item');
     expect(toJson(ruleNode)).toMatchSnapshot();
   });
 

+ 6 - 6
public/app/containers/AlertRuleList/AlertRuleList.tsx

@@ -3,14 +3,14 @@ import { hot } from 'react-hot-loader';
 import classNames from 'classnames';
 import { inject, observer } from 'mobx-react';
 import PageHeader from 'app/core/components/PageHeader/PageHeader';
-import { IAlertRule } from 'app/stores/AlertListStore/AlertListStore';
+import { AlertRule } from 'app/stores/AlertListStore/AlertListStore';
 import appEvents from 'app/core/app_events';
-import IContainerProps from 'app/containers/IContainerProps';
+import ContainerProps from 'app/containers/ContainerProps';
 import Highlighter from 'react-highlight-words';
 
 @inject('view', 'nav', 'alertList')
 @observer
-export class AlertRuleList extends React.Component<IContainerProps, any> {
+export class AlertRuleList extends React.Component<ContainerProps, any> {
   stateFilters = [
     { text: 'All', value: 'all' },
     { text: 'OK', value: 'ok' },
@@ -109,7 +109,7 @@ function AlertStateFilterOption({ text, value }) {
 }
 
 export interface AlertRuleItemProps {
-  rule: IAlertRule;
+  rule: AlertRule;
   search: string;
 }
 
@@ -132,13 +132,13 @@ export class AlertRuleItem extends React.Component<AlertRuleItemProps, any> {
   render() {
     const { rule } = this.props;
 
-    let stateClass = classNames({
+    const stateClass = classNames({
       fa: true,
       'fa-play': rule.isPaused,
       'fa-pause': !rule.isPaused,
     });
 
-    let ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen=true&edit=true&tab=alert`;
+    const ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen=true&edit=true&tab=alert`;
 
     return (
       <li className="alert-rule-item">

+ 2 - 2
public/app/containers/IContainerProps.ts → public/app/containers/ContainerProps.ts

@@ -6,7 +6,7 @@ import { AlertListStore } from './../stores/AlertListStore/AlertListStore';
 import { ViewStore } from './../stores/ViewStore/ViewStore';
 import { FolderStore } from './../stores/FolderStore/FolderStore';
 
-interface IContainerProps {
+interface ContainerProps {
   search: typeof SearchStore.Type;
   serverStats: typeof ServerStatsStore.Type;
   nav: typeof NavStore.Type;
@@ -17,4 +17,4 @@ interface IContainerProps {
   backendSrv: any;
 }
 
-export default IContainerProps;
+export default ContainerProps;

+ 16 - 5
public/app/containers/Explore/Explore.tsx

@@ -63,7 +63,7 @@ function parseUrlState(initial: string | undefined) {
   return { datasource: null, queries: [], range: DEFAULT_RANGE };
 }
 
-interface IExploreState {
+interface ExploreState {
   datasource: any;
   datasourceError: any;
   datasourceLoading: boolean | null;
@@ -88,12 +88,12 @@ interface IExploreState {
   tableResult: any;
 }
 
-export class Explore extends React.Component<any, IExploreState> {
+export class Explore extends React.Component<any, ExploreState> {
   el: any;
 
   constructor(props) {
     super(props);
-    const initialState: IExploreState = props.initialState;
+    const initialState: ExploreState = props.initialState;
     const { datasource, queries, range } = parseUrlState(props.routeParams.state);
     this.state = {
       datasource: null,
@@ -207,6 +207,7 @@ export class Explore extends React.Component<any, IExploreState> {
       datasourceError: null,
       datasourceLoading: true,
       graphResult: null,
+      latency: 0,
       logsResult: null,
       queryErrors: [],
       queryHints: [],
@@ -254,7 +255,10 @@ export class Explore extends React.Component<any, IExploreState> {
     this.setState({
       graphResult: null,
       logsResult: null,
+      latency: 0,
       queries: ensureQueries(),
+      queryErrors: [],
+      queryHints: [],
       tableResult: null,
     });
   };
@@ -276,8 +280,10 @@ export class Explore extends React.Component<any, IExploreState> {
 
   onClickSplit = () => {
     const { onChangeSplit } = this.props;
+    const state = { ...this.state };
+    state.queries = state.queries.map(({ edited, ...rest }) => rest);
     if (onChangeSplit) {
-      onChangeSplit(true, this.state);
+      onChangeSplit(true, state);
     }
   };
 
@@ -340,19 +346,24 @@ export class Explore extends React.Component<any, IExploreState> {
 
   onQuerySuccess(datasourceId: string, queries: any[]): void {
     // save queries to history
-    let { datasource, history } = this.state;
+    let { history } = this.state;
+    const { datasource } = this.state;
+
     if (datasource.meta.id !== datasourceId) {
       // Navigated away, queries did not matter
       return;
     }
+
     const ts = Date.now();
     queries.forEach(q => {
       const { query } = q;
       history = [{ query, ts }, ...history];
     });
+
     if (history.length > MAX_HISTORY_ITEMS) {
       history = history.slice(0, MAX_HISTORY_ITEMS);
     }
+
     // Combine all queries of a datasource type into one history
     const historyKey = `grafana.explore.history.${datasourceId}`;
     store.setObject(historyKey, history);

+ 4 - 4
public/app/containers/Explore/Graph.tsx

@@ -12,10 +12,10 @@ import Legend from './Legend';
 // Copied from graph.ts
 function time_format(ticks, min, max) {
   if (min && max && ticks) {
-    var range = max - min;
-    var secPerTick = range / ticks / 1000;
-    var oneDay = 86400000;
-    var oneYear = 31536000000;
+    const range = max - min;
+    const secPerTick = range / ticks / 1000;
+    const oneDay = 86400000;
+    const oneYear = 31536000000;
 
     if (secPerTick <= 45) {
       return '%H:%M:%S';

+ 19 - 0
public/app/containers/Explore/PromQueryField.test.tsx

@@ -94,6 +94,25 @@ describe('PromQueryField typeahead handling', () => {
       expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
     });
 
+    it('returns label suggestions on label context but leaves out labels that already exist', () => {
+      const instance = shallow(
+        <PromQueryField {...defaultProps} labelKeys={{ '{job="foo"}': ['bar', 'job'] }} />
+      ).instance() as PromQueryField;
+      const value = Plain.deserialize('{job="foo",}');
+      const range = value.selection.merge({
+        anchorOffset: 11,
+      });
+      const valueWithSelection = value.change().select(range).value;
+      const result = instance.getTypeahead({
+        text: '',
+        prefix: '',
+        wrapperClasses: ['context-labels'],
+        value: valueWithSelection,
+      });
+      expect(result.context).toBe('context-labels');
+      expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
+    });
+
     it('returns a refresher on label context and unavailable metric', () => {
       const instance = shallow(
         <PromQueryField {...defaultProps} labelKeys={{ '{__name__="foo"}': ['bar'] }} />

+ 11 - 5
public/app/containers/Explore/PromQueryField.tsx

@@ -10,7 +10,7 @@ import PluginPrism, { setPrismTokens } from './slate-plugins/prism/index';
 import PrismPromql, { FUNCTIONS } from './slate-plugins/prism/promql';
 import BracesPlugin from './slate-plugins/braces';
 import RunnerPlugin from './slate-plugins/runner';
-import { processLabels, RATE_RANGES, cleanText, getCleanSelector } from './utils/prometheus';
+import { processLabels, RATE_RANGES, cleanText, parseSelector } from './utils/prometheus';
 
 import TypeaheadField, {
   Suggestion,
@@ -328,7 +328,7 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
     const closeParensSelectorIndex = leftSide.slice(openParensSelectorIndex).indexOf(')') + openParensSelectorIndex;
     // foo{bar="1"}
     const selectorString = leftSide.slice(openParensSelectorIndex + 1, closeParensSelectorIndex);
-    const selector = getCleanSelector(selectorString, selectorString.length - 2);
+    const selector = parseSelector(selectorString, selectorString.length - 2).selector;
 
     const labelKeys = this.state.labelKeys[selector];
     if (labelKeys) {
@@ -353,12 +353,15 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
 
     // Get normalized selector
     let selector;
+    let parsedSelector;
     try {
-      selector = getCleanSelector(line, cursorOffset);
+      parsedSelector = parseSelector(line, cursorOffset);
+      selector = parsedSelector.selector;
     } catch {
       selector = EMPTY_SELECTOR;
     }
     const containsMetric = selector.indexOf('__name__=') > -1;
+    const existingKeys = parsedSelector ? parsedSelector.labelKeys : [];
 
     if ((text && text.startsWith('=')) || _.includes(wrapperClasses, 'attr-value')) {
       // Label values
@@ -374,8 +377,11 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
       // Label keys
       const labelKeys = this.state.labelKeys[selector] || (containsMetric ? null : DEFAULT_KEYS);
       if (labelKeys) {
-        context = 'context-labels';
-        suggestions.push({ label: `Labels`, items: labelKeys.map(wrapLabel) });
+        const possibleKeys = _.difference(labelKeys, existingKeys);
+        if (possibleKeys.length > 0) {
+          context = 'context-labels';
+          suggestions.push({ label: `Labels`, items: possibleKeys.map(wrapLabel) });
+        }
       }
     }
 

+ 1 - 1
public/app/containers/Explore/QueryField.tsx

@@ -331,7 +331,7 @@ class QueryField extends React.Component<TypeaheadFieldProps, TypeaheadFieldStat
         }
         break;
       }
-
+      case 'Enter':
       case 'Tab': {
         if (this.menuEl) {
           // Dont blur input

+ 1 - 1
public/app/containers/Explore/Table.tsx

@@ -40,7 +40,7 @@ function Cell(props: SFCCellProps) {
 export default class Table extends PureComponent<TableProps, {}> {
   render() {
     const { className = '', data, loading, onClickCell } = this.props;
-    let tableModel = data || EMPTY_TABLE;
+    const tableModel = data || EMPTY_TABLE;
     if (!loading && data && data.rows.length === 0) {
       return (
         <table className={`${className} filter-table`}>

+ 45 - 17
public/app/containers/Explore/utils/prometheus.test.ts

@@ -1,33 +1,61 @@
-import { getCleanSelector } from './prometheus';
+import { parseSelector } from './prometheus';
+
+describe('parseSelector()', () => {
+  let parsed;
 
-describe('getCleanSelector()', () => {
   it('returns a clean selector from an empty selector', () => {
-    expect(getCleanSelector('{}', 1)).toBe('{}');
+    parsed = parseSelector('{}', 1);
+    expect(parsed.selector).toBe('{}');
+    expect(parsed.labelKeys).toEqual([]);
   });
+
   it('throws if selector is broken', () => {
-    expect(() => getCleanSelector('{foo')).toThrow();
+    expect(() => parseSelector('{foo')).toThrow();
   });
+
   it('returns the selector sorted by label key', () => {
-    expect(getCleanSelector('{foo="bar"}')).toBe('{foo="bar"}');
-    expect(getCleanSelector('{foo="bar",baz="xx"}')).toBe('{baz="xx",foo="bar"}');
+    parsed = parseSelector('{foo="bar"}');
+    expect(parsed.selector).toBe('{foo="bar"}');
+    expect(parsed.labelKeys).toEqual(['foo']);
+
+    parsed = parseSelector('{foo="bar",baz="xx"}');
+    expect(parsed.selector).toBe('{baz="xx",foo="bar"}');
   });
+
   it('returns a clean selector from an incomplete one', () => {
-    expect(getCleanSelector('{foo}')).toBe('{}');
-    expect(getCleanSelector('{foo="bar",baz}')).toBe('{foo="bar"}');
-    expect(getCleanSelector('{foo="bar",baz="}')).toBe('{foo="bar"}');
+    parsed = parseSelector('{foo}');
+    expect(parsed.selector).toBe('{}');
+
+    parsed = parseSelector('{foo="bar",baz}');
+    expect(parsed.selector).toBe('{foo="bar"}');
+
+    parsed = parseSelector('{foo="bar",baz="}');
+    expect(parsed.selector).toBe('{foo="bar"}');
   });
+
   it('throws if not inside a selector', () => {
-    expect(() => getCleanSelector('foo{}', 0)).toThrow();
-    expect(() => getCleanSelector('foo{} + bar{}', 5)).toThrow();
+    expect(() => parseSelector('foo{}', 0)).toThrow();
+    expect(() => parseSelector('foo{} + bar{}', 5)).toThrow();
   });
+
   it('returns the selector nearest to the cursor offset', () => {
-    expect(() => getCleanSelector('{foo="bar"} + {foo="bar"}', 0)).toThrow();
-    expect(getCleanSelector('{foo="bar"} + {foo="bar"}', 1)).toBe('{foo="bar"}');
-    expect(getCleanSelector('{foo="bar"} + {baz="xx"}', 1)).toBe('{foo="bar"}');
-    expect(getCleanSelector('{baz="xx"} + {foo="bar"}', 16)).toBe('{foo="bar"}');
+    expect(() => parseSelector('{foo="bar"} + {foo="bar"}', 0)).toThrow();
+
+    parsed = parseSelector('{foo="bar"} + {foo="bar"}', 1);
+    expect(parsed.selector).toBe('{foo="bar"}');
+
+    parsed = parseSelector('{foo="bar"} + {baz="xx"}', 1);
+    expect(parsed.selector).toBe('{foo="bar"}');
+
+    parsed = parseSelector('{baz="xx"} + {foo="bar"}', 16);
+    expect(parsed.selector).toBe('{foo="bar"}');
   });
+
   it('returns a selector with metric if metric is given', () => {
-    expect(getCleanSelector('bar{foo}', 4)).toBe('{__name__="bar"}');
-    expect(getCleanSelector('baz{foo="bar"}', 12)).toBe('{__name__="baz",foo="bar"}');
+    parsed = parseSelector('bar{foo}', 4);
+    expect(parsed.selector).toBe('{__name__="bar"}');
+
+    parsed = parseSelector('baz{foo="bar"}', 12);
+    expect(parsed.selector).toBe('{__name__="baz",foo="bar"}');
   });
 });

+ 11 - 8
public/app/containers/Explore/utils/prometheus.ts

@@ -29,11 +29,14 @@ export const cleanText = s => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim();
 // const cleanSelectorRegexp = /\{(\w+="[^"\n]*?")(,\w+="[^"\n]*?")*\}/;
 const selectorRegexp = /\{[^}]*?\}/;
 const labelRegexp = /\b\w+="[^"\n]*?"/g;
-export function getCleanSelector(query: string, cursorOffset = 1): string {
+export function parseSelector(query: string, cursorOffset = 1): { labelKeys: any[]; selector: string } {
   if (!query.match(selectorRegexp)) {
     // Special matcher for metrics
     if (query.match(/^\w+$/)) {
-      return `{__name__="${query}"}`;
+      return {
+        selector: `{__name__="${query}"}`,
+        labelKeys: ['__name__'],
+      };
     }
     throw new Error('Query must contain a selector: ' + query);
   }
@@ -62,7 +65,7 @@ export function getCleanSelector(query: string, cursorOffset = 1): string {
 
   // Extract clean labels to form clean selector, incomplete labels are dropped
   const selector = query.slice(prefixOpen, suffixClose);
-  let labels = {};
+  const labels = {};
   selector.replace(labelRegexp, match => {
     const delimiterIndex = match.indexOf('=');
     const key = match.slice(0, delimiterIndex);
@@ -79,10 +82,10 @@ export function getCleanSelector(query: string, cursorOffset = 1): string {
   }
 
   // Build sorted selector
-  const cleanSelector = Object.keys(labels)
-    .sort()
-    .map(key => `${key}=${labels[key]}`)
-    .join(',');
+  const labelKeys = Object.keys(labels).sort();
+  const cleanSelector = labelKeys.map(key => `${key}=${labels[key]}`).join(',');
 
-  return ['{', cleanSelector, '}'].join('');
+  const selectorString = ['{', cleanSelector, '}'].join('');
+
+  return { labelKeys, selector: selectorString };
 }

+ 2 - 2
public/app/containers/ManageDashboards/FolderPermissions.tsx

@@ -2,7 +2,7 @@ import React, { Component } from 'react';
 import { hot } from 'react-hot-loader';
 import { inject, observer } from 'mobx-react';
 import { toJS } from 'mobx';
-import IContainerProps from 'app/containers/IContainerProps';
+import ContainerProps from 'app/containers/ContainerProps';
 import PageHeader from 'app/core/components/PageHeader/PageHeader';
 import Permissions from 'app/core/components/Permissions/Permissions';
 import Tooltip from 'app/core/components/Tooltip/Tooltip';
@@ -12,7 +12,7 @@ import SlideDown from 'app/core/components/Animations/SlideDown';
 
 @inject('nav', 'folder', 'view', 'permissions')
 @observer
-export class FolderPermissions extends Component<IContainerProps, any> {
+export class FolderPermissions extends Component<ContainerProps, any> {
   constructor(props) {
     super(props);
     this.handleAddPermission = this.handleAddPermission.bind(this);

+ 2 - 2
public/app/containers/ManageDashboards/FolderSettings.tsx

@@ -3,13 +3,13 @@ import { hot } from 'react-hot-loader';
 import { inject, observer } from 'mobx-react';
 import { toJS } from 'mobx';
 import PageHeader from 'app/core/components/PageHeader/PageHeader';
-import IContainerProps from 'app/containers/IContainerProps';
+import ContainerProps from 'app/containers/ContainerProps';
 import { getSnapshot } from 'mobx-state-tree';
 import appEvents from 'app/core/app_events';
 
 @inject('nav', 'folder', 'view')
 @observer
-export class FolderSettings extends React.Component<IContainerProps, any> {
+export class FolderSettings extends React.Component<ContainerProps, any> {
   formSnapshot: any;
 
   componentDidMount() {

+ 2 - 2
public/app/containers/ServerStats/ServerStats.tsx

@@ -2,11 +2,11 @@ import React from 'react';
 import { hot } from 'react-hot-loader';
 import { inject, observer } from 'mobx-react';
 import PageHeader from 'app/core/components/PageHeader/PageHeader';
-import IContainerProps from 'app/containers/IContainerProps';
+import ContainerProps from 'app/containers/ContainerProps';
 
 @inject('nav', 'serverStats')
 @observer
-export class ServerStats extends React.Component<IContainerProps, any> {
+export class ServerStats extends React.Component<ContainerProps, any> {
   constructor(props) {
     super(props);
     const { nav, serverStats } = this.props;

+ 4 - 4
public/app/containers/Teams/TeamGroupSync.tsx

@@ -1,12 +1,12 @@
 import React from 'react';
 import { hot } from 'react-hot-loader';
 import { observer } from 'mobx-react';
-import { ITeam, ITeamGroup } from 'app/stores/TeamsStore/TeamsStore';
+import { Team, TeamGroup } from 'app/stores/TeamsStore/TeamsStore';
 import SlideDown from 'app/core/components/Animations/SlideDown';
 import Tooltip from 'app/core/components/Tooltip/Tooltip';
 
 interface Props {
-  team: ITeam;
+  team: Team;
 }
 
 interface State {
@@ -27,7 +27,7 @@ export class TeamGroupSync extends React.Component<Props, State> {
     this.props.team.loadGroups();
   }
 
-  renderGroup(group: ITeamGroup) {
+  renderGroup(group: TeamGroup) {
     return (
       <tr key={group.groupId}>
         <td>{group.groupId}</td>
@@ -53,7 +53,7 @@ export class TeamGroupSync extends React.Component<Props, State> {
     this.setState({ isAdding: false, newGroupId: '' });
   };
 
-  onRemoveGroup = (group: ITeamGroup) => {
+  onRemoveGroup = (group: TeamGroup) => {
     this.props.team.removeGroup(group.groupId);
   };
 

+ 4 - 4
public/app/containers/Teams/TeamList.tsx

@@ -3,7 +3,7 @@ import { hot } from 'react-hot-loader';
 import { inject, observer } from 'mobx-react';
 import PageHeader from 'app/core/components/PageHeader/PageHeader';
 import { NavStore } from 'app/stores/NavStore/NavStore';
-import { TeamsStore, ITeam } from 'app/stores/TeamsStore/TeamsStore';
+import { TeamsStore, Team } from 'app/stores/TeamsStore/TeamsStore';
 import { BackendSrv } from 'app/core/services/backend_srv';
 import DeleteButton from 'app/core/components/DeleteButton/DeleteButton';
 
@@ -27,7 +27,7 @@ export class TeamList extends React.Component<Props, any> {
     this.props.teams.loadTeams();
   }
 
-  deleteTeam(team: ITeam) {
+  deleteTeam(team: Team) {
     this.props.backendSrv.delete('/api/teams/' + team.id).then(this.fetchTeams.bind(this));
   }
 
@@ -35,8 +35,8 @@ export class TeamList extends React.Component<Props, any> {
     this.props.teams.setSearchQuery(evt.target.value);
   };
 
-  renderTeamMember(team: ITeam): JSX.Element {
-    let teamUrl = `org/teams/edit/${team.id}`;
+  renderTeamMember(team: Team): JSX.Element {
+    const teamUrl = `org/teams/edit/${team.id}`;
 
     return (
       <tr key={team.id}>

+ 5 - 5
public/app/containers/Teams/TeamMembers.tsx

@@ -1,13 +1,13 @@
 import React from 'react';
 import { hot } from 'react-hot-loader';
 import { observer } from 'mobx-react';
-import { ITeam, ITeamMember } from 'app/stores/TeamsStore/TeamsStore';
+import { Team, TeamMember } from 'app/stores/TeamsStore/TeamsStore';
 import SlideDown from 'app/core/components/Animations/SlideDown';
 import { UserPicker, User } from 'app/core/components/Picker/UserPicker';
 import DeleteButton from 'app/core/components/DeleteButton/DeleteButton';
 
 interface Props {
-  team: ITeam;
+  team: Team;
 }
 
 interface State {
@@ -30,15 +30,15 @@ export class TeamMembers extends React.Component<Props, State> {
     this.props.team.setSearchQuery(evt.target.value);
   };
 
-  removeMember(member: ITeamMember) {
+  removeMember(member: TeamMember) {
     this.props.team.removeMember(member);
   }
 
-  removeMemberConfirmed(member: ITeamMember) {
+  removeMemberConfirmed(member: TeamMember) {
     this.props.team.removeMember(member);
   }
 
-  renderMember(member: ITeamMember) {
+  renderMember(member: TeamMember) {
     return (
       <tr key={member.userId}>
         <td className="width-4 text-center">

+ 2 - 2
public/app/containers/Teams/TeamPages.tsx

@@ -5,7 +5,7 @@ import { inject, observer } from 'mobx-react';
 import config from 'app/core/config';
 import PageHeader from 'app/core/components/PageHeader/PageHeader';
 import { NavStore } from 'app/stores/NavStore/NavStore';
-import { TeamsStore, ITeam } from 'app/stores/TeamsStore/TeamsStore';
+import { TeamsStore, Team } from 'app/stores/TeamsStore/TeamsStore';
 import { ViewStore } from 'app/stores/ViewStore/ViewStore';
 import TeamMembers from './TeamMembers';
 import TeamSettings from './TeamSettings';
@@ -40,7 +40,7 @@ export class TeamPages extends React.Component<Props, any> {
     nav.initTeamPage(this.getCurrentTeam(), this.currentPage, this.isSyncEnabled);
   }
 
-  getCurrentTeam(): ITeam {
+  getCurrentTeam(): Team {
     const { teams, view } = this.props;
     return teams.map.get(view.routeParams.get('id'));
   }

+ 2 - 2
public/app/containers/Teams/TeamSettings.tsx

@@ -1,11 +1,11 @@
 import React from 'react';
 import { hot } from 'react-hot-loader';
 import { observer } from 'mobx-react';
-import { ITeam } from 'app/stores/TeamsStore/TeamsStore';
+import { Team } from 'app/stores/TeamsStore/TeamsStore';
 import { Label } from 'app/core/components/Forms/Forms';
 
 interface Props {
-  team: ITeam;
+  team: Team;
 }
 
 @observer

+ 30 - 27
public/app/core/components/EmptyListCTA/EmptyListCTA.tsx

@@ -1,34 +1,37 @@
 import React, { Component } from 'react';
 
-export interface IProps {
-    model: any;
+export interface Props {
+  model: any;
 }
 
-class EmptyListCTA extends Component<IProps, any> {
-    render() {
-        const {
-            title,
-            buttonIcon,
-            buttonLink,
-            buttonTitle,
-            proTip,
-            proTipLink,
-            proTipLinkTitle,
-            proTipTarget
-        } = this.props.model;
-        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"><i className={buttonIcon} />{buttonTitle}</a>
-                <div className="empty-list-cta__pro-tip">
-                    <i className="fa fa-rocket" /> ProTip: {proTip}
-                    <a className="text-link empty-list-cta__pro-tip-link"
-                        href={proTipLink}
-                        target={proTipTarget}>{proTipLinkTitle}</a>
-                </div>
-            </div>
-        );
-    }
+class EmptyListCTA extends Component<Props, any> {
+  render() {
+    const {
+      title,
+      buttonIcon,
+      buttonLink,
+      buttonTitle,
+      proTip,
+      proTipLink,
+      proTipLinkTitle,
+      proTipTarget,
+    } = this.props.model;
+    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">
+          <i className={buttonIcon} />
+          {buttonTitle}
+        </a>
+        <div className="empty-list-cta__pro-tip">
+          <i className="fa fa-rocket" /> ProTip: {proTip}
+          <a className="text-link empty-list-cta__pro-tip-link" href={proTipLink} target={proTipTarget}>
+            {proTipLinkTitle}
+          </a>
+        </div>
+      </div>
+    );
+  }
 }
 
 export default EmptyListCTA;

+ 4 - 4
public/app/core/components/PageHeader/PageHeader.tsx

@@ -5,7 +5,7 @@ import classNames from 'classnames';
 import appEvents from 'app/core/app_events';
 import { toJS } from 'mobx';
 
-export interface IProps {
+export interface Props {
   model: NavModel;
 }
 
@@ -15,8 +15,8 @@ const SelectNav = ({ main, customCss }: { main: NavModelItem; customCss: string
   });
 
   const gotoUrl = evt => {
-    var element = evt.target;
-    var url = element.options[element.selectedIndex].value;
+    const element = evt.target;
+    const url = element.options[element.selectedIndex].value;
     appEvents.emit('location-change', { href: url });
   };
 
@@ -82,7 +82,7 @@ const Navigation = ({ main }: { main: NavModelItem }) => {
 };
 
 @observer
-export default class PageHeader extends React.Component<IProps, any> {
+export default class PageHeader extends React.Component<Props, any> {
   constructor(props) {
     super(props);
   }

+ 8 - 11
public/app/core/components/PasswordStrength.tsx

@@ -1,32 +1,31 @@
 import React from 'react';
 
-export interface IProps {
+export interface Props {
   password: string;
 }
 
-export class PasswordStrength extends React.Component<IProps, any> {
-
+export class PasswordStrength extends React.Component<Props, any> {
   constructor(props) {
     super(props);
   }
 
   render() {
     const { password } = this.props;
-    let strengthText = "strength: strong like a bull.";
-    let strengthClass = "password-strength-good";
+    let strengthText = 'strength: strong like a bull.';
+    let strengthClass = 'password-strength-good';
 
     if (!password) {
       return null;
     }
 
     if (password.length <= 8) {
-      strengthText = "strength: you can do better.";
-      strengthClass = "password-strength-ok";
+      strengthText = 'strength: you can do better.';
+      strengthClass = 'password-strength-ok';
     }
 
     if (password.length < 4) {
-      strengthText = "strength: weak sauce.";
-      strengthClass = "password-strength-bad";
+      strengthText = 'strength: weak sauce.';
+      strengthClass = 'password-strength-bad';
     }
 
     return (
@@ -36,5 +35,3 @@ export class PasswordStrength extends React.Component<IProps, any> {
     );
   }
 }
-
-

+ 1 - 1
public/app/core/components/Permissions/AddPermissions.test.tsx

@@ -22,7 +22,7 @@ describe('AddPermissions', () => {
   let wrapper;
   let store;
   let instance;
-  let backendSrv: any = getBackendSrv();
+  const backendSrv: any = getBackendSrv();
 
   beforeAll(() => {
     store = RootStore.create({}, { backendSrv: backendSrv });

+ 2 - 2
public/app/core/components/Permissions/DisabledPermissionsListItem.tsx

@@ -2,11 +2,11 @@ import React, { Component } from 'react';
 import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker';
 import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore';
 
-export interface IProps {
+export interface Props {
   item: any;
 }
 
-export default class DisabledPermissionListItem extends Component<IProps, any> {
+export default class DisabledPermissionListItem extends Component<Props, any> {
   render() {
     const { item } = this.props;
 

+ 2 - 2
public/app/core/components/Permissions/Permissions.tsx

@@ -20,7 +20,7 @@ export interface DashboardAcl {
   sortRank?: number;
 }
 
-export interface IProps {
+export interface Props {
   dashboardId: number;
   folderInfo?: FolderInfo;
   permissions?: any;
@@ -29,7 +29,7 @@ export interface IProps {
 }
 
 @observer
-class Permissions extends Component<IProps, any> {
+class Permissions extends Component<Props, any> {
   constructor(props) {
     super(props);
     const { dashboardId, isFolder, folderInfo } = this.props;

+ 2 - 2
public/app/core/components/Permissions/PermissionsList.tsx

@@ -4,7 +4,7 @@ import DisabledPermissionsListItem from './DisabledPermissionsListItem';
 import { observer } from 'mobx-react';
 import { FolderInfo } from './FolderInfo';
 
-export interface IProps {
+export interface Props {
   permissions: any[];
   removeItem: any;
   permissionChanged: any;
@@ -13,7 +13,7 @@ export interface IProps {
 }
 
 @observer
-class PermissionsList extends Component<IProps, any> {
+class PermissionsList extends Component<Props, any> {
   render() {
     const { permissions, removeItem, permissionChanged, fetching, folderInfo } = this.props;
 

+ 2 - 2
public/app/core/components/Picker/DescriptionOption.tsx

@@ -1,6 +1,6 @@
 import React, { Component } from 'react';
 
-export interface IProps {
+export interface Props {
   onSelect: any;
   onFocus: any;
   option: any;
@@ -8,7 +8,7 @@ export interface IProps {
   className: any;
 }
 
-class DescriptionOption extends Component<IProps, any> {
+class DescriptionOption extends Component<Props, any> {
   constructor(props) {
     super(props);
     this.handleMouseDown = this.handleMouseDown.bind(this);

+ 2 - 2
public/app/core/components/Picker/PickerOption.tsx

@@ -1,6 +1,6 @@
 import React, { Component } from 'react';
 
-export interface IProps {
+export interface Props {
   onSelect: any;
   onFocus: any;
   option: any;
@@ -8,7 +8,7 @@ export interface IProps {
   className: any;
 }
 
-class UserPickerOption extends Component<IProps, any> {
+class UserPickerOption extends Component<Props, any> {
   constructor(props) {
     super(props);
     this.handleMouseDown = this.handleMouseDown.bind(this);

+ 2 - 2
public/app/core/components/TagFilter/TagBadge.tsx

@@ -1,14 +1,14 @@
 import React from 'react';
 import tags from 'app/core/utils/tags';
 
-export interface IProps {
+export interface Props {
   label: string;
   removeIcon: boolean;
   count: number;
   onClick: any;
 }
 
-export class TagBadge extends React.Component<IProps, any> {
+export class TagBadge extends React.Component<Props, any> {
   constructor(props) {
     super(props);
     this.onClick = this.onClick.bind(this);

+ 3 - 3
public/app/core/components/TagFilter/TagFilter.tsx

@@ -4,13 +4,13 @@ import { Async } from 'react-select';
 import { TagValue } from './TagValue';
 import { TagOption } from './TagOption';
 
-export interface IProps {
+export interface Props {
   tags: string[];
   tagOptions: () => any;
   onSelect: (tag: string) => void;
 }
 
-export class TagFilter extends React.Component<IProps, any> {
+export class TagFilter extends React.Component<Props, any> {
   inlineTags: boolean;
 
   constructor(props) {
@@ -43,7 +43,7 @@ export class TagFilter extends React.Component<IProps, any> {
   }
 
   render() {
-    let selectOptions = {
+    const selectOptions = {
       loadOptions: this.searchTags,
       onChange: this.onChange,
       value: this.props.tags,

+ 2 - 2
public/app/core/components/TagFilter/TagOption.tsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import { TagBadge } from './TagBadge';
 
-export interface IProps {
+export interface Props {
   onSelect: any;
   onFocus: any;
   option: any;
@@ -9,7 +9,7 @@ export interface IProps {
   className: any;
 }
 
-export class TagOption extends React.Component<IProps, any> {
+export class TagOption extends React.Component<Props, any> {
   constructor(props) {
     super(props);
     this.handleMouseDown = this.handleMouseDown.bind(this);

+ 2 - 2
public/app/core/components/TagFilter/TagValue.tsx

@@ -1,14 +1,14 @@
 import React from 'react';
 import { TagBadge } from './TagBadge';
 
-export interface IProps {
+export interface Props {
   value: any;
   className: any;
   onClick: any;
   onRemove: any;
 }
 
-export class TagValue extends React.Component<IProps, any> {
+export class TagValue extends React.Component<Props, any> {
   constructor(props) {
     super(props);
     this.onClick = this.onClick.bind(this);

+ 2 - 2
public/app/core/components/Tooltip/Popover.tsx

@@ -2,11 +2,11 @@
 import withTooltip from './withTooltip';
 import { Target } from 'react-popper';
 
-interface IPopoverProps {
+interface PopoverProps {
   tooltipSetState: (prevState: object) => void;
 }
 
-class Popover extends React.Component<IPopoverProps, any> {
+class Popover extends React.Component<PopoverProps, any> {
   constructor(props) {
     super(props);
     this.toggleTooltip = this.toggleTooltip.bind(this);

+ 2 - 2
public/app/core/components/Tooltip/Tooltip.tsx

@@ -2,11 +2,11 @@
 import withTooltip from './withTooltip';
 import { Target } from 'react-popper';
 
-interface ITooltipProps {
+interface TooltipProps {
   tooltipSetState: (prevState: object) => void;
 }
 
-class Tooltip extends React.Component<ITooltipProps, any> {
+class Tooltip extends React.Component<TooltipProps, any> {
   constructor(props) {
     super(props);
     this.showTooltip = this.showTooltip.bind(this);

+ 15 - 15
public/app/core/components/code_editor/code_editor.ts

@@ -53,23 +53,23 @@ const DEFAULT_TAB_SIZE = 2;
 const DEFAULT_BEHAVIOURS = true;
 const DEFAULT_SNIPPETS = true;
 
-let editorTemplate = `<div></div>`;
+const editorTemplate = `<div></div>`;
 
 function link(scope, elem, attrs) {
   // Options
-  let langMode = attrs.mode || DEFAULT_MODE;
-  let maxLines = attrs.maxLines || DEFAULT_MAX_LINES;
-  let showGutter = attrs.showGutter !== undefined;
-  let tabSize = attrs.tabSize || DEFAULT_TAB_SIZE;
-  let behavioursEnabled = attrs.behavioursEnabled ? attrs.behavioursEnabled === 'true' : DEFAULT_BEHAVIOURS;
-  let snippetsEnabled = attrs.snippetsEnabled ? attrs.snippetsEnabled === 'true' : DEFAULT_SNIPPETS;
+  const langMode = attrs.mode || DEFAULT_MODE;
+  const maxLines = attrs.maxLines || DEFAULT_MAX_LINES;
+  const showGutter = attrs.showGutter !== undefined;
+  const tabSize = attrs.tabSize || DEFAULT_TAB_SIZE;
+  const behavioursEnabled = attrs.behavioursEnabled ? attrs.behavioursEnabled === 'true' : DEFAULT_BEHAVIOURS;
+  const snippetsEnabled = attrs.snippetsEnabled ? attrs.snippetsEnabled === 'true' : DEFAULT_SNIPPETS;
 
   // Initialize editor
-  let aceElem = elem.get(0);
-  let codeEditor = ace.edit(aceElem);
-  let editorSession = codeEditor.getSession();
+  const aceElem = elem.get(0);
+  const codeEditor = ace.edit(aceElem);
+  const editorSession = codeEditor.getSession();
 
-  let editorOptions = {
+  const editorOptions = {
     maxLines: maxLines,
     showGutter: showGutter,
     tabSize: tabSize,
@@ -93,7 +93,7 @@ function link(scope, elem, attrs) {
 
   // Add classes
   elem.addClass('gf-code-editor');
-  let textarea = elem.find('textarea');
+  const textarea = elem.find('textarea');
   textarea.addClass('gf-form-input');
 
   if (scope.codeEditorFocus) {
@@ -110,14 +110,14 @@ function link(scope, elem, attrs) {
   // Event handlers
   editorSession.on('change', e => {
     scope.$apply(() => {
-      let newValue = codeEditor.getValue();
+      const newValue = codeEditor.getValue();
       scope.content = newValue;
     });
   });
 
   // Sync with outer scope - update editor content if model has been changed from outside of directive.
   scope.$watch('content', (newValue, oldValue) => {
-    let editorValue = codeEditor.getValue();
+    const editorValue = codeEditor.getValue();
     if (newValue !== editorValue && newValue !== oldValue) {
       scope.$$postDigest(function() {
         setEditorContent(newValue);
@@ -157,7 +157,7 @@ function link(scope, elem, attrs) {
       anyEditor.completers.push(scope.getCompleter());
     }
 
-    let aceModeName = `ace/mode/${lang}`;
+    const aceModeName = `ace/mode/${lang}`;
     editorSession.setMode(aceModeName);
   }
 

+ 4 - 4
public/app/core/components/colorpicker/ColorPalette.tsx

@@ -1,12 +1,12 @@
 import React from 'react';
 import { sortedColors } from 'app/core/utils/colors';
 
-export interface IProps {
+export interface Props {
   color: string;
   onColorSelect: (c: string) => void;
 }
 
-export class ColorPalette extends React.Component<IProps, any> {
+export class ColorPalette extends React.Component<Props, any> {
   paletteColors: string[];
 
   constructor(props) {
@@ -29,7 +29,8 @@ export class ColorPalette extends React.Component<IProps, any> {
           key={paletteColor}
           className={'pointer fa ' + cssClass}
           style={{ color: paletteColor }}
-          onClick={this.onColorSelect(paletteColor)}>
+          onClick={this.onColorSelect(paletteColor)}
+        >
           &nbsp;
         </i>
       );
@@ -41,4 +42,3 @@ export class ColorPalette extends React.Component<IProps, any> {
     );
   }
 }
-

+ 4 - 4
public/app/core/components/colorpicker/ColorPicker.tsx

@@ -5,12 +5,12 @@ import Drop from 'tether-drop';
 import { ColorPickerPopover } from './ColorPickerPopover';
 import { react2AngularDirective } from 'app/core/utils/react2angular';
 
-export interface IProps {
+export interface Props {
   color: string;
   onChange: (c: string) => void;
 }
 
-export class ColorPicker extends React.Component<IProps, any> {
+export class ColorPicker extends React.Component<Props, any> {
   pickerElem: any;
   colorPickerDrop: any;
 
@@ -29,10 +29,10 @@ export class ColorPicker extends React.Component<IProps, any> {
   openColorPicker() {
     const dropContent = <ColorPickerPopover color={this.props.color} onColorSelect={this.onColorSelect} />;
 
-    let dropContentElem = document.createElement('div');
+    const dropContentElem = document.createElement('div');
     ReactDOM.render(dropContent, dropContentElem);
 
-    let drop = new Drop({
+    const drop = new Drop({
       target: this.pickerElem[0],
       content: dropContentElem,
       position: 'top center',

+ 27 - 22
public/app/core/components/colorpicker/ColorPickerPopover.tsx

@@ -6,12 +6,12 @@ import { SpectrumPicker } from './SpectrumPicker';
 
 const DEFAULT_COLOR = '#000000';
 
-export interface IProps {
+export interface Props {
   color: string;
   onColorSelect: (c: string) => void;
 }
 
-export class ColorPickerPopover extends React.Component<IProps, any> {
+export class ColorPickerPopover extends React.Component<Props, any> {
   pickerNavElem: any;
 
   constructor(props) {
@@ -19,7 +19,7 @@ export class ColorPickerPopover extends React.Component<IProps, any> {
     this.state = {
       tab: 'palette',
       color: this.props.color || DEFAULT_COLOR,
-      colorString: this.props.color || DEFAULT_COLOR
+      colorString: this.props.color || DEFAULT_COLOR,
     };
   }
 
@@ -28,11 +28,11 @@ export class ColorPickerPopover extends React.Component<IProps, any> {
   }
 
   setColor(color) {
-    let newColor = tinycolor(color);
+    const newColor = tinycolor(color);
     if (newColor.isValid()) {
       this.setState({
         color: newColor.toString(),
-        colorString: newColor.toString()
+        colorString: newColor.toString(),
       });
       this.props.onColorSelect(color);
     }
@@ -43,20 +43,20 @@ export class ColorPickerPopover extends React.Component<IProps, any> {
   }
 
   spectrumColorSelected(color) {
-    let rgbColor = color.toRgbString();
+    const rgbColor = color.toRgbString();
     this.setColor(rgbColor);
   }
 
   onColorStringChange(e) {
-    let colorString = e.target.value;
+    const colorString = e.target.value;
     this.setState({
-      colorString: colorString
+      colorString: colorString,
     });
 
-    let newColor = tinycolor(colorString);
+    const newColor = tinycolor(colorString);
     if (newColor.isValid()) {
       // Update only color state
-      let newColorString = newColor.toString();
+      const newColorString = newColor.toString();
       this.setState({
         color: newColorString,
       });
@@ -65,17 +65,17 @@ export class ColorPickerPopover extends React.Component<IProps, any> {
   }
 
   onColorStringBlur(e) {
-    let colorString = e.target.value;
+    const colorString = e.target.value;
     this.setColor(colorString);
   }
 
   componentDidMount() {
     this.pickerNavElem.find('li:first').addClass('active');
-    this.pickerNavElem.on('show', (e) => {
+    this.pickerNavElem.on('show', e => {
       // use href attr (#name => name)
-      let tab = e.target.hash.slice(1);
+      const tab = e.target.hash.slice(1);
       this.setState({
-        tab: tab
+        tab: tab,
       });
     });
   }
@@ -97,19 +97,24 @@ export class ColorPickerPopover extends React.Component<IProps, any> {
       <div className="gf-color-picker">
         <ul className="nav nav-tabs" id="colorpickernav" ref={this.setPickerNavElem.bind(this)}>
           <li className="gf-tabs-item-colorpicker">
-            <a href="#palette" data-toggle="tab">Colors</a>
+            <a href="#palette" data-toggle="tab">
+              Colors
+            </a>
           </li>
           <li className="gf-tabs-item-colorpicker">
-            <a href="#spectrum" data-toggle="tab">Custom</a>
+            <a href="#spectrum" data-toggle="tab">
+              Custom
+            </a>
           </li>
         </ul>
-        <div className="gf-color-picker__body">
-          {currentTab}
-        </div>
+        <div className="gf-color-picker__body">{currentTab}</div>
         <div>
-          <input className="gf-form-input gf-form-input--small" value={this.state.colorString}
-            onChange={this.onColorStringChange.bind(this)} onBlur={this.onColorStringBlur.bind(this)}>
-          </input>
+          <input
+            className="gf-form-input gf-form-input--small"
+            value={this.state.colorString}
+            onChange={this.onColorStringChange.bind(this)}
+            onBlur={this.onColorStringBlur.bind(this)}
+          />
         </div>
       </div>
     );

+ 2 - 2
public/app/core/components/colorpicker/SeriesColorPicker.tsx

@@ -2,13 +2,13 @@ import React from 'react';
 import { ColorPickerPopover } from './ColorPickerPopover';
 import { react2AngularDirective } from 'app/core/utils/react2angular';
 
-export interface IProps {
+export interface Props {
   series: any;
   onColorChange: (color: string) => void;
   onToggleAxis: () => void;
 }
 
-export class SeriesColorPicker extends React.Component<IProps, any> {
+export class SeriesColorPicker extends React.Component<Props, any> {
   constructor(props) {
     super(props);
     this.onColorChange = this.onColorChange.bind(this);

+ 14 - 14
public/app/core/components/colorpicker/SpectrumPicker.tsx

@@ -3,13 +3,13 @@ import _ from 'lodash';
 import $ from 'jquery';
 import 'vendor/spectrum';
 
-export interface IProps {
+export interface Props {
   color: string;
   options: object;
   onColorSelect: (c: string) => void;
 }
 
-export class SpectrumPicker extends React.Component<IProps, any> {
+export class SpectrumPicker extends React.Component<Props, any> {
   elem: any;
   isMoving: boolean;
 
@@ -29,14 +29,17 @@ export class SpectrumPicker extends React.Component<IProps, any> {
   }
 
   componentDidMount() {
-    let spectrumOptions = _.assignIn({
-      flat: true,
-      showAlpha: true,
-      showButtons: false,
-      color: this.props.color,
-      appendTo: this.elem,
-      move: this.onSpectrumMove,
-    }, this.props.options);
+    const spectrumOptions = _.assignIn(
+      {
+        flat: true,
+        showAlpha: true,
+        showButtons: false,
+        color: this.props.color,
+        appendTo: this.elem,
+        move: this.onSpectrumMove,
+      },
+      this.props.options
+    );
 
     this.elem.spectrum(spectrumOptions);
     this.elem.spectrum('show');
@@ -64,9 +67,6 @@ export class SpectrumPicker extends React.Component<IProps, any> {
   }
 
   render() {
-    return (
-      <div className="spectrum-container" ref={this.setComponentElem}></div>
-    );
+    return <div className="spectrum-container" ref={this.setComponentElem} />;
   }
 }
-

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

@@ -67,7 +67,7 @@ export class FormDropdownCtrl {
 
     // modify typeahead lookup
     // this = typeahead
-    var typeahead = this.inputElement.data('typeahead');
+    const typeahead = this.inputElement.data('typeahead');
     typeahead.lookup = function() {
       this.query = this.$element.val() || '';
       this.source(this.query, this.process.bind(this));
@@ -100,7 +100,7 @@ export class FormDropdownCtrl {
   }
 
   getOptionsInternal(query) {
-    var result = this.getOptions({ $query: query });
+    const result = this.getOptions({ $query: query });
     if (this.isPromiseLike(result)) {
       return result;
     }
@@ -118,7 +118,7 @@ export class FormDropdownCtrl {
       // if we have text use it
       if (this.lookupText) {
         this.getOptionsInternal('').then(options => {
-          var item = _.find(options, { value: this.model });
+          const item = _.find(options, { value: this.model });
           this.updateDisplay(item ? item.text : this.model);
         });
       } else {
@@ -132,7 +132,7 @@ export class FormDropdownCtrl {
       this.optionCache = options;
 
       // extract texts
-      let optionTexts = _.map(options, op => {
+      const optionTexts = _.map(options, op => {
         return _.escape(op.text);
       });
 
@@ -186,7 +186,7 @@ export class FormDropdownCtrl {
     }
 
     this.$scope.$apply(() => {
-      var option = _.find(this.optionCache, { text: text });
+      const option = _.find(this.optionCache, { text: text });
 
       if (option) {
         if (_.isObject(this.model)) {
@@ -228,7 +228,7 @@ export class FormDropdownCtrl {
     this.linkElement.hide();
     this.linkMode = false;
 
-    var typeahead = this.inputElement.data('typeahead');
+    const typeahead = this.inputElement.data('typeahead');
     if (typeahead) {
       this.inputElement.val('');
       typeahead.lookup();

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

@@ -140,7 +140,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
         }
 
         // close all drops
-        for (let drop of Drop.drops) {
+        for (const drop of Drop.drops) {
           drop.destroy();
         }
       });

+ 7 - 7
public/app/core/components/info_popover.ts

@@ -8,10 +8,10 @@ export function infoPopover() {
     template: '<i class="fa fa-info-circle"></i>',
     transclude: true,
     link: function(scope, elem, attrs, ctrl, transclude) {
-      let offset = attrs.offset || '0 -10px';
-      let position = attrs.position || 'right middle';
+      const offset = attrs.offset || '0 -10px';
+      const position = attrs.position || 'right middle';
       let classes = 'drop-help drop-hide-out-of-bounds';
-      let openOn = 'hover';
+      const openOn = 'hover';
 
       elem.addClass('gf-form-help-icon');
 
@@ -24,14 +24,14 @@ export function infoPopover() {
       }
 
       transclude(function(clone, newScope) {
-        let content = document.createElement('div');
+        const content = document.createElement('div');
         content.className = 'markdown-html';
 
         _.each(clone, node => {
           content.appendChild(node);
         });
 
-        let dropOptions = {
+        const dropOptions = {
           target: elem[0],
           content: content,
           position: position,
@@ -52,9 +52,9 @@ export function infoPopover() {
 
         // Create drop in next digest after directive content is rendered.
         scope.$applyAsync(() => {
-          let drop = new Drop(dropOptions);
+          const drop = new Drop(dropOptions);
 
-          let unbind = scope.$on('$destroy', function() {
+          const unbind = scope.$on('$destroy', function() {
             drop.destroy();
             unbind();
           });

+ 8 - 8
public/app/core/components/manage_dashboards/manage_dashboards.ts

@@ -103,10 +103,10 @@ export class ManageDashboardsCtrl {
 
     this.sections = result;
 
-    for (let section of this.sections) {
+    for (const section of this.sections) {
       section.checked = false;
 
-      for (let dashboard of section.items) {
+      for (const dashboard of section.items) {
         dashboard.checked = false;
       }
     }
@@ -119,7 +119,7 @@ export class ManageDashboardsCtrl {
   selectionChanged() {
     let selectedDashboards = 0;
 
-    for (let section of this.sections) {
+    for (const section of this.sections) {
       selectedDashboards += _.filter(section.items, { checked: true }).length;
     }
 
@@ -129,7 +129,7 @@ export class ManageDashboardsCtrl {
   }
 
   getFoldersAndDashboardsToDelete() {
-    let selectedDashboards = {
+    const selectedDashboards = {
       folders: [],
       dashboards: [],
     };
@@ -148,7 +148,7 @@ export class ManageDashboardsCtrl {
 
   getFolderIds(sections) {
     const ids = [];
-    for (let s of sections) {
+    for (const s of sections) {
       if (s.checked) {
         ids.push(s.id);
       }
@@ -191,7 +191,7 @@ export class ManageDashboardsCtrl {
   }
 
   getDashboardsToMove() {
-    let selectedDashboards = [];
+    const selectedDashboards = [];
 
     for (const section of this.sections) {
       const selected = _.filter(section.items, { checked: true });
@@ -238,7 +238,7 @@ export class ManageDashboardsCtrl {
   }
 
   onTagFilterChange() {
-    var res = this.filterByTag(this.selectedTagFilter.term);
+    const res = this.filterByTag(this.selectedTagFilter.term);
     this.selectedTagFilter = this.tagFilterOptions[0];
     return res;
   }
@@ -264,7 +264,7 @@ export class ManageDashboardsCtrl {
   }
 
   onSelectAllChanged() {
-    for (let section of this.sections) {
+    for (const section of this.sections) {
       if (!section.hideHeader) {
         section.checked = this.selectAllChecked;
       }

+ 3 - 3
public/app/core/components/scroll/scroll.ts

@@ -17,7 +17,7 @@ export function geminiScrollbar() {
     restrict: 'A',
     link: function(scope, elem, attrs) {
       let scrollRoot = elem.parent();
-      let scroller = elem;
+      const scroller = elem;
 
       if (attrs.grafanaScrollbar && attrs.grafanaScrollbar === 'scrollonroot') {
         scrollRoot = scroller;
@@ -27,7 +27,7 @@ export function geminiScrollbar() {
       $(scrollBarHTML).appendTo(scrollRoot);
       elem.addClass(scrollerClass);
 
-      let scrollParams = {
+      const scrollParams = {
         root: scrollRoot[0],
         scroller: scroller[0],
         bar: '.baron__bar',
@@ -37,7 +37,7 @@ export function geminiScrollbar() {
         direction: 'v',
       };
 
-      let scrollbar = baron(scrollParams);
+      const scrollbar = baron(scrollParams);
 
       let lastPos = 0;
 

+ 1 - 1
public/app/core/components/search/SearchResult.tsx

@@ -54,7 +54,7 @@ export class SearchResultSection extends React.Component<SectionProps, any> {
   };
 
   render() {
-    let collapseClassNames = classNames({
+    const collapseClassNames = classNames({
       fa: true,
       'fa-plus': !this.props.section.expanded,
       'fa-minus': this.props.section.expanded,

+ 2 - 2
public/app/core/components/sidemenu/sidemenu.ts

@@ -17,13 +17,13 @@ export class SideMenuCtrl {
     this.isSignedIn = contextSrv.isSignedIn;
     this.user = contextSrv.user;
 
-    let navTree = _.cloneDeep(config.bootData.navTree);
+    const navTree = _.cloneDeep(config.bootData.navTree);
     this.mainLinks = _.filter(navTree, item => !item.hideFromMenu);
     this.bottomNav = _.filter(navTree, item => item.hideFromMenu);
     this.loginUrl = 'login?redirect=' + encodeURIComponent(this.$location.path());
 
     if (contextSrv.user.orgCount > 1) {
-      let profileNode = _.find(this.bottomNav, { id: 'profile' });
+      const profileNode = _.find(this.bottomNav, { id: 'profile' });
       if (profileNode) {
         profileNode.showOrgSwitcher = true;
       }

+ 3 - 3
public/app/core/config.ts

@@ -31,7 +31,7 @@ export class Settings {
   loginError: any;
 
   constructor(options) {
-    var defaults = {
+    const defaults = {
       datasources: {},
       window_title_prefix: 'Grafana - ',
       panels: {},
@@ -51,8 +51,8 @@ export class Settings {
   }
 }
 
-var bootData = (<any>window).grafanaBootData || { settings: {} };
-var options = bootData.settings;
+const bootData = (<any>window).grafanaBootData || { settings: {} };
+const options = bootData.settings;
 options.bootData = bootData;
 
 const config = new Settings(options);

+ 5 - 5
public/app/core/controllers/inspect_ctrl.ts

@@ -6,7 +6,7 @@ import coreModule from '../core_module';
 export class InspectCtrl {
   /** @ngInject */
   constructor($scope, $sanitize) {
-    var model = $scope.inspector;
+    const model = $scope.inspector;
 
     $scope.init = function() {
       $scope.editor = { index: 0 };
@@ -53,10 +53,10 @@ export class InspectCtrl {
     };
   }
   getParametersFromQueryString(queryString) {
-    var result = [];
-    var parameters = queryString.split('&');
-    for (var i = 0; i < parameters.length; i++) {
-      var keyValue = parameters[i].split('=');
+    const result = [];
+    const parameters = queryString.split('&');
+    for (let i = 0; i < parameters.length; i++) {
+      const keyValue = parameters[i].split('=');
       if (keyValue[1].length > 0) {
         result.push({
           key: keyValue[0],

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

@@ -9,7 +9,7 @@ export class JsonEditorCtrl {
     $scope.canCopy = $scope.enableCopy;
 
     $scope.update = function() {
-      var newObject = angular.fromJson($scope.json);
+      const newObject = angular.fromJson($scope.json);
       $scope.updateHandler(newObject, $scope.object);
     };
 

+ 21 - 13
public/app/core/controllers/login_ctrl.ts

@@ -13,6 +13,7 @@ export class LoginCtrl {
 
     $scope.command = {};
     $scope.result = '';
+    $scope.loggingIn = false;
 
     contextSrv.sidemenu = false;
 
@@ -45,8 +46,8 @@ export class LoginCtrl {
     };
 
     $scope.changeView = function() {
-      let loginView = document.querySelector('#login-view');
-      let changePasswordView = document.querySelector('#change-password-view');
+      const loginView = document.querySelector('#login-view');
+      const changePasswordView = document.querySelector('#change-password-view');
 
       loginView.className += ' add';
       setTimeout(() => {
@@ -105,20 +106,27 @@ export class LoginCtrl {
       if (!$scope.loginForm.$valid) {
         return;
       }
-
-      backendSrv.post('/login', $scope.formModel).then(function(result) {
-        $scope.result = result;
-
-        if ($scope.formModel.password !== 'admin' || $scope.ldapEnabled || $scope.authProxyEnabled) {
-          $scope.toGrafana();
-          return;
-        }
-        $scope.changeView();
-      });
+      $scope.loggingIn = true;
+
+      backendSrv
+        .post('/login', $scope.formModel)
+        .then(function(result) {
+          $scope.result = result;
+
+          if ($scope.formModel.password !== 'admin' || $scope.ldapEnabled || $scope.authProxyEnabled) {
+            $scope.toGrafana();
+            return;
+          } else {
+            $scope.changeView();
+          }
+        })
+        .catch(() => {
+          $scope.loggingIn = false;
+        });
     };
 
     $scope.toGrafana = function() {
-      var params = $location.search();
+      const params = $location.search();
 
       if (params.redirect && params.redirect[0] === '/') {
         window.location.href = config.appSubUrl + params.redirect;

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

@@ -7,7 +7,7 @@ export class ResetPasswordCtrl {
     $scope.formModel = {};
     $scope.mode = 'send';
 
-    var params = $location.search();
+    const params = $location.search();
     if (params.code) {
       $scope.mode = 'reset';
       $scope.formModel.code = params.code;

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

@@ -9,7 +9,7 @@ export class SignUpCtrl {
 
     $scope.formModel = {};
 
-    var params = $location.search();
+    const params = $location.search();
 
     // validate email is semi ok
     if (params.email && !params.email.match(/^\S+@\S+$/)) {

+ 16 - 16
public/app/core/directives/dropdown_typeahead.ts

@@ -4,12 +4,12 @@ import coreModule from '../core_module';
 
 /** @ngInject */
 export function dropdownTypeahead($compile) {
-  let inputTemplate =
+  const inputTemplate =
     '<input type="text"' +
     ' class="gf-form-input input-medium tight-form-input"' +
     ' spellcheck="false" style="display:none"></input>';
 
-  let buttonTemplate =
+  const buttonTemplate =
     '<a class="gf-form-label tight-form-func dropdown-toggle"' +
     ' tabindex="1" gf-dropdown="menuItems" data-toggle="dropdown"' +
     ' data-placement="top"><i class="fa fa-plus"></i></a>';
@@ -21,8 +21,8 @@ export function dropdownTypeahead($compile) {
       model: '=ngModel',
     },
     link: function($scope, elem, attrs) {
-      let $input = $(inputTemplate);
-      let $button = $(buttonTemplate);
+      const $input = $(inputTemplate);
+      const $button = $(buttonTemplate);
       $input.appendTo(elem);
       $button.appendTo(elem);
 
@@ -42,7 +42,7 @@ export function dropdownTypeahead($compile) {
         });
       }
 
-      let typeaheadValues = _.reduce(
+      const typeaheadValues = _.reduce(
         $scope.menuItems,
         function(memo, value, index) {
           if (!value.submenu) {
@@ -60,8 +60,8 @@ export function dropdownTypeahead($compile) {
       );
 
       $scope.menuItemSelected = function(index, subIndex) {
-        let menuItem = $scope.menuItems[index];
-        let payload: any = { $item: menuItem };
+        const menuItem = $scope.menuItems[index];
+        const payload: any = { $item: menuItem };
         if (menuItem.submenu && subIndex !== void 0) {
           payload.$subItem = menuItem.submenu[subIndex];
         }
@@ -74,7 +74,7 @@ export function dropdownTypeahead($compile) {
         minLength: 1,
         items: 10,
         updater: function(value) {
-          let result: any = {};
+          const result: any = {};
           _.each($scope.menuItems, function(menuItem) {
             _.each(menuItem.submenu, function(submenuItem) {
               if (value === menuItem.text + ' ' + submenuItem.text) {
@@ -124,10 +124,10 @@ export function dropdownTypeahead($compile) {
 
 /** @ngInject */
 export function dropdownTypeahead2($compile) {
-  let inputTemplate =
+  const inputTemplate =
     '<input type="text"' + ' class="gf-form-input"' + ' spellcheck="false" style="display:none"></input>';
 
-  let buttonTemplate =
+  const buttonTemplate =
     '<a class="gf-form-input dropdown-toggle"' +
     ' tabindex="1" gf-dropdown="menuItems" data-toggle="dropdown"' +
     ' data-placement="top"><i class="fa fa-plus"></i></a>';
@@ -139,8 +139,8 @@ export function dropdownTypeahead2($compile) {
       model: '=ngModel',
     },
     link: function($scope, elem, attrs) {
-      let $input = $(inputTemplate);
-      let $button = $(buttonTemplate);
+      const $input = $(inputTemplate);
+      const $button = $(buttonTemplate);
       $input.appendTo(elem);
       $button.appendTo(elem);
 
@@ -160,7 +160,7 @@ export function dropdownTypeahead2($compile) {
         });
       }
 
-      let typeaheadValues = _.reduce(
+      const typeaheadValues = _.reduce(
         $scope.menuItems,
         function(memo, value, index) {
           if (!value.submenu) {
@@ -178,8 +178,8 @@ export function dropdownTypeahead2($compile) {
       );
 
       $scope.menuItemSelected = function(index, subIndex) {
-        let menuItem = $scope.menuItems[index];
-        let payload: any = { $item: menuItem };
+        const menuItem = $scope.menuItems[index];
+        const payload: any = { $item: menuItem };
         if (menuItem.submenu && subIndex !== void 0) {
           payload.$subItem = menuItem.submenu[subIndex];
         }
@@ -192,7 +192,7 @@ export function dropdownTypeahead2($compile) {
         minLength: 1,
         items: 10,
         updater: function(value) {
-          let result: any = {};
+          const result: any = {};
           _.each($scope.menuItems, function(menuItem) {
             _.each(menuItem.submenu, function(submenuItem) {
               if (value === menuItem.text + ' ' + submenuItem.text) {

+ 14 - 14
public/app/core/directives/metric_segment.ts

@@ -4,16 +4,16 @@ import coreModule from '../core_module';
 
 /** @ngInject */
 export function metricSegment($compile, $sce) {
-  let inputTemplate =
+  const inputTemplate =
     '<input type="text" data-provide="typeahead" ' +
     ' class="gf-form-input input-medium"' +
     ' spellcheck="false" style="display:none"></input>';
 
-  let linkTemplate =
+  const linkTemplate =
     '<a class="gf-form-label" ng-class="segment.cssClass" ' +
     'tabindex="1" give-focus="segment.focus" ng-bind-html="segment.html"></a>';
 
-  let selectTemplate =
+  const selectTemplate =
     '<a class="gf-form-input gf-form-input--dropdown" ng-class="segment.cssClass" ' +
     'tabindex="1" give-focus="segment.focus" ng-bind-html="segment.html"></a>';
 
@@ -25,13 +25,13 @@ export function metricSegment($compile, $sce) {
       debounce: '@',
     },
     link: function($scope, elem) {
-      let $input = $(inputTemplate);
-      let segment = $scope.segment;
-      let $button = $(segment.selectMode ? selectTemplate : linkTemplate);
+      const $input = $(inputTemplate);
+      const segment = $scope.segment;
+      const $button = $(segment.selectMode ? selectTemplate : linkTemplate);
       let options = null;
       let cancelBlur = null;
       let linkMode = true;
-      let debounceLookup = $scope.debounce;
+      const debounceLookup = $scope.debounce;
 
       $input.appendTo(elem);
       $button.appendTo(elem);
@@ -44,7 +44,7 @@ export function metricSegment($compile, $sce) {
         value = _.unescape(value);
 
         $scope.$apply(function() {
-          let selected = _.find($scope.altSegments, { value: value });
+          const selected = _.find($scope.altSegments, { value: value });
           if (selected) {
             segment.value = selected.value;
             segment.html = selected.html || selected.value;
@@ -141,10 +141,10 @@ export function metricSegment($compile, $sce) {
         matcher: $scope.matcher,
       });
 
-      let typeahead = $input.data('typeahead');
+      const typeahead = $input.data('typeahead');
       typeahead.lookup = function() {
         this.query = this.$element.val() || '';
-        let items = this.source(this.query, $.proxy(this.process, this));
+        const items = this.source(this.query, $.proxy(this.process, this));
         return items ? this.process(items) : items;
       };
 
@@ -169,7 +169,7 @@ export function metricSegment($compile, $sce) {
 
         linkMode = false;
 
-        let typeahead = $input.data('typeahead');
+        const typeahead = $input.data('typeahead');
         if (typeahead) {
           $input.val('');
           typeahead.lookup();
@@ -200,8 +200,8 @@ export function metricSegmentModel(uiSegmentSrv, $q) {
         let cachedOptions;
 
         $scope.valueToSegment = function(value) {
-          let option = _.find($scope.options, { value: value });
-          let segment = {
+          const option = _.find($scope.options, { value: value });
+          const segment = {
             cssClass: attrs.cssClass,
             custom: attrs.custom,
             value: option ? option.text : value,
@@ -234,7 +234,7 @@ export function metricSegmentModel(uiSegmentSrv, $q) {
 
         $scope.onSegmentChange = function() {
           if (cachedOptions) {
-            let option = _.find(cachedOptions, { text: $scope.segment.value });
+            const option = _.find(cachedOptions, { text: $scope.segment.value });
             if (option && option.value !== $scope.property) {
               $scope.property = option.value;
             } else if (attrs.custom !== 'false') {

Vissa filer visades inte eftersom för många filer har ändrats